Skip to content

Commit 40dfda3

Browse files
authored
[fixes] Couple ClassPropertyAssignToConstructorPromotionRector, RemoveAlwaysTrueIfConditionRector and RemoveUnusedVariableAssignRector fixes (#8055)
* [php 8.0] Skip merged property in PromotedPropertyCandidateResolver * [dead-code] Fix /RemoveAlwaysTrueIfConditionRector with dynamic variable
1 parent 995fa4c commit 40dfda3

10 files changed

Lines changed: 204 additions & 36 deletions

File tree

rules-tests/DeadCode/Rector/Assign/RemoveUnusedVariableAssignRector/Fixture/null_reset_of_plain_object.php.inc

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Fixture;
4+
5+
use Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Source\SomeLock;
6+
7+
final class NullResetOfUserObject
8+
{
9+
public function run(): void
10+
{
11+
$value = new SomeLock();
12+
echo get_class($value);
13+
14+
$value = null;
15+
}
16+
}
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Fixture;
22+
23+
use Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Source\SomeLock;
24+
25+
final class NullResetOfUserObject
26+
{
27+
public function run(): void
28+
{
29+
$value = new SomeLock();
30+
echo get_class($value);
31+
}
32+
}
33+
?>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector\Fixture;
4+
5+
final class SkipNullResetOfInternalObject
6+
{
7+
public function run(string $path): void
8+
{
9+
$finfo = new \finfo(FILEINFO_MIME_TYPE);
10+
echo $finfo->file($path);
11+
12+
$finfo = null;
13+
}
14+
}
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\If_\RemoveAlwaysTrueIfConditionRector\Fixture;
4+
5+
class SkipDynamicVariableAssign
6+
{
7+
public function build(array $params): string
8+
{
9+
$addHelpMessage = true;
10+
11+
foreach (['addHelpMessage'] as $f) {
12+
if (isset($params[$f])) {
13+
${$f} = (bool) $params[$f];
14+
}
15+
}
16+
17+
if ($addHelpMessage) {
18+
return 'help';
19+
}
20+
21+
return 'no help';
22+
}
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Fixture;
4+
5+
use Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Source\SomeCacheAdapterInterface;
6+
use Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Source\SomeCacheProvider;
7+
8+
final class SkipNarrowingVarDoc
9+
{
10+
/**
11+
* @var SomeCacheProvider
12+
*/
13+
private SomeCacheAdapterInterface $cacheProvider;
14+
15+
public function __construct(SomeCacheAdapterInterface $cacheProvider)
16+
{
17+
$this->cacheProvider = $cacheProvider;
18+
}
19+
20+
public function run(): object
21+
{
22+
return $this->cacheProvider->getCacheAdapter();
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Source;
6+
7+
interface SomeCacheAdapterInterface
8+
{
9+
public function clear(): bool;
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Source;
6+
7+
final class SomeCacheProvider implements SomeCacheAdapterInterface
8+
{
9+
public function clear(): bool
10+
{
11+
return true;
12+
}
13+
14+
public function getCacheAdapter(): object
15+
{
16+
return $this;
17+
}
18+
}

rules/DeadCode/Rector/Assign/RemoveUnusedVariableAssignRector.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public function refactor(Node $node): null|ClassMethod|Function_
118118
continue;
119119
}
120120

121-
if ($this->isNullResetOfObject($assign)) {
121+
if ($this->isNullResetOfInternalObject($assign)) {
122122
continue;
123123
}
124124

@@ -142,16 +142,25 @@ public function refactor(Node $node): null|ClassMethod|Function_
142142
return null;
143143
}
144144

145-
private function isNullResetOfObject(Assign $assign): bool
145+
private function isNullResetOfInternalObject(Assign $assign): bool
146146
{
147-
// resetting a native filesystem object to null releases the held file handle,
148-
// e.g. $file = null on a SplFileObject/SplFileInfo/RecursiveDirectoryIterator
147+
// resetting an internal PHP object to null releases the held resource/file handle,
148+
// e.g. $file = null on a SplFileObject/RecursiveDirectoryIterator/finfo
149149
if (! $this->valueResolver->isNull($assign->expr)) {
150150
return false;
151151
}
152152

153-
return (new ObjectType('SplFileInfo'))->isSuperTypeOf($this->getType($assign->var))
154-
->yes();
153+
$varType = $this->getType($assign->var);
154+
if (! $varType instanceof ObjectType) {
155+
return false;
156+
}
157+
158+
$classReflection = $varType->getClassReflection();
159+
if (! $classReflection instanceof ClassReflection) {
160+
return false;
161+
}
162+
163+
return $classReflection->isBuiltin();
155164
}
156165

157166
private function isObjectWithDestructMethod(Expr $expr): bool

rules/DeadCode/Rector/If_/RemoveAlwaysTrueIfConditionRector.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,16 @@ private function shouldSkipFromVariable(Expr $expr): bool
150150
/** @var Variable[] $variables */
151151
$variables = $this->betterNodeFinder->findInstancesOf($expr, [Variable::class]);
152152

153+
$hasPlainVariable = false;
153154
foreach ($variables as $variable) {
154155
if ($this->exprAnalyzer->isNonTypedFromParam($variable)) {
155156
return true;
156157
}
157158

159+
if (! $variable->name instanceof Expr) {
160+
$hasPlainVariable = true;
161+
}
162+
158163
$type = $this->nodeTypeResolver->getNativeType($variable);
159164
if ($type instanceof IntersectionType) {
160165
foreach ($type->getTypes() as $subType) {
@@ -165,7 +170,18 @@ private function shouldSkipFromVariable(Expr $expr): bool
165170
}
166171
}
167172

168-
return false;
173+
// a dynamic variable assignment, e.g. ${$name} = ..., is invisible to native type inference, so the
174+
// condition variable may be overwritten at runtime and is not safe to evaluate as always true
175+
return $hasPlainVariable && $this->hasDynamicVariable();
176+
}
177+
178+
private function hasDynamicVariable(): bool
179+
{
180+
return (bool) $this->betterNodeFinder->findFirst(
181+
$this->getFile()
182+
->getNewStmts(),
183+
static fn (Node $node): bool => $node instanceof Variable && $node->name instanceof Expr
184+
);
169185
}
170186

171187
private function shouldSkipExpr(Expr $expr): bool

rules/Php80/Rector/Class_/ClassPropertyAssignToConstructorPromotionRector.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
use PhpParser\Node\UnionType;
2121
use PhpParser\NodeVisitor;
2222
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
23+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
2324
use PHPStan\Reflection\ClassReflection;
2425
use PHPStan\Type\MixedType;
2526
use PHPStan\Type\TypeCombinator;
27+
use PHPStan\Type\TypeWithClassName;
28+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
2629
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
2730
use Rector\Contract\Rector\ConfigurableRectorInterface;
2831
use Rector\Naming\PropertyRenamer\PropertyPromotionRenamer;
@@ -213,6 +216,10 @@ public function refactor(Node $node): ?Node
213216
continue;
214217
}
215218

219+
if ($this->shouldSkipNarrowingVarDoc($property, $param, $constructorPhpDocInfo, $paramName)) {
220+
continue;
221+
}
222+
216223
$hasChanged = true;
217224

218225
// remove property from class
@@ -388,6 +395,49 @@ private function shouldSkipPropertyOrParam(Property $property, Param $param): bo
388395
&& ! $this->nodeComparator->areNodesEqual($property->type, $param->type);
389396
}
390397

398+
/**
399+
* A class-typed property may carry a narrowing @var (e.g. native AdapterInterface, @var CacheProvider) used to
400+
* type calls on the property. Promotion would drop that @var (it is not preserved as a @param here), $property so skip to
401+
* avoid losing type information.
402+
*/
403+
private function shouldSkipNarrowingVarDoc(
404+
Property $property,
405+
Param $param,
406+
PhpDocInfo $constructorPhpDocInfo,
407+
string $paramName
408+
): bool {
409+
if (! $property->type instanceof Node || ! $param->type instanceof Node) {
410+
return false;
411+
}
412+
413+
// an explicit @param already carries the type onto the promoted property
414+
if ($constructorPhpDocInfo->getParamTagValueByName($paramName) instanceof ParamTagValueNode) {
415+
return false;
416+
}
417+
418+
$propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property);
419+
$varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode();
420+
if (! $varTagValueNode instanceof VarTagValueNode) {
421+
return false;
422+
}
423+
424+
// a description keeps the docblock around anyway
425+
if ($varTagValueNode->description !== '') {
426+
return false;
427+
}
428+
429+
$varType = $this->staticTypeMapper->mapPHPStanPhpDocTypeToPHPStanType($varTagValueNode, $property);
430+
431+
// only class-typed @var narrowing is lost; generics, arrays and int ranges are preserved on promotion
432+
if (! $varType instanceof TypeWithClassName) {
433+
return false;
434+
}
435+
436+
$paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
437+
438+
return ! $this->typeComparator->areTypesEqual($varType, $paramType);
439+
}
440+
391441
private function shouldRemoveNullFromForPromotedParamType(Property $property, Param $param): bool
392442
{
393443
if (! $property->type instanceof Node || ! $param->type instanceof Node) {

0 commit comments

Comments
 (0)