From 35deb66f096644eb4eb9e428b2888fd3cca34175 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 30 Jun 2026 11:01:36 +0200 Subject: [PATCH] [Php81] Skip ReadOnlyPropertyRector on property changed via reference in foreach --- .../skip_changed_by_ref_in_foreach.php.inc | 16 ++++++++++ .../Property/ReadOnlyPropertyRector.php | 31 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_changed_by_ref_in_foreach.php.inc diff --git a/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_changed_by_ref_in_foreach.php.inc b/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_changed_by_ref_in_foreach.php.inc new file mode 100644 index 00000000000..0971532f612 --- /dev/null +++ b/rules-tests/Php81/Rector/Property/ReadOnlyPropertyRector/Fixture/skip_changed_by_ref_in_foreach.php.inc @@ -0,0 +1,16 @@ +relativeDateStrings = ['yesterday', 'today']; + foreach ($this->relativeDateStrings as &$string) { + $string = strtoupper($string); + } + } +} diff --git a/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php b/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php index 31a4fd1c300..38b33743a35 100644 --- a/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php +++ b/rules/Php81/Rector/Property/ReadOnlyPropertyRector.php @@ -13,6 +13,7 @@ use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Return_; use PhpParser\NodeVisitor; @@ -180,6 +181,11 @@ private function refactorProperty(Class_ $class, Property $property, Scope $scop return null; } + // changed via reference in foreach, e.g. foreach ($this->items as &$item), so it cannot be readonly + if ($this->isPropertyChangedInByRefForeach($class, (string) $this->getName($property))) { + return null; + } + $this->visibilityManipulator->makeReadonly($property); $this->removeReadOnlyDoc($property); @@ -247,6 +253,11 @@ private function refactorParam(Class_ $class, ClassMethod $classMethod, Param $p return null; } + // changed via reference in foreach, e.g. foreach ($this->items as &$item), so it cannot be readonly + if ($this->isPropertyChangedInByRefForeach($class, (string) $this->getName($param))) { + return null; + } + $this->visibilityManipulator->makeReadonly($param); $this->removeReadOnlyDoc($param); @@ -283,6 +294,26 @@ private function isPropertyReturnedByRef(Class_ $class, string $propertyName): b return false; } + private function isPropertyChangedInByRefForeach(Class_ $class, string $propertyName): bool + { + return (bool) $this->betterNodeFinder->findFirst($class, function (Node $node) use ($propertyName): bool { + if (! $node instanceof Foreach_) { + return false; + } + + if (! $node->byRef) { + return false; + } + + if (! $node->expr instanceof PropertyFetch) { + return false; + } + + return $this->isName($node->expr->var, 'this') + && $this->isName($node->expr, $propertyName); + }); + } + private function isPromotedPropertyAssigned(Class_ $class, Param $param): bool { $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT);