1: <?php
2:
3: declare(strict_types=1);
4:
5: /**
6: * This file is part of the Nexus framework.
7: *
8: * (c) John Paul E. Balandan, CPA <paulbalandan@gmail.com>
9: *
10: * For the full copyright and license information, please view
11: * the LICENSE file that was distributed with this source code.
12: */
13:
14: namespace Nexus\PHPStan\Rules\PhpDoc;
15:
16: use PhpParser\Node;
17: use PHPStan\Analyser\Scope;
18: use PHPStan\Node\VirtualNode;
19: use PHPStan\PhpDocParser\Lexer\Lexer;
20: use PHPStan\PhpDocParser\Parser\PhpDocParser;
21: use PHPStan\PhpDocParser\Parser\TokenIterator;
22: use PHPStan\Rules\Rule;
23: use PHPStan\Rules\RuleErrorBuilder;
24:
25: /**
26: * @implements Rule<Node\Stmt>
27: */
28: final class DisallowedPhpstanDocTagRule implements Rule
29: {
30: /**
31: * @var list<non-empty-string>
32: */
33: private const ALLOWED_PHPSTAN_TAGS = [
34: '@phpstan-ignore',
35: '@phpstan-ignore-next-line',
36: '@phpstan-ignore-line',
37: '@phpstan-type',
38: '@phpstan-import-type',
39: '@phpstan-assert',
40: '@phpstan-assert-if-true',
41: '@phpstan-assert-if-false',
42: '@phpstan-self-out',
43: '@phpstan-this-out',
44: '@phpstan-allow-private-mutation',
45: '@phpstan-readonly-allow-private-mutation',
46: '@phpstan-require-extends',
47: '@phpstan-require-implements',
48: ];
49:
50: public function __construct(
51: private Lexer $phpDocLexer,
52: private PhpDocParser $phpDocParser,
53: ) {}
54:
55: public function getNodeType(): string
56: {
57: return Node\Stmt::class;
58: }
59:
60: public function processNode(Node $node, Scope $scope): array
61: {
62: if ($node instanceof VirtualNode) {
63: return [];
64: }
65:
66: if ($node instanceof Node\Stmt\Expression) {
67: if (
68: ! $node->expr instanceof Node\Expr\Assign
69: && ! $node->expr instanceof Node\Expr\AssignRef
70: ) {
71: return [];
72: }
73: }
74:
75: $docComment = $node->getDocComment();
76:
77: if (null === $docComment) {
78: return [];
79: }
80:
81: $phpDocString = $docComment->getText();
82: $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
83: $phpDocNode = $this->phpDocParser->parse($tokens);
84: $errors = [];
85:
86: foreach ($phpDocNode->getTags() as $phpDocTagNode) {
87: $phpDocName = $phpDocTagNode->name;
88:
89: if (! str_starts_with($phpDocName, '@phpstan-')) {
90: continue;
91: }
92:
93: if (\in_array($phpDocName, self::ALLOWED_PHPSTAN_TAGS, true)) {
94: continue;
95: }
96:
97: $errors[] = RuleErrorBuilder::message(\sprintf('Disallowed PHPStan-prefixed PHPDoc tag: %s', $phpDocTagNode->__toString()))
98: ->tip(\sprintf('Use the native PHPDoc instead: %s', str_replace('@phpstan-', '@', $phpDocTagNode->__toString())))
99: ->line($docComment->getStartLine())
100: ->identifier('nexus.phpstanPhpdocTag')
101: ->build()
102: ;
103: }
104:
105: return $errors;
106: }
107: }
108: