From 848efbe9ea2d9737ab7345e68959877aae3d4613 Mon Sep 17 00:00:00 2001 From: Kayw Date: Fri, 27 Jun 2025 09:44:54 +0800 Subject: [PATCH] Improve property access checking with property tag support --- .../Properties/AccessPropertiesCheck.php | 14 +++++++- .../Properties/AccessPropertiesRuleTest.php | 33 ++++++++++++++++++- .../Rules/Properties/data/property-exists.php | 9 ++--- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index a92c8ba5dd..655bdf26d8 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -23,9 +23,11 @@ use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_keys; use function array_map; use function array_merge; use function count; +use function in_array; use function sprintf; #[AutowiredService] @@ -125,10 +127,17 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } } + $propertyTags = []; if (count($classNames) === 1) { $propertyClassReflection = $this->reflectionProvider->getClass($classNames[0]); $parentClassReflection = $propertyClassReflection->getParentClass(); + $propertyTags = $propertyClassReflection->isInterface() ? [] : $propertyClassReflection->getPropertyTags(); + while ($parentClassReflection !== null) { + if (!$parentClassReflection->isInterface()) { + $propertyTags[] = $parentClassReflection->getPropertyTags(); + } + if ($parentClassReflection->hasProperty($name)) { if ($write) { if ($scope->canWriteProperty($parentClassReflection->getProperty($name, $scope))) { @@ -146,11 +155,14 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ))->identifier('property.private')->build(), ]; } - $parentClassReflection = $parentClassReflection->getParentClass(); } } + if (in_array($name, array_keys($propertyTags), true)) { + return []; + } + if ($node->name instanceof Expr) { $propertyExistsExpr = new FuncCall(new FullyQualified('property_exists'), [ new Arg($node->var), diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 5d3697767d..4e9f5110fb 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1019,7 +1019,38 @@ public function testPropertyExists(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = true; - $this->analyse([__DIR__ . '/data/property-exists.php'], []); + $this->analyse([__DIR__ . '/data/property-exists.php'], [ + [ + 'Access to an undefined property PropertyExists\Model::$getCreatedByColumn.', + 27, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + [ + 'Access to an undefined property PropertyExists\Model::$getUpdatedByColumn.', + 27, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + [ + 'Access to an undefined property PropertyExists\Model::$getDeletedByColumn.', + 27, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + [ + 'Access to an undefined property PropertyExists\Model::$getCreatedAtColumn.', + 27, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + [ + 'Access to an undefined property PropertyExists\Model::$getUpdatedAtColumn.', + 27, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + [ + 'Access to an undefined property PropertyExists\Model::$getDeletedAtColumn.', + 27, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + ]); } } diff --git a/tests/PHPStan/Rules/Properties/data/property-exists.php b/tests/PHPStan/Rules/Properties/data/property-exists.php index cb7e998027..712b0c5d8a 100644 --- a/tests/PHPStan/Rules/Properties/data/property-exists.php +++ b/tests/PHPStan/Rules/Properties/data/property-exists.php @@ -2,9 +2,11 @@ namespace PropertyExists; +/** + * @property-read \stdClass $getCreator + */ class Model { - } class Defaults @@ -12,6 +14,7 @@ class Defaults public function defaults(Model $model): void { $columns = [ + 'getCreator', 'getCreatedByColumn', 'getUpdatedByColumn', 'getDeletedByColumn', @@ -21,9 +24,7 @@ public function defaults(Model $model): void ]; foreach ($columns as $column) { - if (property_exists($model, $column)) { - echo $model->{$column}; - } + echo $model->{$column}; } } }