diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index ff8b6fdfb0..7b0c7d239c 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3735,15 +3735,19 @@ private function enterAnonymousFunctionWithoutReflection( $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType); } - foreach ($this->invalidateStaticExpressions($this->expressionTypes) as $exprString => $typeHolder) { + $nonStaticExpressions = $this->invalidateStaticExpressions($this->expressionTypes); + foreach ($nonStaticExpressions as $exprString => $typeHolder) { $expr = $typeHolder->getExpr(); + if ($expr instanceof Variable) { continue; } + $variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class); if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) { continue; } + foreach ($variables as $variable) { if (!is_string($variable->name)) { continue 2; @@ -3760,6 +3764,22 @@ private function enterAnonymousFunctionWithoutReflection( $node = new Variable('this'); $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node)); $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node)); + + if ($this->phpVersion->supportsReadOnlyProperties()) { + foreach ($nonStaticExpressions as $exprString => $typeHolder) { + $expr = $typeHolder->getExpr(); + + if (!$expr instanceof PropertyFetch) { + continue; + } + + if (!$this->isReadonlyPropertyFetch($expr, true)) { + continue; + } + + $expressionTypes[$exprString] = $typeHolder; + } + } } return $this->scopeFactory->create( diff --git a/tests/PHPStan/Analyser/nsrt/bug-13321.php b/tests/PHPStan/Analyser/nsrt/bug-13321.php new file mode 100644 index 0000000000..66a1f724d5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13321.php @@ -0,0 +1,65 @@ += 8.1 + +namespace Bug13321; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function __construct(public readonly string $value) + { + } +} + +class Bar +{ + public function __construct( + private readonly ?Foo $foo, + private ?Foo $writableFoo = null, + ) + { + } + + public function bar(): void + { + (function () { + assertType(Foo::class.'|null', $this->foo); + assertType(Foo::class.'|null', $this->writableFoo); + + echo $this->foo->value; + })(); + + if ($this->foo === null) { + return; + } + if ($this->writableFoo === null) { + return; + } + + (function () { + assertType(Foo::class, $this->foo); + assertType(Foo::class.'|null', $this->writableFoo); + + echo $this->foo->value; + })(); + + $test = function () { + assertType(Foo::class, $this->foo); + assertType(Foo::class.'|null', $this->writableFoo); + + echo $this->foo->value; + }; + + $test(); + + $test = static function () { + assertType('mixed', $this->foo); + assertType('mixed', $this->writableFoo); + + echo $this->foo->value; + }; + + $test(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13321b.php b/tests/PHPStan/Analyser/nsrt/bug-13321b.php new file mode 100644 index 0000000000..27e6a25fe8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13321b.php @@ -0,0 +1,50 @@ += 8.2 + +namespace Bug13321b; + +use function PHPStan\Testing\assertType; + +class Foo +{ + public function __construct( + public string $value, + readonly public string $readonlyValue, + ) + { + } +} + +readonly class Bar +{ + public function __construct( + private ?Foo $foo, + ) + { + } + + public function bar(): void + { + if ($this->foo === null) { + return; + } + if ($this->foo->value === '') { + return; + } + if ($this->foo->readonlyValue === '') { + return; + } + + assertType(Foo::class, $this->foo); + assertType('non-empty-string', $this->foo->value); + assertType('non-empty-string', $this->foo->readonlyValue); + + $test = function () { + assertType(Foo::class, $this->foo); + assertType('string', $this->foo->value); + assertType('non-empty-string', $this->foo->readonlyValue); + }; + + $test(); + } + +}