Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 1d3cfe3

Browse files
VincentLangletondrejmirtes
authored andcommitted
Report error when trying to configure a non existing method on MockObject
1 parent 1648b3d commit 1d3cfe3

File tree

8 files changed

+264
-0
lines changed

8 files changed

+264
-0
lines changed

‎extension.neon‎

Lines changed: 9 additions & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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
Lines changed: 85 additions & 0 deletions
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+
}
Lines changed: 29 additions & 0 deletions
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+
}
Lines changed: 42 additions & 0 deletions
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‎

Lines changed: 13 additions & 0 deletions
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+
}
Lines changed: 42 additions & 0 deletions
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+
}
Lines changed: 43 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /