1212use PHPStan \Node \InClassNode ;
1313use PHPStan \Node \InFunctionNode ;
1414use PHPStan \Node \VirtualNode ;
15+ use PHPStan \PhpDoc \PhpDocNodeResolver ;
16+ use PHPStan \PhpDoc \ResolvedPhpDocBlock ;
1517use PHPStan \PhpDoc \Tag \VarTag ;
1618use PHPStan \Rules \IdentifierRuleError ;
1719use PHPStan \Rules \Rule ;
@@ -73,8 +75,12 @@ public function processNode(Node $node, Scope $scope): array
7375 }
7476
7577 $ varTags = [];
78+ $ ignoredVarTags = [];
7679 $ errors = [];
7780 $ function = $ scope ->getFunction ();
81+ // inline @var tags apply in functions and outside classes; above declarations, enum cases
82+ // and trait uses InvalidPHPStanDocTagRule reports @phpstan-ignore-var as misplaced
83+ $ canIgnoreVarTags = !$ node instanceof VirtualNode && !($ scope ->isInClass () && $ function === null );
7884 foreach ($ node ->getComments () as $ comment ) {
7985 if (!$ comment instanceof Doc) {
8086 continue ;
@@ -87,24 +93,94 @@ public function processNode(Node $node, Scope $scope): array
8793 $ comment ->getText (),
8894 );
8995 $ docVarTags = $ resolvedPhpDoc ->getVarTags ();
90- // above declarations InvalidPHPStanDocTagRule reports the tag as misplaced
91- if (count ($ docVarTags ) === 0 && $ resolvedPhpDoc ->areVarTagsIgnored () && !$ node instanceof VirtualNode) {
92- $ errors [] = RuleErrorBuilder ::message ('PHPDoc tag @phpstan-ignore-var without a @var tag has no effect. ' )
93- ->identifier ('varTag.misplaced ' )
94- ->build ();
96+ if ($ canIgnoreVarTags && $ resolvedPhpDoc ->areVarTagsIgnored ()) {
97+ if (!self ::hasVarTag ($ resolvedPhpDoc )) {
98+ $ errors [] = RuleErrorBuilder ::message ('PHPDoc tag @phpstan-ignore-var without a @var tag has no effect. ' )
99+ ->identifier ('varTag.misplaced ' )
100+ ->build ();
101+ }
102+ foreach ($ docVarTags as $ key => $ varTag ) {
103+ if (!is_string ($ key )) {
104+ continue ;
105+ }
106+ $ ignoredVarTags [$ key ] = $ varTag ;
107+ }
95108 }
96109 foreach ($ docVarTags as $ key => $ varTag ) {
97110 $ varTags [$ key ] = $ varTag ;
98111 }
99112 }
100113
114+ foreach ($ this ->processIgnoredVarTags ($ node , $ scope , $ ignoredVarTags ) as $ error ) {
115+ $ errors [] = $ error ;
116+ }
117+
101118 if (count ($ varTags ) === 0 ) {
102119 return $ errors ;
103120 }
104121
105122 return array_merge ($ errors , $ this ->processVarTags ($ node , $ scope , $ varTags ));
106123 }
107124
125+ /**
126+ * Whether the PHPDoc has a @var tag of any prefix, including one with an invalid value.
127+ */
128+ private static function hasVarTag (ResolvedPhpDocBlock $ resolvedPhpDoc ): bool
129+ {
130+ foreach ($ resolvedPhpDoc ->getPhpDocNodes () as $ phpDocNode ) {
131+ foreach (PhpDocNodeResolver::VAR_TAG_NAMES as $ tagName ) {
132+ if (count ($ phpDocNode ->getTagsByName ($ tagName )) > 0 ) {
133+ return true ;
134+ }
135+ }
136+ }
137+
138+ return false ;
139+ }
140+
141+ /**
142+ * StatementsHandler::processStmtVarAnnotation() reports @var tags naming an existing variable
143+ * through VarTagChangedExpressionTypeNode, but skips them in a PHPDoc with @phpstan-ignore-var:
144+ * it runs twice for an assignment and, with their type not assigned, would report them twice.
145+ * The same check runs here instead, once per statement.
146+ *
147+ * @param array<string, VarTag> $varTags
148+ * @return list<IdentifierRuleError>
149+ */
150+ private function processIgnoredVarTags (Node \Stmt $ node , Scope $ scope , array $ varTags ): array
151+ {
152+ // mirrored from StatementsHandler::processStmtVarAnnotation(), which is not called for foreach
153+ if ($ node instanceof Node \Stmt \Foreach_) {
154+ return [];
155+ }
156+
157+ $ assignedVariable = null ;
158+ if (
159+ $ node instanceof Node \Stmt \Expression
160+ && ($ node ->expr instanceof Expr \Assign || $ node ->expr instanceof Expr \AssignRef)
161+ && $ node ->expr ->var instanceof Expr \Variable
162+ && is_string ($ node ->expr ->var ->name )
163+ ) {
164+ $ assignedVariable = $ node ->expr ->var ->name ;
165+ }
166+
167+ $ errors = [];
168+ foreach ($ varTags as $ name => $ varTag ) {
169+ if ($ name === $ assignedVariable || $ scope ->hasVariableType ($ name )->no ()) {
170+ continue ;
171+ }
172+ if ($ scope ->getVariableType ($ name )->equals ($ varTag ->getType ())) {
173+ continue ;
174+ }
175+
176+ foreach ($ this ->varTagTypeRuleHelper ->checkExprType ($ scope , new Expr \Variable ($ name ), $ varTag ->getType ()) as $ error ) {
177+ $ errors [] = $ error ;
178+ }
179+ }
180+
181+ return $ errors ;
182+ }
183+
108184 /**
109185 * @param VarTag[] $varTags
110186 * @return list<IdentifierRuleError>
0 commit comments