Skip to content

Commit 480d60c

Browse files
authored
Remember narrowed readonly types in nested property fetches in anonymous functions
1 parent 6f87293 commit 480d60c

File tree

3 files changed

+136
-1
lines changed

3 files changed

+136
-1
lines changed

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3735,15 +3735,19 @@ private function enterAnonymousFunctionWithoutReflection(
37353735
$nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType);
37363736
}
37373737

3738-
foreach ($this->invalidateStaticExpressions($this->expressionTypes) as $exprString => $typeHolder) {
3738+
$nonStaticExpressions = $this->invalidateStaticExpressions($this->expressionTypes);
3739+
foreach ($nonStaticExpressions as $exprString => $typeHolder) {
37393740
$expr = $typeHolder->getExpr();
3741+
37403742
if ($expr instanceof Variable) {
37413743
continue;
37423744
}
3745+
37433746
$variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class);
37443747
if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) {
37453748
continue;
37463749
}
3750+
37473751
foreach ($variables as $variable) {
37483752
if (!is_string($variable->name)) {
37493753
continue 2;
@@ -3760,6 +3764,22 @@ private function enterAnonymousFunctionWithoutReflection(
37603764
$node = new Variable('this');
37613765
$expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node));
37623766
$nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node));
3767+
3768+
if ($this->phpVersion->supportsReadOnlyProperties()) {
3769+
foreach ($nonStaticExpressions as $exprString => $typeHolder) {
3770+
$expr = $typeHolder->getExpr();
3771+
3772+
if (!$expr instanceof PropertyFetch) {
3773+
continue;
3774+
}
3775+
3776+
if (!$this->isReadonlyPropertyFetch($expr, true)) {
3777+
continue;
3778+
}
3779+
3780+
$expressionTypes[$exprString] = $typeHolder;
3781+
}
3782+
}
37633783
}
37643784

37653785
return $this->scopeFactory->create(
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13321;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
public function __construct(public readonly string $value)
10+
{
11+
}
12+
}
13+
14+
class Bar
15+
{
16+
public function __construct(
17+
private readonly ?Foo $foo,
18+
private ?Foo $writableFoo = null,
19+
)
20+
{
21+
}
22+
23+
public function bar(): void
24+
{
25+
(function () {
26+
assertType(Foo::class.'|null', $this->foo);
27+
assertType(Foo::class.'|null', $this->writableFoo);
28+
29+
echo $this->foo->value;
30+
})();
31+
32+
if ($this->foo === null) {
33+
return;
34+
}
35+
if ($this->writableFoo === null) {
36+
return;
37+
}
38+
39+
(function () {
40+
assertType(Foo::class, $this->foo);
41+
assertType(Foo::class.'|null', $this->writableFoo);
42+
43+
echo $this->foo->value;
44+
})();
45+
46+
$test = function () {
47+
assertType(Foo::class, $this->foo);
48+
assertType(Foo::class.'|null', $this->writableFoo);
49+
50+
echo $this->foo->value;
51+
};
52+
53+
$test();
54+
55+
$test = static function () {
56+
assertType('mixed', $this->foo);
57+
assertType('mixed', $this->writableFoo);
58+
59+
echo $this->foo->value;
60+
};
61+
62+
$test();
63+
}
64+
65+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php // lint >= 8.2
2+
3+
namespace Bug13321b;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
public function __construct(
10+
public string $value,
11+
readonly public string $readonlyValue,
12+
)
13+
{
14+
}
15+
}
16+
17+
readonly class Bar
18+
{
19+
public function __construct(
20+
private ?Foo $foo,
21+
)
22+
{
23+
}
24+
25+
public function bar(): void
26+
{
27+
if ($this->foo === null) {
28+
return;
29+
}
30+
if ($this->foo->value === '') {
31+
return;
32+
}
33+
if ($this->foo->readonlyValue === '') {
34+
return;
35+
}
36+
37+
assertType(Foo::class, $this->foo);
38+
assertType('non-empty-string', $this->foo->value);
39+
assertType('non-empty-string', $this->foo->readonlyValue);
40+
41+
$test = function () {
42+
assertType(Foo::class, $this->foo);
43+
assertType('string', $this->foo->value);
44+
assertType('non-empty-string', $this->foo->readonlyValue);
45+
};
46+
47+
$test();
48+
}
49+
50+
}

0 commit comments

Comments
 (0)