diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index a68d1a352a2..c5e1a878919 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -16,3 +16,4 @@ parameters: reportNestedTooWideType: false # tmp assignToByRefForeachExpr: true curlSetOptArrayTypes: true + reportInvalidInheritDocTag: true diff --git a/conf/config.level2.neon b/conf/config.level2.neon index dd50138b541..ca9f9780566 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -15,6 +15,8 @@ conditionalTags: phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag% PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension: phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag% + PHPStan\Rules\PhpDoc\InvalidInheritDocTagRule: + phpstan.rules.rule: %featureToggles.reportInvalidInheritDocTag% services: - @@ -22,3 +24,6 @@ services: - class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension + + - + class: PHPStan\Rules\PhpDoc\InvalidInheritDocTagRule diff --git a/conf/config.neon b/conf/config.neon index 6c71dae9225..98eb0f4e569 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -43,6 +43,7 @@ parameters: reportNestedTooWideType: false assignToByRefForeachExpr: false curlSetOptArrayTypes: false + reportInvalidInheritDocTag: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index bc79fe7c401..ff2fa61e760 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -45,6 +45,7 @@ parametersSchema: reportNestedTooWideType: bool() assignToByRefForeachExpr: bool() curlSetOptArrayTypes: bool() + reportInvalidInheritDocTag: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/PhpDoc/InvalidInheritDocTagRule.php b/src/Rules/PhpDoc/InvalidInheritDocTagRule.php new file mode 100644 index 00000000000..45052bc1096 --- /dev/null +++ b/src/Rules/PhpDoc/InvalidInheritDocTagRule.php @@ -0,0 +1,111 @@ + + */ +final class InvalidInheritDocTagRule implements Rule +{ + + private const INLINE_INHERIT_DOC_REGEX = '~(?getOriginalNode()->getDocComment(); + if ($docComment === null) { + return []; + } + + $tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment->getText())); + $phpDocNode = $this->phpDocParser->parse($tokens); + + $inheritDocTagName = null; + foreach ($phpDocNode->getTags() as $tag) { + if (strtolower($tag->name) !== '@inheritdoc') { + continue; + } + + $inheritDocTagName = $tag->name; + break; + } + + if ($inheritDocTagName === null) { + foreach ($phpDocNode->children as $child) { + if (!$child instanceof PhpDocTextNode) { + continue; + } + + if (preg_match(self::INLINE_INHERIT_DOC_REGEX, $child->text, $matches) !== 1) { + continue; + } + + $inheritDocTagName = $matches[0]; + break; + } + } + + if ($inheritDocTagName === null) { + return []; + } + + $inheritanceClass = $scope->isInTrait() ? $scope->getTraitReflection() : $node->getClassReflection(); + $methodName = $node->getMethodReflection()->getName(); + + $parentMethods = $this->parentMethodHelper->collectParentMethods($methodName, $inheritanceClass); + + if ($parentMethods === []) { + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s on method %s::%s() does not override or implement any other method.', + $inheritDocTagName, + $inheritanceClass->getDisplayName(), + $methodName, + ))->identifier('inheritDoc.noParent')->build(), + ]; + } + + foreach ($parentMethods as [$parentMethod]) { + if ($parentMethod->getResolvedPhpDoc() !== null) { + return []; + } + } + + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s on method %s::%s() refers to a parent method that does not have a PHPDoc.', + $inheritDocTagName, + $inheritanceClass->getDisplayName(), + $methodName, + ))->identifier('inheritDoc.parentWithoutPhpDoc')->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidInheritDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidInheritDocTagRuleTest.php new file mode 100644 index 00000000000..522a43c4b57 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/InvalidInheritDocTagRuleTest.php @@ -0,0 +1,72 @@ + + */ +class InvalidInheritDocTagRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidInheritDocTagRule( + self::getContainer()->getByType(Lexer::class), + self::getContainer()->getByType(PhpDocParser::class), + self::getContainer()->getByType(ParentMethodHelper::class), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-inherit-doc-tag.php'], [ + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ChildWithInlineInheritDoc::methodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.', + 31, + ], + [ + 'PHPDoc tag @inheritdoc on method InvalidInheritDocTag\ChildWithBlockInheritDoc::methodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.', + 52, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ClassWithoutParent::orphanedInheritDoc() does not override or implement any other method.', + 73, + ], + [ + 'PHPDoc tag @inheritdoc on method InvalidInheritDocTag\ClassWithoutParent::orphanedBlockInheritDoc() does not override or implement any other method.', + 81, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ImplementsInterface::interfaceMethodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.', + 106, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\UsesTraitWithoutPhpDoc::traitMethodWithoutPhpDoc() does not override or implement any other method.', + 216, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\UsesTraitWithPhpDoc::traitMethodWithPhpDoc() does not override or implement any other method.', + 231, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\IssueExampleChild::f() refers to a parent method that does not have a PHPDoc.', + 254, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ChildOfPrivateParentMethod::privateMethod() does not override or implement any other method.', + 280, + ], + [ + 'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\OrphanedInheritDocTrait::orphaned() does not override or implement any other method.', + 293, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-inherit-doc-tag.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-inherit-doc-tag.php new file mode 100644 index 00000000000..8c5c651921a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-inherit-doc-tag.php @@ -0,0 +1,325 @@ +