Skip to content

Commit 051b06e

Browse files
committed
PHP 8.2: Non-final classes without #[AllowDynamicProperties] might still have dynamic properties
Closes phpstan/phpstan#8727 Closes phpstan/phpstan#8474
1 parent 279c781 commit 051b06e

File tree

11 files changed

+248
-34
lines changed

11 files changed

+248
-34
lines changed

src/Type/ObjectType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ public function hasProperty(string $propertyName): TrinaryLogic
143143
return TrinaryLogic::createMaybe();
144144
}
145145

146+
if (!$classReflection->isFinal()) {
147+
return TrinaryLogic::createMaybe();
148+
}
149+
146150
return TrinaryLogic::createNo();
147151
}
148152

tests/PHPStan/Analyser/data/array-column-php82.php

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace ArrayColumn;
3+
namespace ArrayColumn82;
44

55
use DOMElement;
66
use function PHPStan\Testing\assertType;
@@ -175,10 +175,10 @@ public function testImprecise5(array $array): void
175175
assertType('list<string>', array_column($array, 'nodeName'));
176176
assertType('array<string, string>', array_column($array, 'nodeName', 'tagName'));
177177
assertType('array<string, DOMElement>', array_column($array, null, 'tagName'));
178-
assertType('array{}', array_column($array, 'foo'));
179-
assertType('array{}', array_column($array, 'foo', 'tagName'));
180-
assertType('array<*NEVER*, string>', array_column($array, 'nodeName', 'foo'));
181-
assertType('array<*NEVER*, DOMElement>', array_column($array, null, 'foo'));
178+
assertType('list<mixed>', array_column($array, 'foo'));
179+
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
180+
assertType('array<int|string, string>', array_column($array, 'nodeName', 'foo'));
181+
assertType('array<int|string, DOMElement>', array_column($array, null, 'foo'));
182182
}
183183

184184
/** @param non-empty-array<int, DOMElement> $array */
@@ -187,10 +187,10 @@ public function testObjects1(array $array): void
187187
assertType('non-empty-list<string>', array_column($array, 'nodeName'));
188188
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
189189
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
190-
assertType('array{}', array_column($array, 'foo'));
191-
assertType('array{}', array_column($array, 'foo', 'tagName'));
192-
assertType('non-empty-array<*NEVER*, string>', array_column($array, 'nodeName', 'foo'));
193-
assertType('non-empty-array<*NEVER*, DOMElement>', array_column($array, null, 'foo'));
190+
assertType('list<mixed>', array_column($array, 'foo'));
191+
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
192+
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
193+
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
194194
}
195195

196196
/** @param array{DOMElement} $array */
@@ -199,10 +199,22 @@ public function testObjects2(array $array): void
199199
assertType('array{string}', array_column($array, 'nodeName'));
200200
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
201201
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
202-
assertType('array{*NEVER*}', array_column($array, 'foo'));
203-
assertType('non-empty-array<string, *NEVER*>', array_column($array, 'foo', 'tagName'));
204-
assertType('non-empty-array<*NEVER*, string>', array_column($array, 'nodeName', 'foo'));
205-
assertType('non-empty-array<*NEVER*, DOMElement>', array_column($array, null, 'foo'));
202+
assertType('list<mixed>', array_column($array, 'foo'));
203+
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
204+
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
205+
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
206+
}
207+
208+
}
209+
210+
final class Foo
211+
{
212+
213+
/** @param array<int, self> $a */
214+
public function doFoo(array $a): void
215+
{
216+
assertType('array{}', array_column($a, 'nodeName'));
217+
assertType('array{}', array_column($a, 'nodeName', 'tagName'));
206218
}
207219

208220
}

tests/PHPStan/Analyser/data/array-column.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,15 @@ public function testObjects2(array $array): void
220220
}
221221

222222
}
223+
224+
final class Foo
225+
{
226+
227+
/** @param array<int, self> $a */
228+
public function doFoo(array $a): void
229+
{
230+
assertType('list<mixed>', array_column($a, 'nodeName'));
231+
assertType('array<int|string, mixed>', array_column($a, 'nodeName', 'tagName'));
232+
}
233+
234+
}

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,4 +657,18 @@ public function testDocblockAssertEquality(): void
657657
]);
658658
}
659659

660+
public function testBug8727(): void
661+
{
662+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
663+
$this->treatPhpDocTypesAsCertain = true;
664+
$this->analyse([__DIR__ . '/data/bug-8727.php'], []);
665+
}
666+
667+
public function testBug8474(): void
668+
{
669+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
670+
$this->treatPhpDocTypesAsCertain = true;
671+
$this->analyse([__DIR__ . '/data/bug-8474.php'], []);
672+
}
673+
660674
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Bug8474;
4+
5+
class World {}
6+
class HelloWorld extends World {
7+
public string $hello = 'world';
8+
}
9+
10+
function hello(World $world): bool {
11+
return property_exists($world, 'hello');
12+
}
13+
14+
class Alpha
15+
{
16+
public function __construct()
17+
{
18+
if (property_exists($this, 'data')) {
19+
$this->data = 'Hello';
20+
}
21+
}
22+
}
23+
24+
class Beta extends Alpha
25+
{
26+
/** @var string|null */
27+
public $data = null;
28+
}
29+
30+
class Delta extends Alpha
31+
{
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Bug8727;
4+
5+
abstract class Foo
6+
{
7+
abstract public function hello(): void;
8+
9+
protected function message(): string
10+
{
11+
if (property_exists($this, 'lala')) {
12+
return 'Lala!';
13+
}
14+
15+
return 'Hello!';
16+
}
17+
}
18+
19+
class Bar extends Foo {
20+
protected bool $lala = true;
21+
22+
public function hello(): void
23+
{
24+
echo $this->message();
25+
}
26+
}

tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Rules\RuleLevelHelper;
77
use PHPStan\Testing\RuleTestCase;
8+
use function array_merge;
89
use const PHP_VERSION_ID;
910

1011
/**
@@ -564,6 +565,13 @@ public function testBug3659(): void
564565
public function dataDynamicProperties(): array
565566
{
566567
$errors = [
568+
[
569+
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
570+
23,
571+
],
572+
];
573+
574+
$errorsWithMore = array_merge([
567575
[
568576
'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.',
569577
9,
@@ -588,24 +596,48 @@ public function dataDynamicProperties(): array
588596
'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.',
589597
16,
590598
],
599+
], $errors);
600+
601+
$errorsWithMore = array_merge($errorsWithMore, [
591602
[
592603
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
593-
23,
604+
26,
594605
],
595-
];
606+
[
607+
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
608+
27,
609+
],
610+
[
611+
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
612+
28,
613+
],
614+
]);
596615

597-
$errorsWithMore = $errors;
598-
$errorsWithMore[] = [
599-
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
600-
26,
601-
];
602-
$errorsWithMore[] = [
603-
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
604-
27,
605-
];
606-
$errorsWithMore[] = [
607-
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
608-
28,
616+
$otherErrors = [
617+
[
618+
'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.',
619+
36,
620+
],
621+
[
622+
'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.',
623+
37,
624+
],
625+
[
626+
'Access to an undefined property DynamicProperties\FinalFoo::$dynamicProperty.',
627+
38,
628+
],
629+
[
630+
'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.',
631+
41,
632+
],
633+
[
634+
'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.',
635+
42,
636+
],
637+
[
638+
'Access to an undefined property DynamicProperties\FinalBar::$dynamicProperty.',
639+
43,
640+
],
609641
];
610642

611643
return [
@@ -614,8 +646,8 @@ public function dataDynamicProperties(): array
614646
'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.',
615647
23,
616648
],
617-
] : $errors],
618-
[true, $errorsWithMore],
649+
] : array_merge($errors, $otherErrors)],
650+
[true, array_merge($errorsWithMore, $otherErrors)],
619651
];
620652
}
621653

@@ -675,15 +707,25 @@ public function testPhp82AndDynamicProperties(bool $b): void
675707
'Access to an undefined property Php82DynamicProperties\ClassA::$properties.',
676708
34,
677709
];
710+
if ($b) {
711+
$errors[] = [
712+
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',
713+
71,
714+
];
715+
}
678716
$errors[] = [
679-
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',
680-
71,
717+
'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.',
718+
105,
681719
];
682720
} elseif ($b) {
683721
$errors[] = [
684722
'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.',
685723
71,
686724
];
725+
$errors[] = [
726+
'Access to an undefined property Php82DynamicProperties\FinalHelloWorld::$world.',
727+
105,
728+
];
687729
}
688730
$this->checkThisOnly = false;
689731
$this->checkUnionTypes = true;

tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ public function testAccessStaticPropertiesPhp82(): void
373373
'Cannot access static property $anotherProperty on ClassOrString|false.',
374374
150,
375375
],
376+
[
377+
'Static access to instance property ClassOrString::$instanceProperty.',
378+
152,
379+
],
376380
[
377381
'Access to an undefined static property AccessInIsset::$foo.',
378382
178,

tests/PHPStan/Rules/Properties/data/dynamic-properties.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ public function doBar() {
2929
}
3030
}
3131

32+
final class FinalBar {}
33+
34+
final class FinalFoo {
35+
public function doBar() {
36+
isset($this->dynamicProperty);
37+
empty($this->dynamicProperty);
38+
$this->dynamicProperty ?? 'test';
39+
40+
$bar = new FinalBar();
41+
isset($bar->dynamicProperty);
42+
empty($bar->dynamicProperty);
43+
$bar->dynamicProperty ?? 'test';
44+
}
45+
}

tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,37 @@ function (): void {
7373
echo $hello->world;
7474
}
7575
};
76+
77+
final class FinalHelloWorld
78+
{
79+
public function __get(string $attribute): mixed
80+
{
81+
if($attribute == "world")
82+
{
83+
return "Hello World";
84+
}
85+
throw new \Exception("Attribute '{$attribute}' is invalid");
86+
}
87+
88+
89+
public function __isset(string $attribute)
90+
{
91+
try {
92+
if (!isset($this->{$attribute})) {
93+
$x = $this->{$attribute};
94+
}
95+
96+
return isset($this->{$attribute});
97+
} catch (\Exception $e) {
98+
return false;
99+
}
100+
}
101+
}
102+
103+
function (): void {
104+
$hello = new FinalHelloWorld();
105+
if(isset($hello->world))
106+
{
107+
echo $hello->world;
108+
}
109+
};

0 commit comments

Comments
 (0)