From 2ab0e22b28ee3c9bd2a918c32170db1b6f9c68a0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 10 Sep 2025 14:51:53 +0200 Subject: [PATCH 1/4] Extract MutatingScope->isReadonlyPropertyFetchOnThis() --- src/Analyser/MutatingScope.php | 41 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8be043daf2..de769ab442 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -315,24 +315,9 @@ private function rememberConstructorExpressions(array $currentExpressionTypes): continue; } } elseif ($expr instanceof PropertyFetch) { - if ( - !$expr->name instanceof Node\Identifier - || !$expr->var instanceof Variable - || $expr->var->name !== 'this' - || !$this->phpVersion->supportsReadOnlyProperties() - ) { + if (!$this->isReadonlyPropertyFetchOnThis($expr)) { continue; } - - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); - if ($propertyReflection === null) { - continue; - } - - $nativePropertyReflection = $propertyReflection->getNativeReflection(); - if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { - continue; - } } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) { continue; } @@ -369,6 +354,30 @@ public function rememberConstructorScope(): self ); } + private function isReadonlyPropertyFetchOnThis(PropertyFetch $expr): bool + { + if ( + !$expr->name instanceof Node\Identifier + || !$expr->var instanceof Variable + || $expr->var->name !== 'this' + || !$this->phpVersion->supportsReadOnlyProperties() + ) { + return false; + } + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection === null) { + return false; + } + + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { + return false; + } + + return true; + } + /** @api */ public function isInClass(): bool { From 2cc1d8fc0640c2ae4f3cc0f25a238df335702e2f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 10 Sep 2025 14:53:59 +0200 Subject: [PATCH 2/4] Remember narrowed types from the constructor from deep PropertyFetches --- src/Analyser/MutatingScope.php | 37 +++++++++++-------- ...remember-readonly-constructor-narrowed.php | 36 ++++++++++++++++++ 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index de769ab442..36039672af 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -356,23 +356,30 @@ public function rememberConstructorScope(): self private function isReadonlyPropertyFetchOnThis(PropertyFetch $expr): bool { - if ( - !$expr->name instanceof Node\Identifier - || !$expr->var instanceof Variable - || $expr->var->name !== 'this' - || !$this->phpVersion->supportsReadOnlyProperties() - ) { - return false; - } + while ($expr instanceof PropertyFetch) { + if ($expr->var instanceof Variable) { + if ( + ! $expr->name instanceof Node\Identifier + || !is_string($expr->var->name) + || $expr->var->name !== 'this' + ) { + return false; + } + } elseif (!$expr->var instanceof PropertyFetch) { + return false; + } - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); - if ($propertyReflection === null) { - return false; - } + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($propertyReflection === null) { + return false; + } - $nativePropertyReflection = $propertyReflection->getNativeReflection(); - if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { - return false; + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { + return false; + } + + $expr = $expr->var; } return true; diff --git a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php index 7ab2ea364f..55f8351fad 100644 --- a/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php +++ b/tests/PHPStan/Analyser/nsrt/remember-readonly-constructor-narrowed.php @@ -2,6 +2,7 @@ namespace RememberReadOnlyConstructor; +use LogicException; use function PHPStan\Testing\assertType; class HelloWorldReadonlyProperty { @@ -107,3 +108,38 @@ public function doFoo() { assertType('4|10', $this->i); } } + +class Foo { + public readonly int $readonly; + public int $writable; + + public function __construct() + { + $this->readonly = 5; + $this->writable = rand(0,1) ? 5 : 10; + } +} + +class DeepPropertyFetching { + public readonly ?Foo $prop; + + public function __construct() { + $this->prop = new Foo(); + if($this->prop->readonly != 5) { + throw new LogicException(); + } + if ($this->prop->writable != 5) { + throw new LogicException(); + } + + assertType(Foo::class, $this->prop); + assertType('5', $this->prop->readonly); + assertType('5', $this->prop->writable); + } + + public function doFoo() { + assertType(Foo::class, $this->prop); + assertType('5', $this->prop->readonly); + assertType('int', $this->prop->writable); + } +} From 4518c9f997ebc91721b21b015c33764936c53652 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 10 Sep 2025 15:02:57 +0200 Subject: [PATCH 3/4] added back php version check --- src/Analyser/MutatingScope.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 36039672af..4381d8f076 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -356,6 +356,10 @@ public function rememberConstructorScope(): self private function isReadonlyPropertyFetchOnThis(PropertyFetch $expr): bool { + if (!$this->phpVersion->supportsReadOnlyProperties()) { + return false; + } + while ($expr instanceof PropertyFetch) { if ($expr->var instanceof Variable) { if ( From a9046f648aa0c2468be9e3bfada9bc5030b158f7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 10 Sep 2025 15:30:04 +0200 Subject: [PATCH 4/4] refactor shouldInvalidateExpression() --- src/Analyser/MutatingScope.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4381d8f076..caa7da7e93 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -315,7 +315,7 @@ private function rememberConstructorExpressions(array $currentExpressionTypes): continue; } } elseif ($expr instanceof PropertyFetch) { - if (!$this->isReadonlyPropertyFetchOnThis($expr)) { + if (!$this->isReadonlyPropertyFetch($expr, true)) { continue; } } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) { @@ -354,7 +354,7 @@ public function rememberConstructorScope(): self ); } - private function isReadonlyPropertyFetchOnThis(PropertyFetch $expr): bool + private function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool { if (!$this->phpVersion->supportsReadOnlyProperties()) { return false; @@ -363,9 +363,12 @@ private function isReadonlyPropertyFetchOnThis(PropertyFetch $expr): bool while ($expr instanceof PropertyFetch) { if ($expr->var instanceof Variable) { if ( - ! $expr->name instanceof Node\Identifier - || !is_string($expr->var->name) - || $expr->var->name !== 'this' + $allowOnlyOnThis + && ( + ! $expr->name instanceof Node\Identifier + || !is_string($expr->var->name) + || $expr->var->name !== 'this' + ) ) { return false; } @@ -4460,14 +4463,12 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr return false; } - if ($this->phpVersion->supportsReadOnlyProperties() && $expr instanceof PropertyFetch && $expr->name instanceof Node\Identifier && $requireMoreCharacters) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); - if ($propertyReflection !== null) { - $nativePropertyReflection = $propertyReflection->getNativeReflection(); - if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { - return false; - } - } + if ( + $expr instanceof PropertyFetch + && $requireMoreCharacters + && $this->isReadonlyPropertyFetch($expr, false) + ) { + return false; } return true;