Skip to content

Commit 2852aed

Browse files
authored
[CodeQuality] Skip NarrowUnusedSetUpDefinedPropertyRector for property used in nested closure (#693)
* [CodeQuality] Skip NarrowUnusedSetUpDefinedPropertyRector for property used in nested closure The rule turned a setUp() property into a local variable while rewriting every $this->property reference inside setUp(), including references inside nested closures (e.g. resolver callbacks stored for later execution). A closure does not capture the local variable (no 'use' binding is added), so at call time the variable was undefined and the closure silently returned null. Skip narrowing when the property is referenced inside a Closure or ArrowFunction within setUp(). * fixup! [CodeQuality] Skip NarrowUnusedSetUpDefinedPropertyRector for property used in nested closure
1 parent ef9ace2 commit 2852aed

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\NarrowUnusedSetUpDefinedPropertyRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SkipPropertyUsedInClosure extends TestCase
8+
{
9+
private array $dataSource;
10+
11+
protected function setUp(): void
12+
{
13+
$this->dataSource = [1, 2, 3];
14+
15+
$this->resolver = function () {
16+
return $this->dataSource;
17+
};
18+
}
19+
20+
public function test()
21+
{
22+
$resolver = $this->resolver;
23+
$this->assertSame([1, 2, 3], $resolver());
24+
}
25+
}

‎rules/CodeQuality/Rector/Class_/NarrowUnusedSetUpDefinedPropertyRector.php‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PhpParser\Node;
88
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\Closure;
910
use PhpParser\Node\Expr\PropertyFetch;
1011
use PhpParser\Node\Expr\Variable;
1112
use PhpParser\Node\Stmt\Class_;
@@ -121,6 +122,12 @@ public function refactor(Node $node): ?Node
121122
continue;
122123
}
123124

125+
// referenced inside a nested closure that may run after setUp() - turning it into a local
126+
// variable would leave it out of the closure scope, as there is no "use" binding to add it
127+
if ($this->isPropertyUsedInNestedClosure($setUpClassMethod, $propertyName)) {
128+
continue;
129+
}
130+
124131
$hasChanged = true;
125132

126133
unset($node->stmts[$key]);
@@ -189,6 +196,35 @@ private function isPropertyUsedOutsideSetUpClassMethod(
189196
return $isPropertyUsed;
190197
}
191198

199+
private function isPropertyUsedInNestedClosure(ClassMethod $setUpClassMethod, string $propertyName): bool
200+
{
201+
// only regular closures are unsafe: they do not capture outer variables without an
202+
// explicit "use" binding. Arrow functions auto-capture by value, so narrowing is safe there.
203+
$closures = $this->nodeFinder->findInstanceOf($setUpClassMethod, Closure::class);
204+
205+
foreach ($closures as $closure) {
206+
$usedPropertyFetch = $this->nodeFinder->findFirst($closure, function (Node $node) use (
207+
$propertyName
208+
): bool {
209+
if (! $node instanceof PropertyFetch) {
210+
return false;
211+
}
212+
213+
if (! $this->isName($node->var, 'this')) {
214+
return false;
215+
}
216+
217+
return $this->isName($node->name, $propertyName);
218+
});
219+
220+
if ($usedPropertyFetch instanceof PropertyFetch) {
221+
return true;
222+
}
223+
}
224+
225+
return false;
226+
}
227+
192228
private function shouldSkipProperty(
193229
bool $isFinalClass,
194230
Property $property,

0 commit comments

Comments
 (0)