diff --git a/src/PhpStan/MethodCallConsistencyRule.php b/src/PhpStan/MethodCallConsistencyRule.php index 438f390..33e964d 100644 --- a/src/PhpStan/MethodCallConsistencyRule.php +++ b/src/PhpStan/MethodCallConsistencyRule.php @@ -16,6 +16,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -39,6 +40,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if ($node instanceof Node\Expr\StaticCall) { + // parent::method() + if ($node->class instanceof Node\Name && 'parent' === $node->class->toString()) { + return $this->checkNativeMethodCall($node, $scope); + } + return $this->checkStaticCall($node, $scope); } @@ -117,12 +123,48 @@ private function checkInstanceCall(Node\Expr\MethodCall $node, Scope $scope): ar return []; } - private function resolveClassNameForStaticCall(Node\Expr\StaticCall $node, Scope $scope): ?string + /** + * Check parent method call + * + * @param Node\Expr\StaticCall $node + * @param Scope $scope + * + * @return list + * @throws ShouldNotHappenException|MissingMethodFromReflectionException + */ + private function checkNativeMethodCall(Node\Expr\StaticCall $node, Scope $scope): array { - if ($node->class instanceof Node\Name && 'parent' === $node->class->toString()) { - return null; + $methodName = $node->name instanceof Node\Identifier ? $node->name->toString() : null; + + if (null === $methodName) { + return []; } + $classReflection = $scope->getClassReflection(); + + if (null === $classReflection) { + return []; + } + + $method = $classReflection->getNativeMethod($methodName); + + if ($method->getDeclaringClass()->getName() !== $classReflection->getName()) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Class "%s" has no native method "%s" but called statically from parent.', + $classReflection->getName(), + $methodName + )) + ->identifier('methodCall.consistency') + ->build(), + ]; + } + + return []; + } + + private function resolveClassNameForStaticCall(Node\Expr\StaticCall $node, Scope $scope): ?string + { // self::method() if ($node->class instanceof Node\Name && 'self' === $node->class->toString()) { return $scope->getClassReflection()?->getName(); diff --git a/tests/PhpStan/MethodCallConsistencyRuleTest.php b/tests/PhpStan/MethodCallConsistencyRuleTest.php index c7df2c8..ec16880 100644 --- a/tests/PhpStan/MethodCallConsistencyRuleTest.php +++ b/tests/PhpStan/MethodCallConsistencyRuleTest.php @@ -43,6 +43,7 @@ public function shouldSuccessProcessForIsset(): void ['Method "FiveLab\Component\CiRules\Tests\PhpStan\Resources\MethodCallConsistency\ClassForProperty::instanceMethod1" is not static but called statically.', 36], ['Method "FiveLab\Component\CiRules\Tests\PhpStan\Resources\MethodCallConsistency\ClassForProperty->staticMethod1" is static but called dynamically.', 37], ['Method "FiveLab\Component\CiRules\Tests\PhpStan\Resources\MethodCallConsistency\ClassForProperty::instanceMethod1" is not static but called statically.', 40], + ['Class "FiveLab\Component\CiRules\Tests\PhpStan\Resources\MethodCallConsistency\ChildClass" has no native method "instanceMethod2" but called statically from parent.', 53], ], ); }