diff --git a/src/Rules/Methods/MissingMethodImplementationRule.php b/src/Rules/Methods/MissingMethodImplementationRule.php index 3f4d7dd136..534a597106 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -7,6 +7,7 @@ use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; +use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function sprintf; @@ -47,6 +48,17 @@ public function processNode(Node $node, Scope $scope): array $declaringClass = $method->getDeclaringClass(); + if ( + $declaringClass->isInterface() + && $this->isProvidedByBuiltinAncestor($classReflection, $declaringClass->getName()) + ) { + // A non-abstract built-in class implementing the interface always + // provides its methods at runtime, even when a stub reports one as + // abstract because it is version-gated (e.g. IntlBreakIterator + + // IteratorAggregate::getIterator(), SimpleXMLElement + RecursiveIterator). + continue; + } + $classLikeDescription = 'Non-abstract class'; if ($classReflection->isEnum()) { $classLikeDescription = 'Enum'; @@ -65,4 +77,21 @@ public function processNode(Node $node, Scope $scope): array return $messages; } + private function isProvidedByBuiltinAncestor(ClassReflection $classReflection, string $interfaceName): bool + { + foreach ($classReflection->getParents() as $parent) { + if (!$parent->isBuiltin()) { + continue; + } + if ($parent->isAbstract()) { + continue; + } + if ($parent->implementsInterface($interfaceName)) { + return true; + } + } + + return false; + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRulePhp74Test.php b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRulePhp74Test.php new file mode 100644 index 0000000000..98b87dd5e9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRulePhp74Test.php @@ -0,0 +1,31 @@ + + */ +class MissingMethodImplementationRulePhp74Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingMethodImplementationRule(); + } + + public function testBug14964(): void + { + $this->analyse([__DIR__ . '/data/bug-14964.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/missing-method-impl-php74.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-14964.php b/tests/PHPStan/Rules/Methods/data/bug-14964.php new file mode 100644 index 0000000000..97e4754ebc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14964.php @@ -0,0 +1,11 @@ +