From 5e900f86399469b71cc1efd8e88910cf558657f7 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 18 Mar 2025 21:21:21 +0900 Subject: [PATCH 1/4] Fix for handling non-stringable types in dynamic variable access --- src/Rules/Variables/DefinedVariableRule.php | 41 +++++++++++++++---- .../Variables/DefinedVariableRuleTest.php | 26 ++++++++++++ .../PHPStan/Rules/Variables/data/bug-9475.php | 21 ++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-9475.php diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index fc665ed901..bab77a9f31 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -5,8 +5,13 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\VerbosityLevel; +use function array_map; +use function array_merge; use function in_array; use function is_string; use function sprintf; @@ -31,11 +36,33 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!is_string($node->name)) { - return []; + $errors = []; + if (is_string($node->name)) { + $variableNames = [$node->name]; + } else { + $fetchType = $scope->getType($node->name); + $variableNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $fetchType->getConstantStrings()); + $fetchStringType = $fetchType->toString(); + if (! $fetchStringType->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot access variable with a non-stringable type %s.', $fetchType->describe(VerbosityLevel::typeOnly()))) + ->identifier('variable.fetchInvalidExpression') + ->build(); + } + } + + foreach ($variableNames as $name) { + $errors = array_merge($errors, $this->processSingleVariable($scope, $node, $name)); } - if ($this->cliArgumentsVariablesRegistered && in_array($node->name, [ + return $errors; + } + + /** + * @return list + */ + private function processSingleVariable(Scope $scope, Variable $node, string $variableName): array + { + if ($this->cliArgumentsVariablesRegistered && in_array($variableName, [ 'argc', 'argv', ], true)) { @@ -49,18 +76,18 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->hasVariableType($node->name)->no()) { + if ($scope->hasVariableType($variableName)->no()) { return [ - RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $node->name)) + RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $variableName)) ->identifier('variable.undefined') ->build(), ]; } elseif ( $this->checkMaybeUndefinedVariables - && !$scope->hasVariableType($node->name)->yes() + && !$scope->hasVariableType($variableName)->yes() ) { return [ - RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $node->name)) + RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $variableName)) ->identifier('variable.undefined') ->build(), ]; diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 17aa83b245..defcedd60e 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -971,6 +971,32 @@ public function testBug9474(): void $this->analyse([__DIR__ . '/data/bug-9474.php'], []); } + public function testBug9475(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-9475.php'], [ + [ + 'Cannot access variable with a non-stringable type $this(Bug9475\Variables).', + 12, + ], + [ + 'Cannot access variable with a non-stringable type $this(Bug9475\Variables).', + 13, + ], + [ + 'Cannot access variable with a non-stringable type object.', + 14, + ], + [ + 'Cannot access variable with a non-stringable type array.', + 15, + ], + ]); + } + public function testEnum(): void { if (PHP_VERSION_ID < 80100) { diff --git a/tests/PHPStan/Rules/Variables/data/bug-9475.php b/tests/PHPStan/Rules/Variables/data/bug-9475.php new file mode 100644 index 0000000000..94ddc3cf7a --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9475.php @@ -0,0 +1,21 @@ + Date: Tue, 18 Mar 2025 21:22:17 +0900 Subject: [PATCH 2/4] Fix for handling non-stringable types in dynamic property access --- .../Properties/AccessPropertiesCheck.php | 11 +++++-- .../Properties/AccessStaticPropertiesRule.php | 11 +++++-- .../Properties/AccessPropertiesRuleTest.php | 25 +++++++++++++++ .../AccessStaticPropertiesRuleTest.php | 22 +++++++++++++ .../Rules/Properties/data/bug-9475.php | 32 +++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-9475.php diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index f1a70365a7..7375925055 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -41,13 +41,20 @@ public function __construct( */ public function check(PropertyFetch $node, Scope $scope, bool $write): array { + $errors = []; if ($node->name instanceof Identifier) { $names = [$node->name->name]; } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); + $fetchType = $scope->getType($node->name); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $fetchType->getConstantStrings()); + $fetchStringType = $fetchType->toString(); + if (!$fetchStringType->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot access property with a non-stringable type %s.', $fetchType->describe(VerbosityLevel::typeOnly()))) + ->identifier('property.fetchInvalidExpression') + ->build(); + } } - $errors = []; foreach ($names as $name) { $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name, $write)); } diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 94e526da0c..bb7f706be4 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -51,13 +51,20 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + $errors = []; if ($node->name instanceof Node\VarLikeIdentifier) { $names = [$node->name->name]; } else { - $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); + $fetchType = $scope->getType($node->name); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $fetchType->getConstantStrings()); + $fetchStringType = $fetchType->toString(); + if (!$fetchStringType->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot access static property with a non-stringable type %s.', $fetchType->describe(VerbosityLevel::typeOnly()))) + ->identifier('property.fetchInvalidExpression') + ->build(); + } } - $errors = []; foreach ($names as $name) { $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name)); } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index e6aa299b75..20b4b35fdc 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -344,6 +344,31 @@ public function testAccessPropertiesOnThisOnly(): void ); } + public function testBug9475(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = false; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-9475.php'], [ + [ + 'Cannot access property with a non-stringable type $this(Bug9475\Properties).', + 12, + ], + [ + 'Cannot access property with a non-stringable type $this(Bug9475\Properties).', + 13, + ], + [ + 'Cannot access property with a non-stringable type object.', + 14, + ], + [ + 'Cannot access property with a non-stringable type array.', + 15, + ], + ]); + } + public function testBug12692(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 7060aeecea..57c9f9a1e4 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -309,4 +309,26 @@ public function testBug8333(): void ]); } + public function testBug9475(): void + { + $this->analyse([__DIR__ . '/data/bug-9475.php'], [ + [ + 'Cannot access static property with a non-stringable type $this(Bug9475\Properties).', + 23, + ], + [ + 'Cannot access static property with a non-stringable type $this(Bug9475\Properties).', + 24, + ], + [ + 'Cannot access static property with a non-stringable type object.', + 25, + ], + [ + 'Cannot access static property with a non-stringable type array.', + 26, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-9475.php b/tests/PHPStan/Rules/Properties/data/bug-9475.php new file mode 100644 index 0000000000..2c9ede0438 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-9475.php @@ -0,0 +1,32 @@ +{$this}->name; + echo 'Hello, ' . $this->$this->name; + echo 'Hello, ' . $this->$object; + echo 'Hello, ' . $this->$array; + + echo 'Hello, ' . $this->$name; // valid + echo 'Hello, ' . $this->$stringable; // valid + } + + public function testStaticProperties(string $name, Stringable $stringable, object $object, array $array): void + { + echo 'Hello, ' . self::${$this}->name; + echo 'Hello, ' . self::$$this->name; + echo 'Hello, ' . self::$$object; + echo 'Hello, ' . self::$$array; + + echo 'Hello, ' . self::$$name; // valid + echo 'Hello, ' . self::$$stringable; // valid + } + +} From 5e4d26a8b5b2fa62e671cb1ff4158951986c050f Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 18 Mar 2025 21:27:32 +0900 Subject: [PATCH 3/4] Fix for handling non-stringable types in dynamic class constant access --- src/Rules/Classes/ClassConstantRule.php | 29 +++++++++++++++++-- .../Rules/Classes/ClassConstantRuleTest.php | 24 +++++++++++++++ tests/PHPStan/Rules/Classes/data/bug-9475.php | 20 +++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-9475.php diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 968b3d75f3..6cb3d04d58 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -11,6 +11,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -20,6 +21,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function array_map; use function array_merge; use function in_array; use function sprintf; @@ -47,11 +49,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $constantNames = [$node->name->name]; + } else { + $fetchType = $scope->getType($node->name); + $constantNames = array_map(static fn ($type): string => $type->getValue(), $fetchType->getConstantStrings()); + $fetchStringType = $fetchType->toString(); + if (!$fetchStringType->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch class constant with a non-stringable type %s.', $fetchType->describe(VerbosityLevel::typeOnly()))) + ->identifier('classConstant.fetchInvalidExpression') + ->build(); + } + } + + foreach ($constantNames as $constantName) { + $errors = array_merge($errors, $this->processSingleClassConstFetch($scope, $node, $constantName)); } - $constantName = $node->name->name; + return $errors; + } + + /** + * @return list + */ + private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $node, string $constantName): array + { $class = $node->class; $messages = []; if ($class instanceof Node\Name) { diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 32086476be..8bfcb9a07e 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -435,4 +435,28 @@ public function testClassConstantAccessedOnTrait(): void ]); } + public function testBug9475(): void + { + if (PHP_VERSION_ID < 80200) { + $this->markTestSkipped('Test requires PHP 8.2.'); + } + + $this->phpVersion = PHP_VERSION_ID; + + $this->analyse([__DIR__ . '/data/bug-9475.php'], [ + [ + 'Cannot fetch class constant with a non-stringable type $this(Bug9475\Classes).', + 12, + ], + [ + 'Cannot fetch class constant with a non-stringable type object.', + 13, + ], + [ + 'Cannot fetch class constant with a non-stringable type array.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-9475.php b/tests/PHPStan/Rules/Classes/data/bug-9475.php new file mode 100644 index 0000000000..72a5ac1b88 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-9475.php @@ -0,0 +1,20 @@ +=8.3 + +namespace Bug9475; + +use Stringable; + +final class Classes +{ + + public function testStaticMethods(string $name, Stringable $stringable, object $object, array $array): void + { + echo 'Hello, ' . self::{$this}; + echo 'Hello, ' . self::{$object}; + echo 'Hello, ' . self::{$array}; + + echo 'Hello, ' . self::{$name}; // valid + echo 'Hello, ' . self::{$stringable}; // valid + } + +} From c3a51744b855ce14037c2f5d77f70c18f3ae1fb2 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 18 Mar 2025 21:27:58 +0900 Subject: [PATCH 4/4] Fix for handling non-stringable types in dynamic method call --- src/Rules/Methods/CallMethodsRule.php | 31 ++++++++++++++++-- src/Rules/Methods/CallStaticMethodsRule.php | 31 ++++++++++++++++-- .../Rules/Methods/CallMethodsRuleTest.php | 26 +++++++++++++++ .../Methods/CallStaticMethodsRuleTest.php | 25 +++++++++++++++ tests/PHPStan/Rules/Methods/data/bug-9475.php | 32 +++++++++++++++++++ 5 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9475.php diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 19bc52fe80..49b3e56091 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -8,8 +8,13 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\VerbosityLevel; +use function array_map; use function array_merge; +use function sprintf; /** * @implements Rule @@ -31,12 +36,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $methodNames = [$node->name->name]; + } else { + $callType = $scope->getType($node->name); + $methodNames = array_map(static fn ($type): string => $type->getValue(), $callType->getConstantStrings()); + $callStringType = $callType->toString(); + if (!$callStringType->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot call method name with a non-stringable type %s.', $callType->describe(VerbosityLevel::typeOnly()))) + ->identifier('method.callNameInvalidExpression') + ->build(); + } } - $methodName = $node->name->name; + foreach ($methodNames as $methodName) { + $errors = array_merge($errors, $this->processSingleMethodCall($scope, $node, $methodName)); + } + + return $errors; + } + /** + * @return list + */ + private function processSingleMethodCall(Scope $scope, MethodCall $node, string $methodName): array + { [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var); if ($methodReflection === null) { return $errors; diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index e6b2c9dbb5..af7ccb3431 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -8,7 +8,11 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\VerbosityLevel; +use function array_map; use function array_merge; use function sprintf; @@ -32,11 +36,32 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->name instanceof Node\Identifier) { - return []; + $errors = []; + if ($node->name instanceof Node\Identifier) { + $methodNames = [$node->name->name]; + } else { + $callType = $scope->getType($node->name); + $methodNames = array_map(static fn ($type): string => $type->getValue(), $callType->getConstantStrings()); + $callStringType = $callType->toString(); + if (!$callStringType->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot call static method name with a non-stringable type %s.', $callType->describe(VerbosityLevel::typeOnly()))) + ->identifier('staticMethod.callNameInvalidExpression') + ->build(); + } } - $methodName = $node->name->name; + foreach ($methodNames as $methodName) { + $errors = array_merge($errors, $this->processSingleMethodCall($scope, $node, $methodName)); + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleMethodCall(Scope $scope, StaticCall $node, string $methodName): array + { [$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class); if ($method === null) { return $errors; diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index acbbd53dd3..394dd42d2a 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3552,4 +3552,30 @@ public function testBug6828(): void $this->analyse([__DIR__ . '/data/bug-6828.php'], []); } + public function testBug9475(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9475.php'], [ + [ + 'Cannot call method name with a non-stringable type $this(Bug9475\Methods).', + 12, + ], + [ + 'Cannot call method name with a non-stringable type $this(Bug9475\Methods).', + 13, + ], + [ + 'Cannot call method name with a non-stringable type object.', + 14, + ], + [ + 'Cannot call method name with a non-stringable type array.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 51210125cf..3487e93925 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -862,4 +862,29 @@ public function testBug12015(): void $this->analyse([__DIR__ . '/data/bug-12015.php'], []); } + public function testBug9475(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9475.php'], [ + [ + 'Cannot call static method name with a non-stringable type $this(Bug9475\Methods).', + 23, + ], + [ + 'Cannot call static method name with a non-stringable type $this(Bug9475\Methods).', + 24, + ], + [ + 'Cannot call static method name with a non-stringable type object.', + 25, + ], + [ + 'Cannot call static method name with a non-stringable type array.', + 26, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9475.php b/tests/PHPStan/Rules/Methods/data/bug-9475.php new file mode 100644 index 0000000000..9795536d45 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9475.php @@ -0,0 +1,32 @@ +{$this}(); + echo 'Hello, ' . $this->$this(); + echo 'Hello, ' . $this->$object(); + echo 'Hello, ' . $this->$array(); + + echo 'Hello, ' . $this->$name(); // valid + echo 'Hello, ' . $this->$stringable(); // valid + } + + public function testStaticMethods(string $name, Stringable $stringable, object $object, array $array): void + { + echo 'Hello, ' . self::{$this}(); + echo 'Hello, ' . self::$this(); + echo 'Hello, ' . self::$object(); + echo 'Hello, ' . self::$array(); + + echo 'Hello, ' . self::$name(); // valid + echo 'Hello, ' . self::$stringable(); // valid + } + +}