Skip to content

Commit 52aef43

Browse files
Report error when trying to configure a non existing method on MockObject
1 parent 1648b3d commit 52aef43

8 files changed

+264
-0
lines changed

extension.neon

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ parameters:
55
- markTestIncomplete
66
- markTestSkipped
77
stubFiles:
8+
- stubs/InvocationMocker.stub
89
- stubs/MockBuilder.stub
910
- stubs/MockObject.stub
1011
- stubs/TestCase.stub
@@ -26,7 +27,15 @@ services:
2627
class: PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension
2728
tags:
2829
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
30+
-
31+
class: PHPStan\Type\PHPUnit\InvocationMockerDynamicReturnTypeExtension
32+
tags:
33+
- phpstan.broker.dynamicMethodReturnTypeExtension
2934
-
3035
class: PHPStan\Type\PHPUnit\MockBuilderDynamicReturnTypeExtension
3136
tags:
3237
- phpstan.broker.dynamicMethodReturnTypeExtension
38+
-
39+
class: PHPStan\Type\PHPUnit\MockObjectDynamicReturnTypeExtension
40+
tags:
41+
- phpstan.broker.dynamicMethodReturnTypeExtension

rules.neon

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ rules:
22
- PHPStan\Rules\PHPUnit\AssertSameBooleanExpectedRule
33
- PHPStan\Rules\PHPUnit\AssertSameNullExpectedRule
44
- PHPStan\Rules\PHPUnit\AssertSameWithCountRule
5+
- PHPStan\Rules\PHPUnit\MockMethodCallRule
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\Constant\ConstantStringType;
8+
use PHPStan\Type\Generic\GenericObjectType;
9+
use PHPStan\Type\IntersectionType;
10+
use PHPStan\Type\ObjectType;
11+
use PHPUnit\Framework\MockObject\InvocationMocker;
12+
use PHPUnit\Framework\MockObject\MockObject;
13+
14+
/**
15+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall>
16+
*/
17+
class MockMethodCallRule implements \PHPStan\Rules\Rule
18+
{
19+
20+
public function getNodeType(): string
21+
{
22+
return Node\Expr\MethodCall::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
/** @var Node\Expr\MethodCall $node */
28+
$node = $node;
29+
30+
if (!$node->name instanceof Node\Identifier || $node->name->name !== 'method') {
31+
return [];
32+
}
33+
34+
if (count($node->args) < 1) {
35+
return [];
36+
}
37+
38+
$argType = $scope->getType($node->args[0]->value);
39+
if (!($argType instanceof ConstantStringType)) {
40+
return [];
41+
}
42+
43+
$method = $argType->getValue();
44+
$type = $scope->getType($node->var);
45+
46+
if (
47+
$type instanceof IntersectionType
48+
&& in_array(MockObject::class, $type->getReferencedClasses(), true)
49+
&& !$type->hasMethod($method)->yes()
50+
) {
51+
$mockClass = array_filter($type->getReferencedClasses(), function (string $class): bool {
52+
return $class !== MockObject::class;
53+
});
54+
55+
return [
56+
sprintf(
57+
'Trying to mock an undefined method %s() on class %s.',
58+
$method,
59+
\implode('&', $mockClass)
60+
),
61+
];
62+
}
63+
64+
if (
65+
$type instanceof GenericObjectType
66+
&& $type->getClassName() === InvocationMocker::class
67+
&& count($type->getTypes()) > 0
68+
) {
69+
$mockClass = $type->getTypes()[0];
70+
71+
if ($mockClass instanceof ObjectType && !$mockClass->hasMethod($method)->yes()) {
72+
return [
73+
sprintf(
74+
'Trying to mock an undefined method %s() on class %s.',
75+
$method,
76+
$mockClass->getClassName()
77+
),
78+
];
79+
}
80+
}
81+
82+
return [];
83+
}
84+
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Type;
9+
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
10+
11+
class InvocationMockerDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension
12+
{
13+
14+
public function getClass(): string
15+
{
16+
return InvocationMocker::class;
17+
}
18+
19+
public function isMethodSupported(MethodReflection $methodReflection): bool
20+
{
21+
return $methodReflection->getName() !== 'getMatcher';
22+
}
23+
24+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
25+
{
26+
return $scope->getType($methodCall->var);
27+
}
28+
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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\Generic\GenericObjectType;
9+
use PHPStan\Type\IntersectionType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeWithClassName;
12+
use PHPUnit\Framework\MockObject\InvocationMocker;
13+
use PHPUnit\Framework\MockObject\MockObject;
14+
15+
class MockObjectDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension
16+
{
17+
18+
public function getClass(): string
19+
{
20+
return MockObject::class;
21+
}
22+
23+
public function isMethodSupported(MethodReflection $methodReflection): bool
24+
{
25+
return $methodReflection->getName() === 'expects';
26+
}
27+
28+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
29+
{
30+
$type = $scope->getType($methodCall->var);
31+
if (!($type instanceof IntersectionType)) {
32+
return new GenericObjectType(InvocationMocker::class, []);
33+
}
34+
35+
$mockClasses = array_filter($type->getTypes(), function (Type $type): bool {
36+
return !$type instanceof TypeWithClassName || $type->getClassName() === MockObject::class;
37+
});
38+
39+
return new GenericObjectType(InvocationMocker::class, $mockClasses);
40+
}
41+
42+
}

stubs/InvocationMocker.stub

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace PHPUnit\Framework\MockObject\Builder;
4+
5+
use PHPUnit\Framework\MockObject\Stub;
6+
7+
/**
8+
* @template TMockedClass
9+
*/
10+
class InvocationMocker
11+
{
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Rules\Rule;
6+
7+
/**
8+
* @extends \PHPStan\Testing\RuleTestCase<MockMethodCallRule>
9+
*/
10+
class MockMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase
11+
{
12+
13+
protected function getRule(): Rule
14+
{
15+
return new MockMethodCallRule();
16+
}
17+
18+
public function testRule(): void
19+
{
20+
$this->analyse([__DIR__ . '/data/mock-method-call.php'], [
21+
[
22+
'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.',
23+
15,
24+
],
25+
[
26+
'Trying to mock an undefined method doBadThing() on class MockMethodCall\Bar.',
27+
20,
28+
],
29+
]);
30+
}
31+
32+
/**
33+
* @return string[]
34+
*/
35+
public static function getAdditionalConfigFiles(): array
36+
{
37+
return [
38+
__DIR__ . '/../../../extension.neon',
39+
];
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace MockMethodCall;
4+
5+
class Foo extends \PHPUnit\Framework\TestCase
6+
{
7+
8+
public function testGoodMethod()
9+
{
10+
$this->createMock(Bar::class)->method('doThing');
11+
}
12+
13+
public function testBadMethod()
14+
{
15+
$this->createMock(Bar::class)->method('doBadThing');
16+
}
17+
18+
public function testBadMethodWithExpectation()
19+
{
20+
$this->createMock(Bar::class)->expects($this->once())->method('doBadThing');
21+
}
22+
23+
public function testWithAnotherObject()
24+
{
25+
$bar = new BarWithMethod();
26+
$bar->method('doBadThing');
27+
}
28+
29+
}
30+
31+
class Bar {
32+
public function doThing()
33+
{
34+
return 1;
35+
}
36+
};
37+
38+
class BarWithMethod {
39+
public function method(string $string)
40+
{
41+
return $string;
42+
}
43+
};

0 commit comments

Comments
 (0)