22
33namespace PHPStan \Rules \PhpDoc ;
44
5+ use PhpParser \Comment \Doc ;
56use PhpParser \Node ;
67use PhpParser \NodeAbstract ;
78use PHPStan \Analyser \Scope ;
89use PHPStan \DependencyInjection \RegisteredRule ;
910use PHPStan \DependencyInjection \ValidatesStubFiles ;
1011use PHPStan \Node \VirtualNode ;
12+ use PHPStan \PhpDoc \PhpDocNodeResolver ;
1113use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocNode ;
1214use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
1315use PHPStan \PhpDocParser \Lexer \Lexer ;
1618use PHPStan \Rules \IdentifierRuleError ;
1719use PHPStan \Rules \MultipleNodeTypesRule ;
1820use PHPStan \Rules \RuleErrorBuilder ;
21+ use function array_merge ;
1922use function in_array ;
2023use function sprintf ;
24+ use function str_contains ;
2125use function str_starts_with ;
2226
2327/**
@@ -83,7 +87,7 @@ public function __construct(
8387
8488 public function getNodeTypes (): array
8589 {
86- return [Node \Stmt::class, Node \PropertyHook::class];
90+ return [Node \Stmt::class, Node \PropertyHook::class, Node \ Expr \Closure::class, Node \ Expr \ArrowFunction::class ];
8791 }
8892
8993 public function getNodeType (): string
@@ -97,6 +101,13 @@ public function processNode(Node $node, Scope $scope): array
97101 if ($ node instanceof VirtualNode) {
98102 return [];
99103 }
104+ // closure PHPDocs are only checked for @phpstan-ignore-var, which has no effect there
105+ if ($ node instanceof Node \Expr \Closure || $ node instanceof Node \Expr \ArrowFunction) {
106+ return array_merge (
107+ $ this ->processMisplacedIgnoreVar ($ node , $ node ->getDocComment (), $ node instanceof Node \Expr \Closure ? 'above a closure ' : 'above an arrow function ' ),
108+ $ this ->processParameters ($ node ),
109+ );
110+ }
100111 if (!$ node instanceof Node \Stmt && !$ node instanceof Node \PropertyHook) {
101112 return [];
102113 }
@@ -113,10 +124,10 @@ public function processNode(Node $node, Scope $scope): array
113124 $ errors = [];
114125 $ docComment = $ node ->getDocComment ();
115126 if ($ docComment !== null ) {
116- $ declaration = self ::describeDeclaration ($ node );
127+ $ placement = self ::describeMisplacement ($ node, $ scope );
117128 foreach ($ this ->parsePhpDoc ($ docComment ->getText ())->getTags () as $ phpDocTag ) {
118- if ($ phpDocTag -> name === ' @phpstan-ignore-var ' && $ declaration !== null ) {
119- $ errors [] = self ::createMisplacedIgnoreVarError ($ node , $ phpDocTag , $ declaration );
129+ if ($ placement !== null && self :: isIgnoreVarTag ( $ phpDocTag ) ) {
130+ $ errors [] = self ::createMisplacedIgnoreVarError ($ node , $ phpDocTag , $ placement );
120131 continue ;
121132 }
122133
@@ -135,16 +146,9 @@ public function processNode(Node $node, Scope $scope): array
135146 }
136147 }
137148
138- if ($ node instanceof Node \Stmt \ClassMethod) {
139- foreach ($ node ->params as $ param ) {
140- $ paramDocComment = $ param ->getDocComment ();
141- if (!$ param ->isPromoted () || $ paramDocComment === null ) {
142- continue ;
143- }
144-
145- foreach ($ this ->parsePhpDoc ($ paramDocComment ->getText ())->getTagsByName ('@phpstan-ignore-var ' ) as $ phpDocTag ) {
146- $ errors [] = self ::createMisplacedIgnoreVarError ($ param , $ phpDocTag , 'a promoted property ' );
147- }
149+ if ($ node instanceof Node \FunctionLike) {
150+ foreach ($ this ->processParameters ($ node ) as $ error ) {
151+ $ errors [] = $ error ;
148152 }
149153 }
150154
@@ -158,56 +162,101 @@ private function parsePhpDoc(string $phpDocString): PhpDocNode
158162 return $ this ->phpDocParser ->parse ($ tokens );
159163 }
160164
165+ /**
166+ * @return list<IdentifierRuleError>
167+ */
168+ private function processParameters (Node \FunctionLike $ node ): array
169+ {
170+ $ errors = [];
171+ foreach ($ node ->getParams () as $ param ) {
172+ $ placement = $ param ->isPromoted () ? 'above a promoted property ' : 'above a parameter ' ;
173+ foreach ($ this ->processMisplacedIgnoreVar ($ param , $ param ->getDocComment (), $ placement ) as $ error ) {
174+ $ errors [] = $ error ;
175+ }
176+ }
177+
178+ return $ errors ;
179+ }
180+
181+ /**
182+ * @return list<IdentifierRuleError>
183+ */
184+ private function processMisplacedIgnoreVar (Node $ node , ?Doc $ docComment , string $ placement ): array
185+ {
186+ if ($ docComment === null || !str_contains ($ docComment ->getText (), '@phpstan-ignore-var ' )) {
187+ return [];
188+ }
189+
190+ $ errors = [];
191+ foreach ($ this ->parsePhpDoc ($ docComment ->getText ())->getTags () as $ phpDocTag ) {
192+ if (!self ::isIgnoreVarTag ($ phpDocTag )) {
193+ continue ;
194+ }
195+
196+ $ errors [] = self ::createMisplacedIgnoreVarError ($ node , $ phpDocTag , $ placement );
197+ }
198+
199+ return $ errors ;
200+ }
201+
202+ private static function isIgnoreVarTag (PhpDocTagNode $ phpDocTag ): bool
203+ {
204+ return $ phpDocTag ->name === '@phpstan-ignore-var ' || PhpDocNodeResolver ::isIgnoreVarInVarTagDescription ($ phpDocTag );
205+ }
206+
161207 /**
162208 * Null for statements with inline @var tags, the only place @phpstan-ignore-var has an effect.
163209 */
164- private static function describeDeclaration (Node $ node ): ?string
210+ private static function describeMisplacement (Node $ node, Scope $ scope ): ?string
165211 {
166212 if ($ node instanceof Node \Stmt \Property) {
167- return 'a property ' ;
213+ return 'above a property ' ;
168214 }
169215 if ($ node instanceof Node \PropertyHook) {
170- return 'a property hook ' ;
216+ return 'above a property hook ' ;
171217 }
172218 if ($ node instanceof Node \Stmt \ClassConst) {
173- return 'a class constant ' ;
219+ return 'above a class constant ' ;
174220 }
175221 if ($ node instanceof Node \Stmt \Const_) {
176- return 'a constant ' ;
222+ return 'above a constant ' ;
177223 }
178224 if ($ node instanceof Node \Stmt \Function_) {
179- return 'a function ' ;
225+ return 'above a function ' ;
180226 }
181227 if ($ node instanceof Node \Stmt \ClassMethod) {
182- return 'a method ' ;
228+ return 'above a method ' ;
183229 }
184230 if ($ node instanceof Node \Stmt \Interface_) {
185- return 'an interface ' ;
231+ return 'above an interface ' ;
186232 }
187233 if ($ node instanceof Node \Stmt \Trait_) {
188- return 'a trait ' ;
234+ return 'above a trait ' ;
189235 }
190236 if ($ node instanceof Node \Stmt \Enum_) {
191- return 'an enum ' ;
237+ return 'above an enum ' ;
192238 }
193239 if ($ node instanceof Node \Stmt \ClassLike) {
194- return 'a class ' ;
240+ return 'above a class ' ;
195241 }
196242 if ($ node instanceof Node \Stmt \EnumCase) {
197- return 'an enum case ' ;
243+ return 'above an enum case ' ;
198244 }
199245 if ($ node instanceof Node \Stmt \TraitUse) {
200- return 'a trait use ' ;
246+ return 'above a trait use ' ;
247+ }
248+ if ($ node instanceof Node \Stmt \Nop && $ scope ->isInClass () && $ scope ->getFunction () === null ) {
249+ return 'at the end of a class body ' ;
201250 }
202251
203252 return null ;
204253 }
205254
206- private static function createMisplacedIgnoreVarError (Node $ node , PhpDocTagNode $ phpDocTag , string $ declaration ): IdentifierRuleError
255+ private static function createMisplacedIgnoreVarError (Node $ node , PhpDocTagNode $ phpDocTag , string $ placement ): IdentifierRuleError
207256 {
208257 return RuleErrorBuilder ::message (sprintf (
209- 'PHPDoc tag @phpstan-ignore-var above %s has no effect. ' ,
210- $ declaration ,
258+ 'PHPDoc tag @phpstan-ignore-var %s has no effect. ' ,
259+ $ placement ,
211260 ))
212261 ->line (PhpDocLineHelper ::detectLine ($ node , $ phpDocTag ))
213262 ->identifier ('varTag.misplaced ' )->build ();
0 commit comments