Skip to content

Commit 465b649

Browse files
Support for createMockForIntersectionOfInterfaces
1 parent 202afe9 commit 465b649

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-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 count;
16+
use function in_array;
17+
18+
class MockForIntersectionDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
19+
{
20+
21+
public function getClass(): string
22+
{
23+
return TestCase::class;
24+
}
25+
26+
public function isMethodSupported(MethodReflection $methodReflection): bool
27+
{
28+
return in_array(
29+
$methodReflection->getName(),
30+
[
31+
'createMockForIntersectionOfInterfaces',
32+
'createStubForIntersectionOfInterfaces',
33+
],
34+
true,
35+
);
36+
}
37+
38+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
39+
{
40+
$args = $methodCall->getArgs();
41+
if (!isset($args[0])) {
42+
return null;
43+
}
44+
45+
$interfaces = $scope->getType($args[0]->value);
46+
$constantArrays = $interfaces->getConstantArrays();
47+
if (count($constantArrays) !== 1) {
48+
return null;
49+
}
50+
51+
$constantArray = $constantArrays[0];
52+
if (count($constantArray->getOptionalKeys()) > 0) {
53+
return null;
54+
}
55+
56+
$result = [];
57+
if ($methodReflection->getName() === 'createMockForIntersectionOfInterfaces') {
58+
$result[] = new ObjectType(MockObject::class);
59+
} else {
60+
$result[] = new ObjectType(Stub::class);
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((string) $values[0]);
74+
}
75+
76+
return TypeCombinator::intersect(...$result);
77+
}
78+
79+
}

tests/Rules/PHPUnit/MockMethodCallRuleTest.php

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

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\TestCase;
8+
use function method_exists;
79
use const PHP_VERSION_ID;
810

911
/**
@@ -34,6 +36,17 @@ public function testRule(): void
3436
],
3537
];
3638

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

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)