diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index 09453458d3..99d1c142fd 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -42,6 +42,8 @@ public function __construct( private bool $checkDynamicProperties, #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] private bool $checkNonStringableDynamicAccess, + #[AutowiredParameter] + private bool $checkThisOnly, ) { } @@ -58,20 +60,7 @@ public function check(PropertyFetch $node, Scope $scope, bool $write): array $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); if (!$write && $this->checkNonStringableDynamicAccess) { - $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->name, - '', - static fn (Type $type) => $type->toString()->isString()->yes(), - ); - $nameType = $nameTypeResult->getType(); - if ($nameType instanceof ErrorType || $nameType->toString() instanceof ErrorType || !$nameType->toString()->isString()->yes()) { - $originalNameType = $scope->getType($node->name); - $className = $scope->getType($node->var)->describe(VerbosityLevel::typeOnly()); - $errors[] = RuleErrorBuilder::message(sprintf('Property name for %s must be a string, but %s was given.', $className, $originalNameType->describe(VerbosityLevel::precise()))) - ->identifier('property.nameNotString') - ->build(); - } + $errors = array_merge($errors, $this->checkNonStringableDynamicAccess($scope, $node->var, $node->name)); } } @@ -82,6 +71,44 @@ public function check(PropertyFetch $node, Scope $scope, bool $write): array return $errors; } + /** + * @return list + */ + private function checkNonStringableDynamicAccess(Scope $scope, Expr $nodeVar, Expr $nodeName): array + { + if ( + $this->checkThisOnly + && !$this->ruleLevelHelper->isThis($nodeVar) + ) { + return []; + } + + $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $nodeName, + '', + static fn (Type $type) => $type->toString()->isString()->yes(), + ); + $nameType = $nameTypeResult->getType(); + if ( + !$nameType instanceof ErrorType + && !$nameType->toString() instanceof ErrorType + && $nameType->toString()->isString()->yes() + ) { + return []; + } + + $originalNameType = $scope->getType($nodeName); + $className = $scope->getType($nodeVar)->describe(VerbosityLevel::typeOnly()); + return [ + RuleErrorBuilder::message(sprintf( + 'Property name for %s must be a string, but %s was given.', + $className, + $originalNameType->describe(VerbosityLevel::precise()), + ))->identifier('property.nameNotString')->build(), + ]; + } + /** * @return list */ diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 73ea69e5fd..23d42e7986 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -19,7 +19,15 @@ protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesCheck($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, true, true), + new AccessPropertiesCheck( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new PhpVersion(PHP_VERSION_ID), + true, + true, + true, + false, + ), ); } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 39c4948c24..29a7489aa7 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -26,7 +26,15 @@ class AccessPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); - return new AccessPropertiesRule(new AccessPropertiesCheck($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false, false, false, true), new PhpVersion(PHP_VERSION_ID), true, $this->checkDynamicProperties, true)); + return new AccessPropertiesRule(new AccessPropertiesCheck( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false, false, false, true), + new PhpVersion(PHP_VERSION_ID), + true, + $this->checkDynamicProperties, + true, + $this->checkThisOnly, + )); } public function testAccessProperties(): void @@ -1092,6 +1100,14 @@ public function testNewIsAlwaysFinalClass(): void ]); } + public function testBug13271(): void + { + $this->checkThisOnly = true; + $this->checkUnionTypes = false; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-13271.php'], []); + } + public function testPropertyExists(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13271.php b/tests/PHPStan/Rules/Properties/data/bug-13271.php new file mode 100644 index 0000000000..621fc68f6e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13271.php @@ -0,0 +1,13 @@ + 0.5 ? 'example_one' : 'example_two'; +$result = $object->$field;