From 13ff4e2d949d129c1fb3da4abd136470a1d0166f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 11:16:46 +0200 Subject: [PATCH] [Php81] Skip readonly on property returned by reference in ReadOnlyPropertyRector --- .../Fixture/skip_returned_by_ref.php.inc | 16 ++++++++ .../Property/ReadOnlyPropertyRector.php | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_returned_by_ref.php.inc diff --git a/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_returned_by_ref.php.inc b/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_returned_by_ref.php.inc new file mode 100644 index 00000000000..923edcab7c9 --- /dev/null +++ b/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_returned_by_ref.php.inc @@ -0,0 +1,16 @@ +byLine; + } +} diff --git a/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php b/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php index 66c3f6c111f..31a4fd1c300 100644 --- a/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php +++ b/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php @@ -14,6 +14,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\Return_; use PhpParser\NodeVisitor; use PHPStan\Analyser\Scope; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; @@ -174,6 +175,11 @@ private function refactorProperty(Class_ $class, Property $property, Scope $scop return null; } + // returned by reference can be mutated outside the class, so it cannot be readonly + if ($this->isPropertyReturnedByRef($class, (string) $this->getName($property))) { + return null; + } + $this->visibilityManipulator->makeReadonly($property); $this->removeReadOnlyDoc($property); @@ -236,6 +242,11 @@ private function refactorParam(Class_ $class, ClassMethod $classMethod, Param $p return null; } + // returned by reference can be mutated outside the class, so it cannot be readonly + if ($this->isPropertyReturnedByRef($class, (string) $this->getName($param))) { + return null; + } + $this->visibilityManipulator->makeReadonly($param); $this->removeReadOnlyDoc($param); @@ -243,6 +254,35 @@ private function refactorParam(Class_ $class, ClassMethod $classMethod, Param $p return $param; } + private function isPropertyReturnedByRef(Class_ $class, string $propertyName): bool + { + foreach ($class->getMethods() as $classMethod) { + if (! $classMethod->byRef) { + continue; + } + + $returns = $this->betterNodeFinder->findInstanceOf($classMethod, Return_::class); + foreach ($returns as $return) { + if (! $return->expr instanceof Expr) { + continue; + } + + $propertyFetch = $this->betterNodeFinder->findFirst( + $return->expr, + fn (Node $subNode): bool => $subNode instanceof PropertyFetch + && $this->isName($subNode->var, 'this') + && $this->isName($subNode, $propertyName) + ); + + if ($propertyFetch instanceof PropertyFetch) { + return true; + } + } + } + + return false; + } + private function isPromotedPropertyAssigned(Class_ $class, Param $param): bool { $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT);