Repository navigation
Expand file tree
/
Copy pathNoResolveClassReflectionInForeachRule.php
More file actions
104 lines (86 loc) · 3.24 KB
/
Copy pathNoResolveClassReflectionInForeachRule.php
File metadata and controls
104 lines (86 loc) · 3.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<?php
declare(strict_types=1);
namespace Rector\Utils\PHPStan\Rule;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\NodeFinder;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Rector\Contract\Rector\RectorInterface;
/**
* Inside a Rector rule, the refactored class reflection is identical for every
* loop iteration, so resolving it inside a foreach() is wasted work. Hoist the
* call above the loop.
*
* @implements Rule<Foreach_>
* @see \Rector\Utils\PHPStan\Tests\Rule\NoResolveClassReflectionInForeachRule\NoResolveClassReflectionInForeachRuleTest
*/
final readonly class NoResolveClassReflectionInForeachRule implements Rule
{
private const string ERROR_MESSAGE = 'resolveClassReflection() is called inside a foreach() loop. The class reflection is the same for every iteration, hoist the call above the loop.';
private const string METHOD_NAME = 'resolveClassReflection';
private NodeFinder $nodeFinder;
public function __construct()
{
$this->nodeFinder = new NodeFinder();
}
public function getNodeType(): string
{
return Foreach_::class;
}
/**
* @param Foreach_ $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return [];
}
// only Rector rules resolve the single refactored class per node
if (! $classReflection->implementsInterface(RectorInterface::class)) {
return [];
}
$ruleErrors = [];
foreach ($this->matchResolveClassReflectionCalls($node) as $methodCall) {
$ruleErrors[] = RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('rector.resolveClassReflectionInForeach')
->line($methodCall->getStartLine())
->build();
}
return $ruleErrors;
}
/**
* @return MethodCall[]
*/
private function matchResolveClassReflectionCalls(Foreach_ $foreach): array
{
// skip calls nested in an inner foreach, that inner one reports them
$nestedCallObjectIds = [];
foreach ($this->nodeFinder->findInstanceOf($foreach->stmts, Foreach_::class) as $nestedForeach) {
foreach ($this->nodeFinder->findInstanceOf($nestedForeach->stmts, MethodCall::class) as $nestedCall) {
$nestedCallObjectIds[spl_object_id($nestedCall)] = true;
}
}
$matchedCalls = [];
foreach ($this->nodeFinder->findInstanceOf($foreach->stmts, MethodCall::class) as $methodCall) {
if (! $methodCall->name instanceof Identifier) {
continue;
}
if ($methodCall->name->toString() !== self::METHOD_NAME) {
continue;
}
if (isset($nestedCallObjectIds[spl_object_id($methodCall)])) {
continue;
}
$matchedCalls[] = $methodCall;
}
return $matchedCalls;
}
}