diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 901da7a660..57e52fac59 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -252,4 +252,9 @@ public function supportsAbstractTraitMethods(): bool return $this->versionId >= 80000; } + public function supportsOverrideAttribute(): bool + { + return $this->versionId >= 80300; + } + } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index f92148c815..c23f95653d 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -79,6 +79,17 @@ public function processNode(Node $node, Scope $scope): array } } } + + if ($this->phpVersion->supportsOverrideAttribute() && $this->hasOverrideAttribute($node->getOriginalNode())) { + return [ + RuleErrorBuilder::message(sprintf( + 'Method %s::%s() has #[Override] attribute but does not override any method.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->nonIgnorable()->build(), + ]; + } + return []; } @@ -267,6 +278,19 @@ private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method) return false; } + private function hasOverrideAttribute(Node\Stmt\ClassMethod $method): bool + { + foreach ($method->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'override') { + return true; + } + } + } + + return false; + } + private function findPrototype(ClassReflection $classReflection, string $methodName): ?ExtendedMethodReflection { foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index eef272ca49..4c0ec4048d 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -724,4 +724,19 @@ public function testTraits(): void $this->analyse([__DIR__ . '/data/overriding-trait-methods.php'], $errors); } + public function testOverrideAttribute(): void + { + if (PHP_VERSION_ID < 80300) { + $this->markTestSkipped('Test requires PHP 8.3.'); + } + + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/override-attribute.php'], [ + [ + 'Method OverrideAttribute\Bar::test2() has #[Override] attribute but does not override any method.', + 24, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/override-attribute.php b/tests/PHPStan/Rules/Methods/data/override-attribute.php new file mode 100644 index 0000000000..0b0600be2c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/override-attribute.php @@ -0,0 +1,30 @@ +