|
5 | 5 | namespace Rector\Php80\NodeAnalyzer; |
6 | 6 |
|
7 | 7 | use PhpParser\Node; |
| 8 | +use PhpParser\Node\Expr\ArrayDimFetch; |
8 | 9 | use PhpParser\Node\Expr\Assign; |
9 | 10 | use PhpParser\Node\Expr\PropertyFetch; |
10 | 11 | use PhpParser\Node\Expr\Variable; |
@@ -135,6 +136,15 @@ private function matchPropertyPromotionCandidate( |
135 | 136 | continue; |
136 | 137 | } |
137 | 138 |
|
| 139 | + if ($this->isParamReadAfterPropertyMutation( |
| 140 | + $constructClassMethod, |
| 141 | + $assignStmtPosition, |
| 142 | + $propertyName, |
| 143 | + $matchedParam |
| 144 | + )) { |
| 145 | + continue; |
| 146 | + } |
| 147 | + |
138 | 148 | return new PropertyPromotionCandidate( |
139 | 149 | $property, |
140 | 150 | $matchedParam, |
@@ -205,6 +215,61 @@ private function isParamUsedBeforeAssign(Variable $variable, array $firstParamAs |
205 | 215 | return $firstVariablePosition < $variable->getStartTokenPos(); |
206 | 216 | } |
207 | 217 |
|
| 218 | + /** |
| 219 | + * The rule rewrites later $param reads to $this->property. That is only safe while $this->property still |
| 220 | + * equals the param. Once the property is mutated in the constructor, a following $param read would diverge, |
| 221 | + * so the promotion must be skipped. |
| 222 | + */ |
| 223 | + private function isParamReadAfterPropertyMutation( |
| 224 | + ClassMethod $constructClassMethod, |
| 225 | + int $assignStmtPosition, |
| 226 | + string $propertyName, |
| 227 | + Param $matchedParam |
| 228 | + ): bool { |
| 229 | + $followingStmts = array_slice((array) $constructClassMethod->stmts, $assignStmtPosition + 1); |
| 230 | + if ($followingStmts === []) { |
| 231 | + return false; |
| 232 | + } |
| 233 | + |
| 234 | + $firstMutationTokenPos = null; |
| 235 | + foreach ($this->betterNodeFinder->findInstanceOf($followingStmts, Assign::class) as $assign) { |
| 236 | + $assignVar = $assign->var; |
| 237 | + while ($assignVar instanceof ArrayDimFetch) { |
| 238 | + $assignVar = $assignVar->var; |
| 239 | + } |
| 240 | + |
| 241 | + if (! $this->propertyFetchAnalyzer->isLocalPropertyFetchName($assignVar, $propertyName)) { |
| 242 | + continue; |
| 243 | + } |
| 244 | + |
| 245 | + $tokenPos = $assign->var->getStartTokenPos(); |
| 246 | + if ($firstMutationTokenPos === null || $tokenPos < $firstMutationTokenPos) { |
| 247 | + $firstMutationTokenPos = $tokenPos; |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + if ($firstMutationTokenPos === null) { |
| 252 | + return false; |
| 253 | + } |
| 254 | + |
| 255 | + $paramName = $this->nodeNameResolver->getName($matchedParam); |
| 256 | + |
| 257 | + return $this->betterNodeFinder->findFirst($followingStmts, function (Node $node) use ( |
| 258 | + $paramName, |
| 259 | + $firstMutationTokenPos |
| 260 | + ): bool { |
| 261 | + if (! $node instanceof Variable) { |
| 262 | + return false; |
| 263 | + } |
| 264 | + |
| 265 | + if (! $this->nodeNameResolver->isName($node, $paramName)) { |
| 266 | + return false; |
| 267 | + } |
| 268 | + |
| 269 | + return $node->getStartTokenPos() > $firstMutationTokenPos; |
| 270 | + }) instanceof Node; |
| 271 | + } |
| 272 | + |
208 | 273 | /** |
209 | 274 | * @param int[] $firstParamAsVariable |
210 | 275 | */ |
|
0 commit comments