Skip to content

Commit 8c74ebb

Browse files
committed
[code-quality] Add AssignedMocksCollector
1 parent b49146b commit 8c74ebb

File tree

8 files changed

+352
-94
lines changed

8 files changed

+352
-94
lines changed

rules-tests/CodeQuality/Rector/ClassMethod/BareCreateMockAssignToDirectUseRector/Fixture/some_class.php.inc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ final class SomeClass extends TestCase
88
{
99
public function test()
1010
{
11-
$someMock = $this->createMock('SomeClass');
11+
$someMock = $this->createMock(\Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector\Source\AnotherClass::class);
1212

1313
$this->useMock($someMock);
1414
}
@@ -30,7 +30,7 @@ final class SomeClass extends TestCase
3030
{
3131
public function test()
3232
{
33-
$this->useMock($this->createMock('SomeClass'));
33+
$this->useMock($this->createMock(\Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector\Source\AnotherClass::class));
3434
}
3535

3636
private function useMock($someMock)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector\Source;
6+
7+
final class AnotherClass
8+
{
9+
}

rules-tests/CodeQuality/Rector/ClassMethod/BareCreateMockAssignToDirectUseRector/config/configured_rule.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
declare(strict_types=1);
44

55
use Rector\Config\RectorConfig;
6+
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector;
67

7-
return static function (RectorConfig $rectorConfig): void {
8-
$rectorConfig->rules(
9-
rectorClasses: [\Rector\PHPUnit\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector::class]
10-
);
11-
};
8+
return RectorConfig::configure()
9+
->withRules([BareCreateMockAssignToDirectUseRector::class]);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\NodeAnalyser;
6+
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr\Assign;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Expr\Variable;
11+
use PhpParser\Node\Stmt\ClassMethod;
12+
use PhpParser\Node\Stmt\Expression;
13+
use PhpParser\Node\Stmt\Foreach_;
14+
use PHPStan\Reflection\ReflectionProvider;
15+
use Rector\NodeNameResolver\NodeNameResolver;
16+
use Rector\PhpParser\Node\Value\ValueResolver;
17+
18+
final readonly class AssignedMocksCollector
19+
{
20+
public function __construct(
21+
private ReflectionProvider $reflectionProvider,
22+
private ValueResolver $valueResolver,
23+
private NodeNameResolver $nodeNameResolver,
24+
private DoctrineEntityDocumentAnalyser $doctrineEntityDocumentAnalyser,
25+
) {
26+
}
27+
28+
/**
29+
* @return array<string, string>
30+
*/
31+
public function collect(ClassMethod|Foreach_ $stmtsAware): array
32+
{
33+
if ($stmtsAware->stmts === null) {
34+
return [];
35+
}
36+
37+
$mockedVariablesToTypes = [];
38+
39+
foreach ($stmtsAware->stmts as $stmt) {
40+
// find assign mock
41+
if (! $stmt instanceof Expression || ! $stmt->expr instanceof Assign) {
42+
continue;
43+
}
44+
45+
$assign = $stmt->expr;
46+
47+
$firstArg = $this->matchCreateMockArgAssignedToVariable($assign);
48+
if (! $firstArg instanceof Arg) {
49+
continue;
50+
}
51+
52+
$mockedClass = $this->valueResolver->getValue($firstArg->value);
53+
if (! is_string($mockedClass)) {
54+
continue;
55+
}
56+
57+
if (! $this->reflectionProvider->hasClass($mockedClass)) {
58+
continue;
59+
}
60+
61+
$mockClassReflection = $this->reflectionProvider->getClass($mockedClass);
62+
63+
// these cannot be instantiated
64+
if ($mockClassReflection->isAbstract() || $mockClassReflection->isInterface()) {
65+
continue;
66+
}
67+
68+
$mockedVariableName = $this->nodeNameResolver->getName($assign->var);
69+
$mockedVariablesToTypes[$mockedVariableName] = $mockedClass;
70+
}
71+
72+
return $mockedVariablesToTypes;
73+
}
74+
75+
/**
76+
* @return array<string, string>
77+
*/
78+
public function collectEntityClasses(ClassMethod|Foreach_ $stmtsAware): array
79+
{
80+
$variableNamesToClassNames = $this->collect($stmtsAware);
81+
82+
$variableNamesToEntityClasses = [];
83+
84+
foreach ($variableNamesToClassNames as $variableName => $className) {
85+
if (! $this->doctrineEntityDocumentAnalyser->isEntityClass($className)) {
86+
continue;
87+
}
88+
89+
$variableNamesToEntityClasses[$variableName] = $className;
90+
}
91+
92+
return $variableNamesToEntityClasses;
93+
}
94+
95+
public function matchCreateMockArgAssignedToVariable(Assign $assign): ?Arg
96+
{
97+
if (! $assign->var instanceof Variable) {
98+
return null;
99+
}
100+
101+
if (! $assign->expr instanceof MethodCall) {
102+
return null;
103+
}
104+
105+
$methodCall = $assign->expr;
106+
if (! $this->nodeNameResolver->isName($methodCall->name, 'createMock')) {
107+
return null;
108+
}
109+
110+
if ($methodCall->isFirstClassCallable()) {
111+
return null;
112+
}
113+
114+
return $methodCall->getArgs()[0];
115+
}
116+
}

rules/CodeQuality/NodeAnalyser/DoctrineEntityDocumentAnalyser.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace Rector\PHPUnit\CodeQuality\NodeAnalyser;
66

77
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
8-
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Reflection\ReflectionProvider;
99

1010
final readonly class DoctrineEntityDocumentAnalyser
1111
{
@@ -14,8 +14,19 @@
1414
*/
1515
private const array ENTITY_DOCBLOCK_MARKERS = ['@Document', '@ORM\\Document', '@Entity', '@ORM\\Entity'];
1616

17-
public function isEntityClass(ClassReflection $classReflection): bool
17+
public function __construct(
18+
private ReflectionProvider $reflectionProvider,
19+
) {
20+
}
21+
22+
public function isEntityClass(string $className): bool
1823
{
24+
if (! $this->reflectionProvider->hasClass($className)) {
25+
return false;
26+
}
27+
28+
$classReflection = $this->reflectionProvider->getClass($className);
29+
1930
$resolvedPhpDocBlock = $classReflection->getResolvedPhpDoc();
2031
if (! $resolvedPhpDocBlock instanceof ResolvedPhpDocBlock) {
2132
return false;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\NodeFinder;
6+
7+
use PhpParser\Node\Expr\Variable;
8+
use Rector\NodeNameResolver\NodeNameResolver;
9+
use Rector\PhpParser\Node\BetterNodeFinder;
10+
11+
final readonly class VariableFinder
12+
{
13+
public function __construct(
14+
private BetterNodeFinder $betterNodeFinder,
15+
private NodeNameResolver $nodeNameResolver,
16+
) {
17+
}
18+
19+
/**
20+
* @return Variable[]
21+
*/
22+
public function find(\PhpParser\Node $node, string $variableName): array
23+
{
24+
$variables = $this->betterNodeFinder->findInstancesOfScoped([$node], Variable::class);
25+
26+
return array_filter($variables, function (Variable $variable) use ($variableName): bool {
27+
return $this->nodeNameResolver->isName($variable, $variableName);
28+
});
29+
}
30+
}

rules/CodeQuality/Rector/ClassMethod/BareCreateMockAssignToDirectUseRector.php

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55
namespace Rector\PHPUnit\CodeQuality\Rector\ClassMethod;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr\Assign;
10+
use PhpParser\Node\Expr\CallLike;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\New_;
13+
use PhpParser\Node\Expr\StaticCall;
14+
use PhpParser\Node\Expr\Variable;
15+
use PhpParser\Node\Stmt;
816
use PhpParser\Node\Stmt\ClassMethod;
17+
use PhpParser\Node\Stmt\Expression;
918
use PhpParser\Node\Stmt\Foreach_;
10-
use Rector\PHPUnit\CodeQuality\NodeAnalyser\NullableObjectAssignCollector;
11-
use Rector\PHPUnit\CodeQuality\NodeFactory\AssertMethodCallFactory;
12-
use Rector\PHPUnit\CodeQuality\TypeAnalyzer\MethodCallParameterTypeResolver;
13-
use Rector\PHPUnit\NodeAnalyzer\AssertCallAnalyzer;
19+
use Rector\PhpParser\Node\BetterNodeFinder;
20+
use Rector\PHPUnit\CodeQuality\NodeAnalyser\AssignedMocksCollector;
21+
use Rector\PHPUnit\CodeQuality\NodeFinder\VariableFinder;
1422
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
1523
use Rector\Rector\AbstractRector;
1624
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
@@ -23,10 +31,9 @@ final class BareCreateMockAssignToDirectUseRector extends AbstractRector
2331
{
2432
public function __construct(
2533
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
26-
private readonly NullableObjectAssignCollector $nullableObjectAssignCollector,
27-
private readonly AssertMethodCallFactory $assertMethodCallFactory,
28-
private readonly MethodCallParameterTypeResolver $methodCallParameterTypeResolver,
29-
private readonly AssertCallAnalyzer $assertCallAnalyzer,
34+
private readonly AssignedMocksCollector $assignedMocksCollector,
35+
private readonly BetterNodeFinder $betterNodeFinder,
36+
private readonly VariableFinder $variableFinder,
3037
) {
3138
}
3239

@@ -96,12 +103,65 @@ public function refactor(Node $node): ?Node
96103
return null;
97104
}
98105

99-
$hasChanged = false;
106+
$mockedClassesToVariableNames = $this->assignedMocksCollector->collect($node);
107+
if ($mockedClassesToVariableNames === []) {
108+
return null;
109+
}
100110

101-
$next = 0;
102-
foreach ($node->stmts as $key => $stmt) {
111+
$hasChanged = false;
103112

104-
++$next;
113+
foreach ($mockedClassesToVariableNames as $variableName => $mockedClass) {
114+
// variable cannot be part of any method call
115+
if ($this->isVariableUsedAsPartOfMethodCall($node, $variableName)) {
116+
continue;
117+
}
118+
119+
if ($this->isUsedMoreOftenThanInCallLikeArgs($node, $variableName)) {
120+
continue;
121+
}
122+
123+
// 1. remove initial assign
124+
foreach ($node->stmts as $key => $stmt) {
125+
if ($stmt instanceof Expression && $stmt->expr instanceof Assign) {
126+
$assign = $stmt->expr;
127+
128+
$instanceArg = $this->assignedMocksCollector->matchCreateMockArgAssignedToVariable($assign);
129+
if (! $instanceArg instanceof Arg) {
130+
continue;
131+
}
132+
133+
if (! $assign->var instanceof Variable) {
134+
continue;
135+
}
136+
137+
if (! $this->isName($assign->var, $variableName)) {
138+
continue;
139+
}
140+
141+
// 1. remove assign
142+
unset($node->stmts[$key]);
143+
$hasChanged = true;
144+
}
145+
146+
// 2. replace variable with call-like args of new instance
147+
/** @var CallLike[] $callLikes */
148+
$callLikes = $this->findCallLikes($stmt);
149+
150+
foreach ($callLikes as $callLike) {
151+
foreach ($callLike->getArgs() as $arg) {
152+
if (! $arg->value instanceof Variable) {
153+
continue;
154+
}
155+
156+
if (! $this->isName($arg->value, $variableName)) {
157+
continue;
158+
}
159+
160+
// 2. replace variable with call-like args
161+
$arg->value = $assign->expr;
162+
}
163+
}
164+
}
105165
}
106166

107167
if (! $hasChanged) {
@@ -110,4 +170,62 @@ public function refactor(Node $node): ?Node
110170

111171
return $node;
112172
}
173+
174+
private function isVariableUsedAsPartOfMethodCall(ClassMethod|Foreach_ $stmtsAware, string $variableName): bool
175+
{
176+
/** @var MethodCall[] $methodCalls */
177+
$methodCalls = $this->betterNodeFinder->findInstancesOfScoped([$stmtsAware], [MethodCall::class]);
178+
179+
foreach ($methodCalls as $methodCall) {
180+
if ($this->isName($methodCall->var, $variableName)) {
181+
return true;
182+
}
183+
}
184+
185+
return false;
186+
}
187+
188+
private function isUsedMoreOftenThanInCallLikeArgs(ClassMethod|Foreach_ $stmtsAware, string $variableName): bool
189+
{
190+
// get use count
191+
$foundVariables = $this->variableFinder->find($stmtsAware, $variableName);
192+
193+
// found method call, static call or new arg-only usage
194+
$callLikeVariableUseCount = 0;
195+
196+
/** @var CallLike[] $callLikes */
197+
$callLikes = $this->findCallLikes($stmtsAware);
198+
199+
foreach ($callLikes as $callLike) {
200+
foreach ($callLike->getArgs() as $arg) {
201+
if (! $arg->value instanceof Variable) {
202+
continue;
203+
}
204+
205+
if (! $this->isName($arg->value, $variableName)) {
206+
continue;
207+
}
208+
209+
++$callLikeVariableUseCount;
210+
}
211+
}
212+
213+
// not suitable for direct replacing
214+
return (count($foundVariables) - 1) > ($callLikeVariableUseCount);
215+
}
216+
217+
/**
218+
* @return CallLike[]
219+
*/
220+
private function findCallLikes(ClassMethod|Foreach_|Stmt $node): array
221+
{
222+
$callLikes = $this->betterNodeFinder->findInstancesOfScoped(
223+
[$node],
224+
[MethodCall::class, StaticCall::class, New_::class]
225+
);
226+
227+
return array_filter($callLikes, function (CallLike $callLike): bool {
228+
return ! $callLike->isFirstClassCallable();
229+
});
230+
}
113231
}

0 commit comments

Comments
 (0)