From a1cd53d0f6d84c82778900c29692ecf6c4e7f372 Mon Sep 17 00:00:00 2001 From: fluffycondor <7ionmail@gmail.com> Date: Thu, 4 May 2023 17:59:02 +0600 Subject: [PATCH] Allow dynamic properties from PHPDoc --- .../Fetch/AtomicPropertyFetchAnalyzer.php | 3 +- src/Psalm/Storage/ClassLikeStorage.php | 33 ++++++++ .../Fetch/AtomicPropertyFetchAnalyzerTest.php | 75 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 4224da09ca8..6636793836b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -1197,7 +1197,8 @@ private static function handleNonExistentProperty( ?string $var_id, bool &$has_valid_fetch_type ): void { - if ($config->use_phpdoc_property_without_magic_or_parent + if (($config->use_phpdoc_property_without_magic_or_parent + || $class_storage->hasAttributeIncludingParents('AllowDynamicProperties', $codebase)) && isset($class_storage->pseudo_property_get_types['$' . $prop_name]) ) { $stmt_type = $class_storage->pseudo_property_get_types['$' . $prop_name]; diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 0f184137f9e..d5e65cb7e35 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -4,6 +4,7 @@ use Psalm\Aliases; use Psalm\CodeLocation; +use Psalm\Codebase; use Psalm\Config; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\MethodIdentifier; @@ -13,7 +14,9 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; +use function array_map; use function array_values; +use function in_array; final class ClassLikeStorage implements HasAttributesInterface { @@ -486,6 +489,24 @@ public function getAttributeStorages(): array return $this->attributes; } + public function hasAttributeIncludingParents( + string $fq_class_name, + Codebase $codebase + ): bool { + if ($this->hasAttribute($fq_class_name)) { + return true; + } + + foreach ($this->parent_classes as $parent_class) { + $parent_class_storage = $codebase->classlike_storage_provider->get($parent_class); + if ($parent_class_storage->hasAttribute($fq_class_name)) { + return true; + } + } + + return false; + } + /** * Get the template constraint types for the class. * @@ -511,4 +532,16 @@ public function hasSealedMethods(Config $config): bool { return $this->sealed_methods ?? $config->seal_all_methods; } + + private function hasAttribute(string $fq_class_name): bool + { + return in_array( + $fq_class_name, + array_map( + fn(AttributeStorage $storage) => $storage->fq_class_name, + $this->attributes, + ), + true, + ); + } } diff --git a/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php b/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php new file mode 100644 index 00000000000..250f52dae6a --- /dev/null +++ b/tests/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzerTest.php @@ -0,0 +1,75 @@ + [ + 'code' => '$key = $value; + } + } + + echo (new A("foo", "bar"))->foo; + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'allowDynamicProperties for child' => [ + 'code' => '$key = $value; + } + } + + class B extends A {} + + echo (new B("foo", "bar"))->foo; + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'allowDynamicProperties for grandchild' => [ + 'code' => '$key = $value; + } + } + + class B extends A {} + class C extends B {} + + echo (new C("foo", "bar"))->foo; + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + ]; + } +}