Skip to content

Commit 9f5f19d

Browse files
authored
add variable support to CreateStubOverCreateMockArgRector (#633)
1 parent bc79241 commit 9f5f19d

File tree

9 files changed

+293
-95
lines changed

9 files changed

+293
-95
lines changed

config/sets/phpunit-code-quality.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWithMethodRector;
5151
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\WithCallbackIdenticalToStandaloneAssertsRector;
5252
use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector;
53+
use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector;
54+
use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector;
5355
use Rector\PHPUnit\PHPUnit60\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector;
5456
use Rector\PHPUnit\PHPUnit90\Rector\MethodCall\ReplaceAtMethodWithDesiredMatcherRector;
5557
use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector;
@@ -125,6 +127,10 @@
125127
SimplerWithIsInstanceOfRector::class,
126128
DirectInstanceOverMockArgRector::class,
127129

130+
// stub over mock
131+
CreateStubOverCreateMockArgRector::class,
132+
ExpressionCreateMockToCreateStubRector::class,
133+
128134
// @test first, enable later
129135
// \Rector\PHPUnit\CodeQuality\Rector\Expression\ConfiguredMockEntityToSetterObjectRector::class,
130136

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class HandleAssignedVariable extends TestCase
8+
{
9+
public function testThat()
10+
{
11+
$itemMock = $this->createMock(\stdClass::class);
12+
13+
$items = [
14+
$itemMock
15+
];
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;
24+
25+
use PHPUnit\Framework\TestCase;
26+
27+
final class HandleAssignedVariable extends TestCase
28+
{
29+
public function testThat()
30+
{
31+
$itemMock = $this->createStub(\stdClass::class);
32+
33+
$items = [
34+
$itemMock
35+
];
36+
}
37+
}
38+
39+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class HandleAssignedVariableMultipleTimes extends TestCase
8+
{
9+
public function testThat()
10+
{
11+
$itemMock = $this->createMock(\stdClass::class);
12+
13+
$items = [
14+
$itemMock
15+
];
16+
17+
$anotherValue = new \stdClass($itemMock);
18+
}
19+
}
20+
21+
?>
22+
-----
23+
<?php
24+
25+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Fixture;
26+
27+
use PHPUnit\Framework\TestCase;
28+
29+
final class HandleAssignedVariableMultipleTimes extends TestCase
30+
{
31+
public function testThat()
32+
{
33+
$itemMock = $this->createStub(\stdClass::class);
34+
35+
$items = [
36+
$itemMock
37+
];
38+
39+
$anotherValue = new \stdClass($itemMock);
40+
}
41+
}
42+
43+
?>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
9+
10+
final class HandleUsedOutsideArg extends TestCase
11+
{
12+
public function test()
13+
{
14+
$mock = $this->createMock(\stdClass::class);
15+
16+
if ($mock instanceof \stdClass) {
17+
// do something
18+
}
19+
20+
$someObject = new ClassWithDependency($mock);
21+
$this->assertSame($mock, $someObject->getDependency());
22+
}
23+
}
24+
25+
?>
26+
-----
27+
<?php
28+
29+
declare(strict_types=1);
30+
31+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
32+
33+
use PHPUnit\Framework\TestCase;
34+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
35+
36+
final class HandleUsedOutsideArg extends TestCase
37+
{
38+
public function test()
39+
{
40+
$mock = $this->createStub(\stdClass::class);
41+
42+
if ($mock instanceof \stdClass) {
43+
// do something
44+
}
45+
46+
$someObject = new ClassWithDependency($mock);
47+
$this->assertSame($mock, $someObject->getDependency());
48+
}
49+
}
50+
51+
?>

rules-tests/PHPUnit120/Rector/ClassMethod/ExpressionCreateMockToCreateStubRector/Fixture/skip_used_outside_arg.php.inc

Lines changed: 0 additions & 21 deletions
This file was deleted.

rules/CodeQuality/NodeAnalyser/AssertMethodAnalyzer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function detectTestCaseCall(MethodCall|StaticCall $call): bool
3030
? $call->var
3131
: $call->class;
3232

33-
if (! $this->nodeTypeResolver->isObjectType($objectCaller, new ObjectType('PHPUnit\Framework\TestCase'))) {
33+
if (! $this->nodeTypeResolver->isObjectType($objectCaller, new ObjectType(PHPUnitClassName::TEST_CASE))) {
3434
return false;
3535
}
3636

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\NodeAnalyser;
6+
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Expr\Variable;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use Rector\NodeNameResolver\NodeNameResolver;
12+
use Rector\PhpParser\Node\BetterNodeFinder;
13+
use Rector\PHPUnit\CodeQuality\NodeFinder\VariableFinder;
14+
15+
final readonly class MockObjectExprDetector
16+
{
17+
public function __construct(
18+
private BetterNodeFinder $betterNodeFinder,
19+
private NodeNameResolver $nodeNameResolver,
20+
private VariableFinder $variableFinder,
21+
) {
22+
}
23+
24+
public function isUsedForMocking(Expr $expr, ClassMethod $classMethod): bool
25+
{
26+
if (! $expr instanceof Variable) {
27+
return false;
28+
}
29+
30+
$variableName = $this->nodeNameResolver->getName($expr);
31+
32+
// to be safe
33+
if ($variableName === null) {
34+
return true;
35+
}
36+
37+
$relatedVariables = $this->variableFinder->find($classMethod, $variableName);
38+
39+
// only self variable found, nothing to mock
40+
if (count($relatedVariables) === 1) {
41+
return false;
42+
}
43+
44+
// find out, how many are used in call likes as args
45+
/** @var array<Expr\MethodCall> $methodCalls */
46+
$methodCalls = $this->betterNodeFinder->findInstancesOfScoped((array) $classMethod->stmts, [MethodCall::class]);
47+
48+
foreach ($methodCalls as $methodCall) {
49+
if (! $methodCall->var instanceof Variable) {
50+
continue;
51+
}
52+
53+
if ($this->nodeNameResolver->isName($methodCall->var, $variableName)) {
54+
// variable is being called on, most like mocking, lets skip
55+
return true;
56+
}
57+
}
58+
59+
return false;
60+
}
61+
}

rules/PHPUnit120/Rector/CallLike/CreateStubOverCreateMockArgRector.php

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66

77
use PhpParser\Node;
88
use PhpParser\Node\ArrayItem;
9+
use PhpParser\Node\Expr;
10+
use PhpParser\Node\Expr\Assign;
911
use PhpParser\Node\Expr\MethodCall;
1012
use PhpParser\Node\Expr\New_;
1113
use PhpParser\Node\Expr\StaticCall;
1214
use PhpParser\Node\Identifier;
15+
use PhpParser\Node\Stmt\ClassMethod;
16+
use PhpParser\Node\Stmt\Expression;
1317
use Rector\PHPStan\ScopeFetcher;
18+
use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockObjectExprDetector;
1419
use Rector\PHPUnit\Enum\PHPUnitClassName;
20+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
1521
use Rector\Rector\AbstractRector;
1622
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
1723
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@@ -23,6 +29,12 @@
2329
*/
2430
final class CreateStubOverCreateMockArgRector extends AbstractRector
2531
{
32+
public function __construct(
33+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
34+
private readonly MockObjectExprDetector $mockObjectExprDetector,
35+
) {
36+
}
37+
2638
public function getRuleDefinition(): RuleDefinition
2739
{
2840
return new RuleDefinition(
@@ -70,13 +82,13 @@ private function someMethod($someClass)
7082
*/
7183
public function getNodeTypes(): array
7284
{
73-
return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class];
85+
return [StaticCall::class, MethodCall::class, New_::class, ArrayItem::class, ClassMethod::class];
7486
}
7587

7688
/**
77-
* @param MethodCall|StaticCall|New_|ArrayItem $node
89+
* @param MethodCall|StaticCall|New_|ArrayItem|ClassMethod $node
7890
*/
79-
public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null
91+
public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|ClassMethod|null
8092
{
8193
$scope = ScopeFetcher::fetch($node);
8294
if (! $scope->isInClass()) {
@@ -88,19 +100,12 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null
88100
return null;
89101
}
90102

91-
if ($node instanceof ArrayItem) {
92-
if (! $node->value instanceof MethodCall) {
93-
return null;
94-
}
95-
96-
$methodCall = $node->value;
97-
if (! $this->isName($methodCall->name, 'createMock')) {
98-
return null;
99-
}
100-
101-
$methodCall->name = new Identifier('createStub');
103+
if ($node instanceof ClassMethod) {
104+
return $this->refactorClassMethod($node);
105+
}
102106

103-
return $node;
107+
if ($node instanceof ArrayItem) {
108+
return $this->refactorArrayItem($node);
104109
}
105110

106111
$hasChanges = false;
@@ -129,4 +134,72 @@ public function refactor(Node $node): MethodCall|StaticCall|New_|ArrayItem|null
129134

130135
return null;
131136
}
137+
138+
private function matchCreateMockMethodCall(Expr $expr): null|MethodCall
139+
{
140+
if (! $expr instanceof MethodCall) {
141+
return null;
142+
}
143+
144+
if (! $this->isName($expr->name, 'createMock')) {
145+
return null;
146+
}
147+
148+
return $expr;
149+
}
150+
151+
private function refactorClassMethod(ClassMethod $classMethod): ?ClassMethod
152+
{
153+
if (! $this->testsNodeAnalyzer->isTestClassMethod($classMethod)) {
154+
return null;
155+
}
156+
157+
$hasChanged = false;
158+
foreach ((array) $classMethod->stmts as $stmt) {
159+
if (! $stmt instanceof Expression) {
160+
continue;
161+
}
162+
163+
if (! $stmt->expr instanceof Assign) {
164+
continue;
165+
}
166+
167+
$assign = $stmt->expr;
168+
$createMockMethodCall = $this->matchCreateMockMethodCall($assign->expr);
169+
170+
if (! $createMockMethodCall instanceof MethodCall) {
171+
continue;
172+
}
173+
174+
// no change, as we use the variable for mocking later
175+
if ($this->mockObjectExprDetector->isUsedForMocking($assign->var, $classMethod)) {
176+
continue;
177+
}
178+
179+
$createMockMethodCall->name = new Identifier('createStub');
180+
$hasChanged = true;
181+
}
182+
183+
if ($hasChanged) {
184+
return $classMethod;
185+
}
186+
187+
return null;
188+
}
189+
190+
private function refactorArrayItem(ArrayItem $arrayItem): ?ArrayItem
191+
{
192+
if (! $arrayItem->value instanceof MethodCall) {
193+
return null;
194+
}
195+
196+
$methodCall = $arrayItem->value;
197+
if (! $this->isName($methodCall->name, 'createMock')) {
198+
return null;
199+
}
200+
201+
$methodCall->name = new Identifier('createStub');
202+
203+
return $arrayItem;
204+
}
132205
}

0 commit comments

Comments
 (0)