diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 31d4874741..b44a73faa7 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -28,6 +28,7 @@ rules: - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule + - PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule - PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php new file mode 100644 index 0000000000..b53b00cc37 --- /dev/null +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -0,0 +1,85 @@ + + */ +class InvalidPHPStanDocTagRule implements \PHPStan\Rules\Rule +{ + + private const POSSIBLE_PHPSTAN_TAGS = [ + '@phpstan-param', + '@phpstan-var', + '@phpstan-template', + '@phpstan-extends', + '@phpstan-implements', + '@phpstan-use', + '@phpstan-template', + '@phpstan-template-covariant', + '@phpstan-return', + ]; + + /** @var Lexer */ + private $phpDocLexer; + + /** @var PhpDocParser */ + private $phpDocParser; + + public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + { + $this->phpDocLexer = $phpDocLexer; + $this->phpDocParser = $phpDocParser; + } + + public function getNodeType(): string + { + return \PhpParser\Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ( + !$node instanceof Node\Stmt\ClassLike + && !$node instanceof Node\FunctionLike + && !$node instanceof Node\Stmt\Foreach_ + && !$node instanceof Node\Stmt\Property + && !$node instanceof Node\Expr\Assign + && !$node instanceof Node\Expr\AssignRef + ) { + return []; + } + + $docComment = $node->getDocComment(); + if ($docComment === null) { + return []; + } + $phpDocString = $docComment->getText(); + $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString)); + $phpDocNode = $this->phpDocParser->parse($tokens); + + $errors = []; + foreach ($phpDocNode->getTags() as $phpDocTag) { + if (strpos($phpDocTag->name, '@phpstan-') !== 0 + || in_array($phpDocTag->name, self::POSSIBLE_PHPSTAN_TAGS, true) + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Encountered unknown tag that had the phpstan prefix: %s', + $phpDocTag->name + ))->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php new file mode 100644 index 0000000000..57cdbb0903 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -0,0 +1,36 @@ + + */ +class InvalidPHPStanDocTagRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new InvalidPHPStanDocTagRule( + self::getContainer()->getByType(Lexer::class), + self::getContainer()->getByType(PhpDocParser::class) + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-phpstan-doc.php'], [ + [ + 'Encountered unknown tag that had the phpstan prefix: @phpstan-extens', + 7, + ], + [ + 'Encountered unknown tag that had the phpstan prefix: @phpstan-pararm', + 14, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php new file mode 100644 index 0000000000..4b1efb0805 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php @@ -0,0 +1,15 @@ + $a + * @phpstan-return T + */ + function foo(string $a){} +}