diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 968b3d75f3..23c3aafdaa 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; @@ -11,6 +13,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; @@ -47,11 +50,30 @@ 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) { + $constantNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $constantNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } + } + + foreach ($constantNameScopes as $constantName => $constantScope) { + $errors = array_merge($errors, $this->processSingleClassConstFetch($constantScope, $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/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 19bc52fe80..68957aa2e5 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -3,11 +3,14 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use function array_merge; @@ -31,12 +34,30 @@ 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) { + $methodNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $methodNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - $methodName = $node->name->name; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall($methodScope, $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..954c3a8669 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -3,11 +3,14 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use function array_merge; use function sprintf; @@ -32,11 +35,30 @@ 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) { + $methodNameScopes = [$node->name->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $methodNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } } - $methodName = $node->name->name; + foreach ($methodNameScopes as $methodName => $methodScope) { + $errors = array_merge($errors, $this->processSingleMethodCall($methodScope, $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/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index fc665ed901..2c7163434c 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -3,10 +3,14 @@ namespace PHPStan\Rules\Variables; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_merge; use function in_array; use function is_string; use function sprintf; @@ -31,11 +35,31 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!is_string($node->name)) { - return []; + $errors = []; + if (is_string($node->name)) { + $variableNameScopes = [$node->name => $scope]; + } else { + $nameType = $scope->getType($node->name); + $variableNameScopes = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $variableNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); + } + } + + foreach ($variableNameScopes as $name => $variableScope) { + $errors = array_merge($errors, $this->processSingleVariable($variableScope, $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 +73,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/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 32086476be..838201ffde 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -435,4 +435,48 @@ public function testClassConstantAccessedOnTrait(): void ]); } + public function testDynamicAccess(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->phpVersion = PHP_VERSION_ID; + + $this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [ + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 20, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.', + 20, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 37, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.', + 39, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::QUX.', + 41, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::QUX.', + 44, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.', + 44, + ], + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 44, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php new file mode 100644 index 0000000000..10809e566a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php @@ -0,0 +1,48 @@ += 8.3 + +namespace ClassConstantDynamicAccess; + +final class Foo +{ + + private const BAR = 'BAR'; + + /** @var 'FOO'|'BAR'|'BUZ' */ + public $name; + + public function test(string $string, object $obj): void + { + $bar = 'FOO'; + + echo self::{$foo}; + echo self::{$string}; + echo self::{$obj}; + echo self::{$this->name}; + } + + public function testScope(): void + { + $name1 = 'FOO'; + $rand = rand(); + if ($rand === 1) { + $foo = 1; + $name = $name1; + } elseif ($rand === 2) { + $name = 'BUZ'; + } else { + $name = 'QUX'; + } + + if ($name === 'FOO') { + echo self::{$name}; + } elseif ($name === 'BUZ') { + echo self::{$name}; + } else { + echo self::{$name}; + } + + echo self::{$name}; + } + + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index acbbd53dd3..e544eeeb5b 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3552,4 +3552,39 @@ public function testBug6828(): void $this->analyse([__DIR__ . '/data/bug-6828.php'], []); } + public function testDynamicCall(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/dynamic-call.php'], [ + [ + 'Call to an undefined method MethodsDynamicCall\Foo::bar().', + 23, + ], + [ + 'Call to an undefined method MethodsDynamicCall\Foo::doBar().', + 26, + ], + [ + 'Call to an undefined method MethodsDynamicCall\Foo::doBuz().', + 26, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.', + 53, + ], + [ + 'Parameter #1 $s of method MethodsDynamicCall\Foo::doQux() expects string, int given.', + 54, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.', + 55, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 51210125cf..2cfc7891b5 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -862,4 +862,38 @@ public function testBug12015(): void $this->analyse([__DIR__ . '/data/bug-12015.php'], []); } + public function testDynamicCall(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/dynamic-call.php'], [ + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::bar().', + 33, + ], + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::doBar().', + 36, + ], + [ + 'Call to an undefined static method MethodsDynamicCall\Foo::doBuz().', + 36, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.', + 58, + ], + [ + 'Parameter #1 $s of static method MethodsDynamicCall\Foo::doQux() expects string, int given.', + 59, + ], + [ + 'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.', + 60, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/dynamic-call.php b/tests/PHPStan/Rules/Methods/data/dynamic-call.php new file mode 100644 index 0000000000..3a917c0c81 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dynamic-call.php @@ -0,0 +1,62 @@ +$foo(); + echo $this->$string(); + echo $this->$obj(); + echo $this->{self::$name}(); + } + + public function testStaticCall(string $string, object $obj): void + { + $foo = 'bar'; + + echo self::$foo(); + echo self::$string(); + echo self::$obj(); + echo self::{self::$name}(); + } + + public function testScope(): void + { + $param1 = 1; + $param2 = 'str'; + $name1 = 'doFoo'; + if (rand(0, 1)) { + $name = $name1; + $param = $param1; + } else { + $name = 'doQux'; + $param = $param2; + } + + $this->$name($param); // ok + $this->$name1($param); + $this->$name($param1); + $this->$name($param2); + + self::$name($param); // ok + self::$name1($param); + self::$name($param1); + self::$name($param2); + } +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 17aa83b245..e6b29e2809 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1098,4 +1098,82 @@ public function testPropertyHooks(): void ]); } + public function testDynamicAccess(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/dynamic-access.php'], [ + [ + 'Undefined variable: $bar', + 15, + ], + [ + 'Undefined variable: $bar', + 18, + ], + [ + 'Undefined variable: $buz', + 18, + ], + [ + 'Variable $foo might not be defined.', + 36, + ], + [ + 'Variable $foo might not be defined.', + 37, + ], + [ + 'Variable $bar might not be defined.', + 38, + ], + [ + 'Variable $bar might not be defined.', + 40, + ], + [ + 'Variable $foo might not be defined.', + 41, + ], + [ + 'Variable $bar might not be defined.', + 42, + ], + [ + 'Undefined variable: $buz', + 44, + ], + [ + 'Undefined variable: $foo', + 45, + ], + [ + 'Undefined variable: $bar', + 46, + ], + [ + 'Undefined variable: $buz', + 49, + ], + [ + 'Variable $bar might not be defined.', + 49, + ], + [ + 'Variable $foo might not be defined.', + 49, + ], + [ + 'Variable $foo might not be defined.', + 50, + ], + [ + 'Variable $bar might not be defined.', + 51, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/dynamic-access.php b/tests/PHPStan/Rules/Variables/data/dynamic-access.php new file mode 100644 index 0000000000..a1109c5858 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/dynamic-access.php @@ -0,0 +1,54 @@ +name}; + } + + public function testScope(): void + { + $name1 = 'foo'; + $rand = rand(); + if ($rand === 1) { + $foo = 1; + $name = $name1; + } elseif ($rand === 2) { + $name = 'bar'; + $bar = 'str'; + } else { + $name = 'buz'; + } + + if ($name === 'foo') { + echo $$name; // ok + echo $foo; // ok + echo $bar; + } elseif ($name === 'bar') { + echo $$name; // ok + echo $foo; + echo $bar; // ok + } else { + echo $$name; // ok + echo $foo; + echo $bar; + } + + echo $$name; // ok + echo $foo; + echo $bar; + } + +}