Repository navigation
Expand file tree
/
Copy pathNoScopeFetchBeforeCheapGuardRule.php
More file actions
151 lines (124 loc) · 4.15 KB
/
Copy pathNoScopeFetchBeforeCheapGuardRule.php
File metadata and controls
151 lines (124 loc) · 4.15 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?php
declare(strict_types=1);
namespace Rector\Utils\PHPStan\Rule;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeFinder;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Rector\PHPStan\ScopeFetcher;
/**
* Flags ScopeFetcher::fetch() called right before a cheap guard that does not use the scope.
* The guard should run first, so most nodes bail out before scope resolution.
*
* @implements Rule<ClassMethod>
* @see \Rector\Utils\PHPStan\Tests\Rule\NoScopeFetchBeforeCheapGuardRule\NoScopeFetchBeforeCheapGuardRuleTest
*/
final class NoScopeFetchBeforeCheapGuardRule implements Rule
{
private const string ERROR_MESSAGE = 'Move the "return null" guard above ScopeFetcher::fetch(), as it does not use the scope - most nodes then bail out before scope resolution.';
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param ClassMethod $node
* @return list<IdentifierRuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->name->toString() !== 'refactor') {
return [];
}
$stmts = $node->stmts;
if ($stmts === null) {
return [];
}
foreach ($stmts as $key => $stmt) {
$scopeVariableName = $this->matchScopeFetchVariableName($stmt);
if ($scopeVariableName === null) {
continue;
}
$nextStmt = $stmts[$key + 1] ?? null;
if (! $nextStmt instanceof If_) {
continue;
}
if (! $this->isReturnNullGuard($nextStmt)) {
continue;
}
if ($this->usesVariable($nextStmt->cond, $scopeVariableName)) {
continue;
}
return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('rector.scopeFetchBeforeCheapGuard')
->line($stmt->getStartLine())
->build(),
];
}
return [];
}
private function matchScopeFetchVariableName(Stmt $stmt): ?string
{
if (! $stmt instanceof Expression) {
return null;
}
if (! $stmt->expr instanceof Assign) {
return null;
}
$assign = $stmt->expr;
if (! $assign->var instanceof Variable || ! is_string($assign->var->name)) {
return null;
}
if (! $assign->expr instanceof StaticCall) {
return null;
}
$staticCall = $assign->expr;
if (! $staticCall->class instanceof Name) {
return null;
}
if ($staticCall->class->toString() !== ScopeFetcher::class) {
return null;
}
if (! $staticCall->name instanceof Identifier || $staticCall->name->toString() !== 'fetch') {
return null;
}
return $assign->var->name;
}
private function isReturnNullGuard(If_ $if): bool
{
if (count($if->stmts) !== 1) {
return false;
}
$onlyStmt = $if->stmts[0];
if (! $onlyStmt instanceof Return_) {
return false;
}
return ! $onlyStmt->expr instanceof Expr || $this->isNullConstant($onlyStmt->expr);
}
private function isNullConstant(Expr $expr): bool
{
return $expr instanceof ConstFetch && $expr->name->toLowerString() === 'null';
}
private function usesVariable(Expr $expr, string $variableName): bool
{
$foundVariable = new NodeFinder()->findFirst(
[$expr],
static fn (Node $subNode): bool => $subNode instanceof Variable && $subNode->name === $variableName
);
return $foundVariable instanceof Variable;
}
}