Skip to content

Commit 995fa4c

Browse files
authored
[dead-code] Fix RemoveUnusedVariableAssignRector, allow for SplFileInfo as cleanup on purpose for gc (#8054)
* [dead-code] Fix RemoveUnusedVariableAssignRector, allow for SplFileInfo as cleanup on purpose for gc * [php 8.0] Skip merged property in PromotedPropertyCandidateResolver
1 parent 0b70133 commit 995fa4c

5 files changed

Lines changed: 135 additions & 21 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Fixture;
4+
5+
final class NullResetOfPlainObject
6+
{
7+
public function run(): void
8+
{
9+
$value = new \stdClass();
10+
echo $value->name;
11+
12+
$value = null;
13+
}
14+
}
15+
?>
16+
-----
17+
<?php
18+
19+
namespace Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Fixture;
20+
21+
final class NullResetOfPlainObject
22+
{
23+
public function run(): void
24+
{
25+
$value = new \stdClass();
26+
echo $value->name;
27+
}
28+
}
29+
?>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Fixture;
4+
5+
final class SkipNullResetOfObject
6+
{
7+
public function process(string $path): bool
8+
{
9+
try {
10+
$file = new \SplFileObject($path);
11+
} catch (\Exception $e) {
12+
return false;
13+
}
14+
15+
$file->seek(0);
16+
echo $file->current();
17+
18+
// Close the file
19+
$file = null;
20+
21+
return true;
22+
}
23+
}

‎rules-tests/Php80/Rector/Class_/ClassPropertyAssignToConstructorPromotionRector/Fixture/skip_merged_property.php.inc‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,3 @@ final class SkipMergedProperty
2727
}
2828

2929
?>
30-
-----
31-
<?php declare(strict_types = 1);
32-
33-
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Fixture;
34-
35-
final class SkipMergedProperty
36-
{
37-
/**
38-
* @param array<string, mixed> $metadata
39-
*/
40-
public function __construct(private array $metadata)
41-
{
42-
if (!isset($this->metadata['permission'])) {
43-
$this->metadata['permission'] = [];
44-
}
45-
46-
$permission = $this->metadata['permission'];
47-
}
48-
}
49-
50-
?>

‎rules/DeadCode/Rector/Assign/RemoveUnusedVariableAssignRector.php‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Rector\Php\ReservedKeywordAnalyzer;
2929
use Rector\PhpParser\Enum\NodeGroup;
3030
use Rector\PhpParser\Node\BetterNodeFinder;
31+
use Rector\PhpParser\Node\Value\ValueResolver;
3132
use Rector\Rector\AbstractRector;
3233
use Rector\ValueObject\MethodName;
3334
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
@@ -45,6 +46,7 @@ public function __construct(
4546
private readonly BetterNodeFinder $betterNodeFinder,
4647
private readonly StmtsManipulator $stmtsManipulator,
4748
private readonly NoDiscardCallAnalyzer $noDiscardCallAnalyzer,
49+
private readonly ValueResolver $valueResolver,
4850
) {
4951
}
5052

@@ -116,6 +118,10 @@ public function refactor(Node $node): null|ClassMethod|Function_
116118
continue;
117119
}
118120

121+
if ($this->isNullResetOfObject($assign)) {
122+
continue;
123+
}
124+
119125
if ($this->hasCallLikeInAssignExpr($assign)) {
120126
// clean safely
121127
$cleanAssignedExpr = $this->cleanCastedExpr($assign->expr);
@@ -136,6 +142,18 @@ public function refactor(Node $node): null|ClassMethod|Function_
136142
return null;
137143
}
138144

145+
private function isNullResetOfObject(Assign $assign): bool
146+
{
147+
// resetting a native filesystem object to null releases the held file handle,
148+
// e.g. $file = null on a SplFileObject/SplFileInfo/RecursiveDirectoryIterator
149+
if (! $this->valueResolver->isNull($assign->expr)) {
150+
return false;
151+
}
152+
153+
return (new ObjectType('SplFileInfo'))->isSuperTypeOf($this->getType($assign->var))
154+
->yes();
155+
}
156+
139157
private function isObjectWithDestructMethod(Expr $expr): bool
140158
{
141159
$exprType = $this->getType($expr);

‎rules/Php80/NodeAnalyzer/PromotedPropertyCandidateResolver.php‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Rector\Php80\NodeAnalyzer;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ArrayDimFetch;
89
use PhpParser\Node\Expr\Assign;
910
use PhpParser\Node\Expr\PropertyFetch;
1011
use PhpParser\Node\Expr\Variable;
@@ -135,6 +136,15 @@ private function matchPropertyPromotionCandidate(
135136
continue;
136137
}
137138

139+
if ($this->isParamReadAfterPropertyMutation(
140+
$constructClassMethod,
141+
$assignStmtPosition,
142+
$propertyName,
143+
$matchedParam
144+
)) {
145+
continue;
146+
}
147+
138148
return new PropertyPromotionCandidate(
139149
$property,
140150
$matchedParam,
@@ -205,6 +215,61 @@ private function isParamUsedBeforeAssign(Variable $variable, array $firstParamAs
205215
return $firstVariablePosition < $variable->getStartTokenPos();
206216
}
207217

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+
208273
/**
209274
* @param int[] $firstParamAsVariable
210275
*/

0 commit comments

Comments
 (0)