Skip to content

Commit 5abc4b4

Browse files
committed
Report @inheritDoc when there is no PHPDoc to inherit from
1 parent 697e042 commit 5abc4b4

7 files changed

Lines changed: 530 additions & 0 deletions

File tree

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ parameters:
1616
reportNestedTooWideType: false # tmp
1717
assignToByRefForeachExpr: true
1818
curlSetOptArrayTypes: true
19+
reportInvalidInheritDocTag: true

conf/config.level2.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ conditionalTags:
1515
phpstan.restrictedPropertyUsageExtension: %featureToggles.internalTag%
1616
PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension:
1717
phpstan.restrictedMethodUsageExtension: %featureToggles.internalTag%
18+
PHPStan\Rules\PhpDoc\InvalidInheritDocTagRule:
19+
phpstan.rules.rule: %featureToggles.reportInvalidInheritDocTag%
1820

1921
services:
2022
-
2123
class: PHPStan\Rules\InternalTag\RestrictedInternalPropertyUsageExtension
2224

2325
-
2426
class: PHPStan\Rules\InternalTag\RestrictedInternalMethodUsageExtension
27+
28+
-
29+
class: PHPStan\Rules\PhpDoc\InvalidInheritDocTagRule

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ parameters:
4343
reportNestedTooWideType: false
4444
assignToByRefForeachExpr: false
4545
curlSetOptArrayTypes: false
46+
reportInvalidInheritDocTag: false
4647
fileExtensions:
4748
- php
4849
checkAdvancedIsset: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ parametersSchema:
4545
reportNestedTooWideType: bool()
4646
assignToByRefForeachExpr: bool()
4747
curlSetOptArrayTypes: bool()
48+
reportInvalidInheritDocTag: bool()
4849
])
4950
fileExtensions: listOf(string())
5051
checkAdvancedIsset: bool()
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClassMethodNode;
8+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
9+
use PHPStan\PhpDocParser\Lexer\Lexer;
10+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
11+
use PHPStan\PhpDocParser\Parser\TokenIterator;
12+
use PHPStan\Reflection\ClassReflection;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use function preg_match;
16+
use function sprintf;
17+
use function strtolower;
18+
19+
/**
20+
* @implements Rule<InClassMethodNode>
21+
*/
22+
final class InvalidInheritDocTagRule implements Rule
23+
{
24+
25+
private const INLINE_INHERIT_DOC_REGEX = '~(?<![a-zA-Z0-9])\{@inheritDoc\b[^}]*\}~i';
26+
27+
public function __construct(
28+
private Lexer $phpDocLexer,
29+
private PhpDocParser $phpDocParser,
30+
)
31+
{
32+
}
33+
34+
public function getNodeType(): string
35+
{
36+
return InClassMethodNode::class;
37+
}
38+
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
$docComment = $node->getOriginalNode()->getDocComment();
42+
if ($docComment === null) {
43+
return [];
44+
}
45+
46+
$tokens = new TokenIterator($this->phpDocLexer->tokenize($docComment->getText()));
47+
$phpDocNode = $this->phpDocParser->parse($tokens);
48+
49+
$inheritDocTagName = null;
50+
foreach ($phpDocNode->getTags() as $tag) {
51+
if (strtolower($tag->name) !== '@inheritdoc') {
52+
continue;
53+
}
54+
55+
$inheritDocTagName = $tag->name;
56+
break;
57+
}
58+
59+
if ($inheritDocTagName === null) {
60+
foreach ($phpDocNode->children as $child) {
61+
if (!$child instanceof PhpDocTextNode) {
62+
continue;
63+
}
64+
65+
if (preg_match(self::INLINE_INHERIT_DOC_REGEX, $child->text, $matches) !== 1) {
66+
continue;
67+
}
68+
69+
$inheritDocTagName = $matches[0];
70+
break;
71+
}
72+
}
73+
74+
if ($inheritDocTagName === null) {
75+
return [];
76+
}
77+
78+
$inheritanceClass = $scope->isInTrait() ? $scope->getTraitReflection() : $node->getClassReflection();
79+
$methodName = $node->getMethodReflection()->getName();
80+
81+
if ($this->hasInheritablePhpDoc($inheritanceClass, $methodName)) {
82+
return [];
83+
}
84+
85+
return [
86+
RuleErrorBuilder::message(sprintf(
87+
'PHPDoc tag %s on method %s::%s() refers to a parent method that does not have a PHPDoc.',
88+
$inheritDocTagName,
89+
$inheritanceClass->getDisplayName(),
90+
$methodName,
91+
))->identifier('phpDoc.invalidInheritDoc')->build(),
92+
];
93+
}
94+
95+
private function hasInheritablePhpDoc(ClassReflection $classReflection, string $methodName): bool
96+
{
97+
$parent = $classReflection->getParentClass();
98+
if ($parent !== null && $this->parentHasPhpDocForMethod($parent, $methodName)) {
99+
return true;
100+
}
101+
102+
foreach ($classReflection->getImmediateInterfaces() as $interface) {
103+
if ($this->parentHasPhpDocForMethod($interface, $methodName)) {
104+
return true;
105+
}
106+
}
107+
108+
foreach ($classReflection->getTraits() as $trait) {
109+
if ($this->parentHasPhpDocForMethod($trait, $methodName)) {
110+
return true;
111+
}
112+
}
113+
114+
return false;
115+
}
116+
117+
private function parentHasPhpDocForMethod(ClassReflection $parent, string $methodName): bool
118+
{
119+
if (!$parent->hasNativeMethod($methodName)) {
120+
return false;
121+
}
122+
123+
$parentMethod = $parent->getNativeMethod($methodName);
124+
if ($parentMethod->isPrivate()) {
125+
return false;
126+
}
127+
128+
return $parentMethod->getResolvedPhpDoc() !== null;
129+
}
130+
131+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PHPStan\PhpDocParser\Lexer\Lexer;
6+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
10+
/**
11+
* @extends RuleTestCase<InvalidInheritDocTagRule>
12+
*/
13+
class InvalidInheritDocTagRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new InvalidInheritDocTagRule(
19+
self::getContainer()->getByType(Lexer::class),
20+
self::getContainer()->getByType(PhpDocParser::class),
21+
);
22+
}
23+
24+
public function testRule(): void
25+
{
26+
$this->analyse([__DIR__ . '/data/invalid-inherit-doc-tag.php'], [
27+
[
28+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ChildWithInlineInheritDoc::methodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.',
29+
31,
30+
],
31+
[
32+
'PHPDoc tag @inheritdoc on method InvalidInheritDocTag\ChildWithBlockInheritDoc::methodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.',
33+
52,
34+
],
35+
[
36+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ClassWithoutParent::orphanedInheritDoc() refers to a parent method that does not have a PHPDoc.',
37+
73,
38+
],
39+
[
40+
'PHPDoc tag @inheritdoc on method InvalidInheritDocTag\ClassWithoutParent::orphanedBlockInheritDoc() refers to a parent method that does not have a PHPDoc.',
41+
81,
42+
],
43+
[
44+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ImplementsInterface::interfaceMethodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.',
45+
106,
46+
],
47+
[
48+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\UsesTraitWithoutPhpDoc::traitMethodWithoutPhpDoc() refers to a parent method that does not have a PHPDoc.',
49+
216,
50+
],
51+
[
52+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\IssueExampleChild::f() refers to a parent method that does not have a PHPDoc.',
53+
254,
54+
],
55+
[
56+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\ChildOfPrivateParentMethod::privateMethod() refers to a parent method that does not have a PHPDoc.',
57+
280,
58+
],
59+
[
60+
'PHPDoc tag {@inheritdoc} on method InvalidInheritDocTag\OrphanedInheritDocTrait::orphaned() refers to a parent method that does not have a PHPDoc.',
61+
293,
62+
],
63+
]);
64+
}
65+
66+
}

0 commit comments

Comments
 (0)