Skip to content

Commit 425904d

Browse files
Support for createMockForIntersectionOfInterfaces
1 parent 202afe9 commit 425904d

File tree

4 files changed

+119
-0
lines changed

4 files changed

+119
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ services:
4747
class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension
4848
tags:
4949
- phpstan.broker.dynamicMethodReturnTypeExtension
50+
-
51+
class: PHPStan\Type\PHPUnit\MockForIntersectionDynamicReturnTypeExtension
52+
tags:
53+
- phpstan.broker.dynamicMethodReturnTypeExtension
5054
-
5155
class: PHPStan\Rules\PHPUnit\CoversHelper
5256
-
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\PHPUnit;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
9+
use PHPStan\Type\ObjectType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
12+
use PHPUnit\Framework\MockObject\MockObject;
13+
use PHPUnit\Framework\MockObject\Stub;
14+
use PHPUnit\Framework\TestCase;
15+
use function in_array;
16+
17+
class MockForIntersectionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
18+
{
19+
20+
public function getClass(): string
21+
{
22+
return TestCase::class;
23+
}
24+
25+
public function isMethodSupported(MethodReflection $methodReflection): bool
26+
{
27+
return in_array(
28+
$methodReflection->getName(),
29+
[
30+
'createMockForIntersectionOfInterfaces',
31+
'createStubForIntersectionOfInterfaces',
32+
],
33+
true,
34+
);
35+
}
36+
37+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
38+
{
39+
$args = $methodCall->getArgs();
40+
if (!isset($args[0])) {
41+
return null;
42+
}
43+
44+
$interfaces = $scope->getType($args[0]->value);
45+
$constantArrays = $interfaces->getConstantArrays();
46+
if (count($constantArrays) !== 1) {
47+
return null;
48+
}
49+
50+
$constantArray = $constantArrays[0];
51+
if (count($constantArray->getOptionalKeys()) > 0) {
52+
return null;
53+
}
54+
55+
$result = [];
56+
if ($methodReflection->getName() === 'createMockForIntersectionOfInterfaces') {
57+
$result[] = new ObjectType(MockObject::class);
58+
} else {
59+
$result[] = new ObjectType(Stub::class);
60+
}
61+
62+
63+
foreach ($constantArray->getValueTypes() as $valueType) {
64+
if (!$valueType->isClassString()->yes()) {
65+
return null;
66+
}
67+
68+
$values = $valueType->getConstantScalarValues();
69+
if (count($values) !== 1) {
70+
return null;
71+
}
72+
73+
$result[] = new ObjectType($values[0]);
74+
}
75+
76+
return TypeCombinator::intersect(...$result);
77+
}
78+
79+
}

tests/Rules/PHPUnit/MockMethodCallRuleTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\TestCase;
78
use const PHP_VERSION_ID;
89

910
/**
@@ -34,6 +35,17 @@ public function testRule(): void
3435
],
3536
];
3637

38+
if (method_exists(TestCase::class, 'createMockForIntersectionOfInterfaces')) {
39+
$expectedErrors[] = [
40+
'Trying to mock an undefined method bazMethod() on class MockMethodCall\FooInterface&MockMethodCall\BarInterface.',
41+
49,
42+
];
43+
$expectedErrors[] = [
44+
'Trying to mock an undefined method bazMethod() on class MockMethodCall\FooInterface&MockMethodCall\BarInterface.',
45+
57,
46+
];
47+
}
48+
3749
$this->analyse([__DIR__ . '/data/mock-method-call.php'], $expectedErrors);
3850
}
3951

tests/Rules/PHPUnit/data/mock-method-call.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ public function testMockObject(\PHPUnit\Framework\MockObject\MockObject $mock)
4141
$mock->method('doFoo');
4242
}
4343

44+
public function testMockForIntersection()
45+
{
46+
$mock = $this->createMockForIntersectionOfInterfaces([FooInterface::class, BarInterface::class]);
47+
$mock->method('fooMethod');
48+
$mock->method('barMethod');
49+
$mock->method('bazMethod');
50+
}
51+
52+
public function testStubForIntersection()
53+
{
54+
$stub = $this->createStubForIntersectionOfInterfaces([FooInterface::class, BarInterface::class]);
55+
$stub->method('fooMethod');
56+
$stub->method('barMethod');
57+
$stub->method('bazMethod');
58+
}
59+
4460
}
4561

4662
class Bar {
@@ -71,3 +87,11 @@ public function testMockFinalClass()
7187
}
7288

7389
}
90+
91+
interface FooInterface {
92+
public function fooMethod(): int;
93+
}
94+
95+
interface BarInterface {
96+
public function barMethod(): string;
97+
}

0 commit comments

Comments
 (0)