From 8bd73706ed4cf064484d08803dd7e5b8c4f34993 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 23 Sep 2022 10:40:26 +0200 Subject: [PATCH] Test PHP 8.2 dynamic properties behaviour --- src/Reflection/ClassReflection.php | 22 ++++-- tests/PHPStan/Analyser/data/bug-1216.php | 2 + .../classPhpDocs-phpstanPropertyPrefix.php | 2 + .../Analyser/data/properties-defined.php | 2 + .../data/annotations-properties.php | 2 + .../Properties/AccessPropertiesRuleTest.php | 55 ++++++++++++- .../ReadingWriteOnlyPropertiesRuleTest.php | 12 +-- .../TypesAssignedToPropertiesRuleTest.php | 4 +- .../WritingToReadOnlyPropertiesRuleTest.php | 14 ++-- .../Rules/Properties/data/bug-1216.php | 3 + tests/PHPStan/Rules/Properties/data/mixin.php | 4 + .../data/php-82-dynamic-properties-allow.php | 79 +++++++++++++++++++ .../data/php-82-dynamic-properties.php | 75 ++++++++++++++++++ .../data/reading-write-only-properties.php | 3 + .../data/writing-to-read-only-properties.php | 3 + 15 files changed, 260 insertions(+), 22 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties-allow.php create mode 100644 tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 63b371a07a..caa75e944c 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -350,15 +350,25 @@ public function allowsDynamicProperties(): bool return true; } - if ($this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset')) { - return true; + $class = $this; + $attributes = $class->reflection->getAttributes('AllowDynamicProperties'); + while (count($attributes) === 0 && $class->getParentClass() !== null) { + $attributes = $class->getParentClass()->reflection->getAttributes('AllowDynamicProperties'); + $class = $class->getParentClass(); } - $attributes = $this->reflection->getAttributes('AllowDynamicProperties'); - return count($attributes) > 0; } + private function allowsDynamicPropertiesExtensions(): bool + { + if ($this->allowsDynamicProperties()) { + return true; + } + + return $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset'); + } + public function hasProperty(string $propertyName): bool { if ($this->isEnum()) { @@ -366,7 +376,7 @@ public function hasProperty(string $propertyName): bool } foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { - if ($i > 0 && !$this->allowsDynamicProperties()) { + if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { continue; } if ($extension->hasProperty($this, $propertyName)) { @@ -518,7 +528,7 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco } if (!isset($this->properties[$key])) { foreach ($this->propertiesClassReflectionExtensions as $i => $extension) { - if ($i > 0 && !$this->allowsDynamicProperties()) { + if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) { continue; } if (!$extension->hasProperty($this, $propertyName)) { diff --git a/tests/PHPStan/Analyser/data/bug-1216.php b/tests/PHPStan/Analyser/data/bug-1216.php index 1ccb7d093d..7c0beae95d 100644 --- a/tests/PHPStan/Analyser/data/bug-1216.php +++ b/tests/PHPStan/Analyser/data/bug-1216.php @@ -2,6 +2,7 @@ namespace Bug1216; +use AllowDynamicProperties; use function PHPStan\Testing\assertType; abstract class Foo @@ -27,6 +28,7 @@ trait Bar * @property string $bar * @property string $untypedBar */ +#[AllowDynamicProperties] class Baz extends Foo { diff --git a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php b/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php index f08edc152b..c528b5cffe 100644 --- a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php +++ b/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php @@ -2,6 +2,7 @@ namespace ClassPhpDocsNamespace; +use AllowDynamicProperties; use function PHPStan\Testing\assertType; /** @@ -16,6 +17,7 @@ * @property-write string $baz * @phpstan-property-write int $baz */ +#[AllowDynamicProperties] class PhpstanProperties { public function doFoo() diff --git a/tests/PHPStan/Analyser/data/properties-defined.php b/tests/PHPStan/Analyser/data/properties-defined.php index 2ca412e875..cdc4cbb255 100644 --- a/tests/PHPStan/Analyser/data/properties-defined.php +++ b/tests/PHPStan/Analyser/data/properties-defined.php @@ -2,6 +2,7 @@ namespace PropertiesNamespace; +use AllowDynamicProperties; use DOMDocument; use SomeNamespace\Sit as Dolor; @@ -9,6 +10,7 @@ * @property-read int $readOnlyProperty * @property-read int $overriddenReadOnlyProperty */ +#[AllowDynamicProperties] class Bar extends DOMDocument { diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php b/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php index fc7363ccfc..240e142c62 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-properties.php @@ -2,6 +2,7 @@ namespace AnnotationsProperties; +use AllowDynamicProperties; use OtherNamespace\Test as OtherTest; use OtherNamespace\Ipsum; @@ -12,6 +13,7 @@ * @property Ipsum $conflictingProperty * @property Foo $overridenProperty */ +#[AllowDynamicProperties] class Foo implements FooInterface { diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 4e5ed1aad8..78b6b79cb3 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -395,7 +395,7 @@ public function testMixin(): void $this->analyse([__DIR__ . '/data/mixin.php'], [ [ 'Access to an undefined property MixinProperties\GenericFoo::$namee.', - 51, + 55, ], ]); } @@ -656,4 +656,57 @@ public function testBug3171OnDynamicProperties(): void $this->analyse([__DIR__ . '/data/bug-3171.php'], []); } + public function dataTrueAndFalse(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataTrueAndFalse + */ + public function testPhp82AndDynamicProperties(bool $b): void + { + $errors = []; + if (PHP_VERSION_ID >= 80200) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\ClassA::$properties.', + 34, + ]; + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + ]; + } elseif ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicProperties\HelloWorld::$world.', + 71, + ]; + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = $b; + $this->analyse([__DIR__ . '/data/php-82-dynamic-properties.php'], $errors); + } + + /** + * @dataProvider dataTrueAndFalse + */ + public function testPhp82AndDynamicPropertiesAllow(bool $b): void + { + $errors = []; + if ($b) { + $errors[] = [ + 'Access to an undefined property Php82DynamicPropertiesAllow\HelloWorld::$world.', + 75, + ]; + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = $b; + $this->analyse([__DIR__ . '/data/php-82-dynamic-properties-allow.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php index 1fda6853f9..5044bf10dc 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -25,11 +25,11 @@ public function testPropertyMustBeReadableInAssignOp(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 22, + 25, ], [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 32, + 35, ], ]); } @@ -40,7 +40,7 @@ public function testPropertyMustBeReadableInAssignOpCheckThisOnly(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 22, + 25, ], ]); } @@ -51,11 +51,11 @@ public function testReadingWriteOnlyProperties(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 17, + 20, ], [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 22, + 25, ], ]); } @@ -66,7 +66,7 @@ public function testReadingWriteOnlyPropertiesCheckThisOnly(): void $this->analyse([__DIR__ . '/data/reading-write-only-properties.php'], [ [ 'Property ReadingWriteOnlyProperties\Foo::$writeOnlyProperty is not readable.', - 17, + 20, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 84dc5c0164..7e78d519a7 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -115,11 +115,11 @@ public function testBug1216(): void $this->analyse([__DIR__ . '/data/bug-1216.php'], [ [ 'Property Bug1216PropertyTest\Baz::$untypedBar (string) does not accept int.', - 35, + 38, ], [ 'Property Bug1216PropertyTest\Dummy::$foo (Exception) does not accept stdClass.', - 59, + 62, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php index 320218a0dc..358fb56411 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -25,11 +25,11 @@ public function testCheckThisOnlyProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 15, + 18, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 16, + 19, ], ]); } @@ -40,23 +40,23 @@ public function testCheckAllProperties(): void $this->analyse([__DIR__ . '/data/writing-to-read-only-properties.php'], [ [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 15, + 18, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 16, + 19, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 25, + 28, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 26, + 29, ], [ 'Property WritingToReadOnlyProperties\Foo::$readOnlyProperty is not writable.', - 35, + 38, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/bug-1216.php b/tests/PHPStan/Rules/Properties/data/bug-1216.php index 756a4b6bf7..0338e8373a 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-1216.php +++ b/tests/PHPStan/Rules/Properties/data/bug-1216.php @@ -2,6 +2,8 @@ namespace Bug1216PropertyTest; +use AllowDynamicProperties; + abstract class Foo { /** @@ -25,6 +27,7 @@ trait Bar * @property string $bar * @property string $untypedBar */ +#[AllowDynamicProperties] class Baz extends Foo { diff --git a/tests/PHPStan/Rules/Properties/data/mixin.php b/tests/PHPStan/Rules/Properties/data/mixin.php index e6ba546647..21c94ffb73 100644 --- a/tests/PHPStan/Rules/Properties/data/mixin.php +++ b/tests/PHPStan/Rules/Properties/data/mixin.php @@ -2,6 +2,8 @@ namespace MixinProperties; +use AllowDynamicProperties; + class Foo { @@ -12,6 +14,7 @@ class Foo /** * @mixin Foo */ +#[AllowDynamicProperties] class Bar { @@ -34,6 +37,7 @@ function (Baz $baz): void { * @template T * @mixin T */ +#[AllowDynamicProperties] class GenericFoo { diff --git a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties-allow.php b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties-allow.php new file mode 100644 index 0000000000..9559322bd8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties-allow.php @@ -0,0 +1,79 @@ + $properties + */ +trait TraitA { + /** + * @var array + */ + public array $items = []; +} + +/** + * @phpstan-use TraitA + */ +#[AllowDynamicProperties] +class ClassA { + /** + * @phpstan-use TraitA + */ + use TraitA; +} + +class ClassB { + public function test(): void { + // empty + } +} + +function (): void { + foreach ((new ClassA())->properties as $property) { + $property->test(); + } + + foreach ((new ClassA())->items as $item) { + $item->test(); + } +}; + +#[AllowDynamicProperties] +class HelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new HelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; diff --git a/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php new file mode 100644 index 0000000000..1474072a75 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/php-82-dynamic-properties.php @@ -0,0 +1,75 @@ + $properties + */ +trait TraitA { + /** + * @var array + */ + public array $items = []; +} + +/** + * @phpstan-use TraitA + */ +class ClassA { + /** + * @phpstan-use TraitA + */ + use TraitA; +} + +class ClassB { + public function test(): void { + // empty + } +} + +function (): void { + foreach ((new ClassA())->properties as $property) { + $property->test(); + } + + foreach ((new ClassA())->items as $item) { + $item->test(); + } +}; + +class HelloWorld +{ + public function __get(string $attribute): mixed + { + if($attribute == "world") + { + return "Hello World"; + } + throw new \Exception("Attribute '{$attribute}' is invalid"); + } + + + public function __isset(string $attribute) + { + try { + if (!isset($this->{$attribute})) { + $x = $this->{$attribute}; + } + + return isset($this->{$attribute}); + } catch (\Exception $e) { + return false; + } + } +} + +function (): void { + $hello = new HelloWorld(); + if(isset($hello->world)) + { + echo $hello->world; + } +}; diff --git a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php index 9dd1f23695..38656a0d66 100644 --- a/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/reading-write-only-properties.php @@ -2,11 +2,14 @@ namespace ReadingWriteOnlyProperties; +use AllowDynamicProperties; + /** * @property-read int $readOnlyProperty * @property int $usualProperty * @property-write int $writeOnlyProperty */ +#[AllowDynamicProperties] class Foo { diff --git a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php index 23ea31efdc..23cc8fcb14 100644 --- a/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php +++ b/tests/PHPStan/Rules/Properties/data/writing-to-read-only-properties.php @@ -2,11 +2,14 @@ namespace WritingToReadOnlyProperties; +use AllowDynamicProperties; + /** * @property-read int $readOnlyProperty * @property int $usualProperty * @property-write int $writeOnlyProperty */ +#[AllowDynamicProperties] class Foo {