Skip to content

Commit 29b543f

Browse files
committed
handle mocking multiple classes
1 parent e27a765 commit 29b543f

11 files changed

+238
-20
lines changed

Diff for: src/Type/PHPUnit/CreateMockDynamicReturnTypeExtension.php

+17-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\MethodReflection;
88
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Constant\ConstantArrayType;
910
use PHPStan\Type\Constant\ConstantStringType;
1011
use PHPStan\Type\ObjectType;
1112
use PHPStan\Type\Type;
@@ -31,6 +32,7 @@ public function getClass(): string
3132

3233
public function isMethodSupported(MethodReflection $methodReflection): bool
3334
{
35+
$name = $methodReflection->getName();
3436
return array_key_exists($methodReflection->getName(), $this->methods);
3537
}
3638

@@ -42,15 +44,25 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
4244
return $parametersAcceptor->getReturnType();
4345
}
4446
$argType = $scope->getType($methodCall->args[$argumentIndex]->value);
45-
if (!$argType instanceof ConstantStringType) {
46-
return $parametersAcceptor->getReturnType();
47+
48+
$types = [];
49+
if ($argType instanceof ConstantStringType) {
50+
$types[] = new ObjectType($argType->getValue());
51+
}
52+
53+
if ($argType instanceof ConstantArrayType) {
54+
$types = array_map(function (Type $argType): ObjectType {
55+
return new ObjectType($argType->getValue());
56+
}, $argType->getValueTypes());
4757
}
4858

49-
$class = $argType->getValue();
59+
if (count($types) === 0) {
60+
return $parametersAcceptor->getReturnType();
61+
}
5062

5163
return TypeCombinator::intersect(
52-
new ObjectType($class),
53-
$parametersAcceptor->getReturnType()
64+
$parametersAcceptor->getReturnType(),
65+
...$types
5466
);
5567
}
5668

Diff for: src/Type/PHPUnit/GetMockBuilderDynamicReturnTypeExtension.php

+15-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\MethodReflection;
88
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Constant\ConstantArrayType;
910
use PHPStan\Type\Constant\ConstantStringType;
1011
use PHPStan\Type\Type;
1112
use PHPStan\Type\TypeWithClassName;
@@ -30,18 +31,26 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
3031
if (count($methodCall->args) === 0) {
3132
return $mockBuilderType;
3233
}
34+
if (!$mockBuilderType instanceof TypeWithClassName) {
35+
throw new \PHPStan\ShouldNotHappenException();
36+
}
37+
3338
$argType = $scope->getType($methodCall->args[0]->value);
34-
if (!$argType instanceof ConstantStringType) {
35-
return $mockBuilderType;
39+
if ($argType instanceof ConstantStringType) {
40+
$class = $argType->getValue();
41+
42+
return new MockBuilderType($mockBuilderType, $class);
3643
}
3744

38-
$class = $argType->getValue();
45+
if ($argType instanceof ConstantArrayType) {
46+
$classes = array_map(function (Type $argType): string {
47+
return $argType->getValue();
48+
}, $argType->getValueTypes());
3949

40-
if (!$mockBuilderType instanceof TypeWithClassName) {
41-
throw new \PHPStan\ShouldNotHappenException();
50+
return new MockBuilderType($mockBuilderType, ...$classes);
4251
}
4352

44-
return new MockBuilderType($mockBuilderType, $class);
53+
return $mockBuilderType;
4554
}
4655

4756
}

Diff for: src/Type/PHPUnit/MockBuilderDynamicReturnTypeExtension.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
6060
if (!$calledOnType instanceof MockBuilderType) {
6161
return $parametersAcceptor->getReturnType();
6262
}
63+
$types = array_map(function (string $type): ObjectType {
64+
return new ObjectType($type);
65+
}, $calledOnType->getMockedClasses());
6366

6467
return TypeCombinator::intersect(
65-
new ObjectType($calledOnType->getMockedClass()),
66-
$parametersAcceptor->getReturnType()
68+
$parametersAcceptor->getReturnType(),
69+
...$types
6770
);
6871
}
6972

Diff for: src/Type/PHPUnit/MockBuilderType.php

+10-7
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,29 @@
88
class MockBuilderType extends \PHPStan\Type\ObjectType
99
{
1010

11-
/** @var string */
12-
private $mockedClass;
11+
/** @var array<string> */
12+
private $mockedClasses;
1313

1414
public function __construct(
1515
TypeWithClassName $mockBuilderType,
16-
string $mockedClass
16+
string ...$mockedClasses
1717
)
1818
{
1919
parent::__construct($mockBuilderType->getClassName());
20-
$this->mockedClass = $mockedClass;
20+
$this->mockedClasses = $mockedClasses;
2121
}
2222

23-
public function getMockedClass(): string
23+
/**
24+
* @return array<string>
25+
*/
26+
public function getMockedClasses(): array
2427
{
25-
return $this->mockedClass;
28+
return $this->mockedClasses;
2629
}
2730

2831
public function describe(VerbosityLevel $level): string
2932
{
30-
return sprintf('%s<%s>', parent::describe($level), $this->mockedClass);
33+
return sprintf('%s<%s>', parent::describe($level), implode('&', $this->mockedClasses));
3134
}
3235

3336
}

Diff for: tests/Type/PHPUnit/CreateMockExtensionTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\PHPUnit;
4+
5+
use ExampleTestCase\BarInterface;
6+
use ExampleTestCase\FooInterface;
7+
use Iterator;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
10+
class CreateMockExtensionTest extends ExtensionTestCase
11+
{
12+
13+
/**
14+
* @dataProvider getProvider
15+
* @param string $expression
16+
* @param string $type
17+
*/
18+
public function testCreateMock(string $expression, string $type): void
19+
{
20+
$this->processFile(
21+
__DIR__ . '/data/create-mock.php',
22+
$expression,
23+
$type,
24+
[new CreateMockDynamicReturnTypeExtension(), new GetMockBuilderDynamicReturnTypeExtension()]
25+
);
26+
}
27+
28+
/**
29+
* @return Iterator<mixed>
30+
*/
31+
public function getProvider(): Iterator
32+
{
33+
yield ['$simpleInterface', implode('&', [FooInterface::class, MockObject::class])];
34+
yield ['$doubleInterface', implode('&', [BarInterface::class, FooInterface::class, MockObject::class])];
35+
}
36+
37+
}

Diff for: tests/Type/PHPUnit/ExtensionTestCase.php

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PhpParser\PrettyPrinter\Standard;
7+
use PHPStan\Analyser\NodeScopeResolver;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Analyser\ScopeContext;
10+
use PHPStan\Broker\AnonymousClassNameHelper;
11+
use PHPStan\Cache\Cache;
12+
use PHPStan\File\FileHelper;
13+
use PHPStan\Node\VirtualNode;
14+
use PHPStan\PhpDoc\PhpDocNodeResolver;
15+
use PHPStan\PhpDoc\PhpDocStringResolver;
16+
use PHPStan\Testing\TestCase;
17+
use PHPStan\Type\FileTypeMapper;
18+
use PHPStan\Type\VerbosityLevel;
19+
20+
abstract class ExtensionTestCase extends TestCase
21+
{
22+
23+
protected function processFile(
24+
string $file,
25+
string $expression,
26+
string $type,
27+
array $extensions
28+
): void
29+
{
30+
$broker = $this->createBroker($extensions);
31+
$parser = $this->getParser();
32+
$currentWorkingDirectory = $this->getCurrentWorkingDirectory();
33+
$fileHelper = new FileHelper($currentWorkingDirectory);
34+
$typeSpecifier = $this->createTypeSpecifier(new Standard(), $broker);
35+
/** @var \PHPStan\PhpDoc\PhpDocStringResolver $phpDocStringResolver */
36+
$phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class);
37+
$resolver = new NodeScopeResolver(
38+
$broker,
39+
$parser,
40+
new FileTypeMapper(
41+
$parser,
42+
$phpDocStringResolver,
43+
self::getContainer()->getByType(PhpDocNodeResolver::class),
44+
$this->createMock(Cache::class),
45+
$this->createMock(AnonymousClassNameHelper::class)
46+
),
47+
$fileHelper,
48+
$typeSpecifier,
49+
true,
50+
true,
51+
true,
52+
[],
53+
[]
54+
);
55+
$resolver->setAnalysedFiles([$fileHelper->normalizePath($file)]);
56+
57+
$run = false;
58+
$resolver->processNodes(
59+
$parser->parseFile($file),
60+
$this->createScopeFactory($broker, $typeSpecifier)->create(ScopeContext::create($file)),
61+
function (Node $node, Scope $scope) use ($expression, $type, &$run): void {
62+
if ($node instanceof VirtualNode) {
63+
return;
64+
}
65+
if ((new Standard())->prettyPrint([$node]) !== 'die') {
66+
return;
67+
}
68+
/** @var \PhpParser\Node\Stmt\Expression $expNode */
69+
$expNode = $this->getParser()->parseString(sprintf('<?php %s;', $expression))[0];
70+
self::assertSame($type, $scope->getType($expNode->expr)->describe(VerbosityLevel::typeOnly()));
71+
$run = true;
72+
}
73+
);
74+
self::assertTrue($run);
75+
}
76+
77+
}

Diff for: tests/Type/PHPUnit/MockBuilderTypeExtensionTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\PHPUnit;
4+
5+
use ExampleTestCase\BarInterface;
6+
use ExampleTestCase\FooInterface;
7+
use Iterator;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
10+
class MockBuilderTypeExtensionTest extends ExtensionTestCase
11+
{
12+
13+
/**
14+
* @dataProvider getProvider
15+
* @param string $expression
16+
* @param string $type
17+
*/
18+
public function testMockBuilder(string $expression, string $type): void
19+
{
20+
$this->processFile(
21+
__DIR__ . '/data/mock-builder.php',
22+
$expression,
23+
$type,
24+
[new MockBuilderDynamicReturnTypeExtension(), new GetMockBuilderDynamicReturnTypeExtension()]
25+
);
26+
}
27+
28+
/**
29+
* @return Iterator<mixed>
30+
*/
31+
public function getProvider(): Iterator
32+
{
33+
yield ['$simpleInterface', implode('&', [FooInterface::class, MockObject::class])];
34+
yield ['$doubleInterface', implode('&', [BarInterface::class, FooInterface::class, MockObject::class])];
35+
}
36+
37+
}

Diff for: tests/Type/PHPUnit/data/BarInterface.php

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
4+
namespace ExampleTestCase;
5+
6+
7+
interface BarInterface
8+
{
9+
}

Diff for: tests/Type/PHPUnit/data/FooInterface.php

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
4+
namespace ExampleTestCase;
5+
6+
7+
interface FooInterface
8+
{
9+
}

Diff for: tests/Type/PHPUnit/data/create-mock.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
$test = new class () extends TestCase {};
6+
7+
$reflection = new ReflectionObject($test);
8+
$reflection->getMethod('createMock')->setAccessible(true);
9+
$simpleInterface = $test->createMock(\ExampleTestCase\FooInterface::class);
10+
$doubleInterface = $test->createMock([\ExampleTestCase\FooInterface::class, \ExampleTestCase\BarInterface::class]);
11+
12+
die;

Diff for: tests/Type/PHPUnit/data/mock-builder.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
$test = new class () extends TestCase {};
6+
7+
$simpleInterface = $test->getMockBuilder(\ExampleTestCase\FooInterface::class)->getMock();
8+
$doubleInterface = $test->getMockBuilder([\ExampleTestCase\FooInterface::class, \ExampleTestCase\BarInterface::class])->getMock();
9+
10+
die;

0 commit comments

Comments
 (0)