diff --git a/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_param_reassign.php.inc b/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_param_reassign.php.inc new file mode 100644 index 00000000000..8bc6a114f66 --- /dev/null +++ b/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_param_reassign.php.inc @@ -0,0 +1,13 @@ +foo = $foo; + } +} diff --git a/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php b/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php index e10da090cab..1640184b5d1 100644 --- a/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php +++ b/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Property; +use Rector\Core\NodeAnalyzer\ParamAnalyzer; use Rector\Core\NodeManipulator\PropertyFetchAssignManipulator; use Rector\Core\NodeManipulator\PropertyManipulator; use Rector\Core\Rector\AbstractRector; @@ -29,6 +30,7 @@ final class ReadOnlyPropertyRector extends AbstractRector implements MinPhpVersi public function __construct( private readonly PropertyManipulator $propertyManipulator, private readonly PropertyFetchAssignManipulator $propertyFetchAssignManipulator, + private readonly ParamAnalyzer $paramAnalyzer, private readonly VisibilityManipulator $visibilityManipulator, ) { } @@ -156,6 +158,10 @@ private function refactorParam(Param $param): Param | null return null; } + if ($this->paramAnalyzer->isParamReassign($param)) { + return null; + } + $this->visibilityManipulator->makeReadonly($param); return $param; } diff --git a/src/NodeAnalyzer/ParamAnalyzer.php b/src/NodeAnalyzer/ParamAnalyzer.php index 8a66349ad2b..0c9ad7f73c1 100644 --- a/src/NodeAnalyzer/ParamAnalyzer.php +++ b/src/NodeAnalyzer/ParamAnalyzer.php @@ -5,6 +5,7 @@ namespace Rector\Core\NodeAnalyzer; use PhpParser\Node; +use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\ConstFetch; @@ -19,8 +20,10 @@ use PhpParser\NodeTraverser; use Rector\Core\NodeManipulator\FuncCallManipulator; use Rector\Core\PhpParser\Comparing\NodeComparator; +use Rector\Core\PhpParser\Node\BetterNodeFinder; use Rector\Core\PhpParser\Node\Value\ValueResolver; use Rector\NodeNameResolver\NodeNameResolver; +use Rector\NodeTypeResolver\Node\AttributeKey; use Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser; final class ParamAnalyzer @@ -31,6 +34,7 @@ public function __construct( private readonly NodeNameResolver $nodeNameResolver, private readonly FuncCallManipulator $funcCallManipulator, private readonly SimpleCallableNodeTraverser $simpleCallableNodeTraverser, + private readonly BetterNodeFinder $betterNodeFinder ) { } @@ -101,6 +105,30 @@ public function hasDefaultNull(Param $param): bool return $param->default instanceof ConstFetch && $this->valueResolver->isNull($param->default); } + public function isParamReassign(Param $param): bool + { + $classMethod = $param->getAttribute(AttributeKey::PARENT_NODE); + + if (! $classMethod instanceof ClassMethod) { + return false; + } + + $paramName = (string) $this->nodeNameResolver->getName($param->var); + return (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped($classMethod, function (Node $node) use ( + $paramName + ): bool { + if (! $node instanceof Assign) { + return false; + } + + if (! $node->var instanceof Variable) { + return false; + } + + return $this->nodeNameResolver->isName($node->var, $paramName); + }); + } + private function isVariableInClosureUses(Closure $closure, Variable $variable): bool { foreach ($closure->uses as $use) {