diff --git a/README.md b/README.md index 79ed1ac2..eedfde13 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ * Functions `in_array` and `array_search` must be called with third parameter `$strict` set to `true` to search values with matching types only. * Variables assigned in `while` loop condition and `for` loop initial assignment cannot be used after the loop. * Types in `switch` condition and `case` value must match. PHP compares them loosely by default and that can lead to unexpected results. +* Statically declared methods are called statically. Additional rules are coming in subsequent releases! diff --git a/rules.neon b/rules.neon index 503cc51b..6a7d6d36 100644 --- a/rules.neon +++ b/rules.neon @@ -32,6 +32,11 @@ services: tags: - phpstan.rules.rule + - + class: PHPStan\Rules\StrictCalls\DynamicCallOnStaticMethodsRule + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\StrictCalls\StrictFunctionCallsRule tags: diff --git a/src/Rules/StrictCalls/DynamicCallOnStaticMethodsRule.php b/src/Rules/StrictCalls/DynamicCallOnStaticMethodsRule.php new file mode 100644 index 00000000..47664bdd --- /dev/null +++ b/src/Rules/StrictCalls/DynamicCallOnStaticMethodsRule.php @@ -0,0 +1,67 @@ +ruleLevelHelper = $ruleLevelHelper; + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param \PhpParser\Node\Expr\MethodCall $node + * @param \PHPStan\Analyser\Scope $scope + * @return string[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (!is_string($node->name)) { + return []; + } + + $name = $node->name; + $type = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->var, + '' + )->getType(); + + if ($type instanceof ErrorType || !$type->canCallMethods() || !$type->hasMethod($name)) { + return []; + } + + $methodReflection = $type->getMethod($name, $scope); + if (!$scope->canCallMethod($methodReflection)) { + return []; + } + + if ($methodReflection->isStatic()) { + return [sprintf( + 'Dynamic call to static method %s::%s().', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName() + )]; + } + + return []; + } + +} diff --git a/tests/Rules/StrictCalls/DynamicCallOnStaticMethodsRuleTest.php b/tests/Rules/StrictCalls/DynamicCallOnStaticMethodsRuleTest.php new file mode 100644 index 00000000..4fb481da --- /dev/null +++ b/tests/Rules/StrictCalls/DynamicCallOnStaticMethodsRuleTest.php @@ -0,0 +1,61 @@ +createBroker(); + $ruleLevelHelper = new RuleLevelHelper($broker, true, $this->checkThisOnly, true); + return new DynamicCallOnStaticMethodsRule($ruleLevelHelper); + } + + public function testRule() + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/dynamic-calls-on-static-methods.php'], [ + [ + 'Dynamic call to static method StrictCalls\ClassWithStaticMethod::foo().', + 14, + ], + [ + 'Dynamic call to static method StrictCalls\ClassWithStaticMethod::foo().', + 20, + ], + [ + 'Dynamic call to static method StrictCalls\ClassUsingTrait::foo().', + 32, + ], + [ + 'Dynamic call to static method StrictCalls\ClassUsingTrait::foo().', + 43, + ], + ]); + } + + public function testRuleOnThisOnly() + { + $this->checkThisOnly = true; + $this->analyse([__DIR__ . '/data/dynamic-calls-on-static-methods.php'], [ + [ + 'Dynamic call to static method StrictCalls\ClassWithStaticMethod::foo().', + 14, + ], + [ + 'Dynamic call to static method StrictCalls\ClassUsingTrait::foo().', + 32, + ], + ]); + } + +} diff --git a/tests/Rules/StrictCalls/data/dynamic-calls-on-static-methods.php b/tests/Rules/StrictCalls/data/dynamic-calls-on-static-methods.php new file mode 100644 index 00000000..62e12618 --- /dev/null +++ b/tests/Rules/StrictCalls/data/dynamic-calls-on-static-methods.php @@ -0,0 +1,44 @@ +foo(); + } +} + +function () { + $classWithStaticMethod = new ClassWithStaticMethod(); + $classWithStaticMethod->foo(); +}; + +trait TraitWithStaticMethod +{ + public static function foo() + { + + } + + public function bar() + { + $this->foo(); + } +} + +class ClassUsingTrait +{ + use TraitWithStaticMethod; +} + +function () { + $classUsingTrait = new ClassUsingTrait(); + $classUsingTrait->foo(); +};