diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8be043daf2..caa7da7e93 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() - ) { - continue; - } - - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); - if ($propertyReflection === null) { + if (!$this->isReadonlyPropertyFetch($expr, true)) { continue; } - - $nativePropertyReflection = $propertyReflection->getNativeReflection(); - if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { - continue; - } } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) { continue; } @@ -369,6 +354,44 @@ public function rememberConstructorScope(): self ); } + private function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool + { + if (!$this->phpVersion->supportsReadOnlyProperties()) { + return false; + } + + while ($expr instanceof PropertyFetch) { + if ($expr->var instanceof Variable) { + if ( + $allowOnlyOnThis + && ( + ! $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; + } + + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) { + return false; + } + + $expr = $expr->var; + } + + return true; + } + /** @api */ public function isInClass(): bool { @@ -4440,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; 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); + } +}