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: private const array ALLOWED_PHPSTAN_TAGS = [
31: '@phpstan-ignore',
32: '@phpstan-ignore-next-line',
33: '@phpstan-ignore-line',
34: '@phpstan-type',
35: '@phpstan-import-type',
36: '@phpstan-assert',
37: '@phpstan-assert-if-true',
38: '@phpstan-assert-if-false',
39: '@phpstan-self-out',
40: '@phpstan-this-out',
41: '@phpstan-allow-private-mutation',
42: '@phpstan-readonly-allow-private-mutation',
43: '@phpstan-require-extends',
44: '@phpstan-require-implements',
45: ];
46:
47: public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) {}
48:
49: #[\Override]
50: public function getNodeType(): string
51: {
52: return Node\Stmt::class;
53: }
54:
55: #[\Override]
56: public function processNode(Node $node, Scope $scope): array
57: {
58: if ($node instanceof VirtualNode) {
59: return [];
60: }
61:
62: if ($node instanceof Node\Stmt\Expression) {
63: if (
64: ! $node->expr instanceof Node\Expr\Assign
65: && ! $node->expr instanceof Node\Expr\AssignRef
66: ) {
67: return [];
68: }
69: }
70:
71: $docComment = $node->getDocComment();
72:
73: if (null === $docComment) {
74: return [];
75: }
76:
77: $phpDocString = $docComment->getText();
78: $tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
79: $phpDocNode = $this->phpDocParser->parse($tokens);
80: $errors = [];
81:
82: foreach ($phpDocNode->getTags() as $phpDocTagNode) {
83: $phpDocName = $phpDocTagNode->name;
84:
85: if (! str_starts_with($phpDocName, '@phpstan-')) {
86: continue;
87: }
88:
89: if (\in_array($phpDocName, self::ALLOWED_PHPSTAN_TAGS, true)) {
90: continue;
91: }
92:
93: $errors[] = RuleErrorBuilder::message(\sprintf('Disallowed PHPStan-prefixed PHPDoc tag: %s', $phpDocTagNode->__toString()))
94: ->tip(\sprintf('Use the native PHPDoc instead: %s', str_replace('@phpstan-', '@', $phpDocTagNode->__toString())))
95: ->line($docComment->getStartLine())
96: ->identifier('nexus.phpstanPhpdocTag')
97: ->build()
98: ;
99: }
100:
101: return $errors;
102: }
103: }
104: