Skip to content

Commit 2de4c07

Browse files
committed
Check ignored @var tags once per statement and tighten the @phpstan-ignore-var errors
1 parent cf0dfd6 commit 2de4c07

11 files changed

Lines changed: 205 additions & 14 deletions

‎src/Analyser/StatementsHandler.php‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,6 +1953,12 @@ public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, M
19531953
continue;
19541954
}
19551955

1956+
// WrongVariableNameInVarTagRule checks these: with their type not assigned, the
1957+
// second run of this method for an assignment would report them again
1958+
if ($areVarTagsIgnored) {
1959+
continue;
1960+
}
1961+
19561962
if ($name === $assignedVariable) {
19571963
continue;
19581964
}
@@ -1975,9 +1981,6 @@ public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, M
19751981
if (!$originalType->equals($varTag->getType())) {
19761982
$nodeScopeResolver->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage);
19771983
}
1978-
if ($areVarTagsIgnored) {
1979-
continue;
1980-
}
19811984
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
19821985
if ($templateArgumentFrame !== null) {
19831986
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($varTag->getType(), $originalType));

‎src/PhpDoc/PhpDocNodeResolver.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
final class PhpDocNodeResolver
5656
{
5757

58+
/** The names of the @var tag, the last one taking precedence. */
59+
public const VAR_TAG_NAMES = ['@var', '@phan-var', '@psalm-var', '@phpstan-var'];
60+
5861
public function __construct(
5962
private TypeNodeResolver $typeNodeResolver,
6063
private ConstExprNodeResolver $constExprNodeResolver,
@@ -70,7 +73,7 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar
7073
{
7174
$resolved = [];
7275
$resolvedByTag = [];
73-
foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) {
76+
foreach (self::VAR_TAG_NAMES as $tagName) {
7477
$tagResolved = [];
7578
foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) {
7679
$type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope);

‎src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ private static function describeDeclaration(Node $node): ?string
193193
if ($node instanceof Node\Stmt\ClassLike) {
194194
return 'a class';
195195
}
196+
if ($node instanceof Node\Stmt\EnumCase) {
197+
return 'an enum case';
198+
}
199+
if ($node instanceof Node\Stmt\TraitUse) {
200+
return 'a trait use';
201+
}
196202

197203
return null;
198204
}

‎src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php‎

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use PHPStan\Node\InClassNode;
1313
use PHPStan\Node\InFunctionNode;
1414
use PHPStan\Node\VirtualNode;
15+
use PHPStan\PhpDoc\PhpDocNodeResolver;
16+
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
1517
use PHPStan\PhpDoc\Tag\VarTag;
1618
use PHPStan\Rules\IdentifierRuleError;
1719
use 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>

‎tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@ public function testMisplacedIgnoreVar(): void
104104
'PHPDoc tag @phpstan-ignore-var above a trait has no effect.',
105105
65,
106106
],
107+
[
108+
'PHPDoc tag @phpstan-ignore-var above a trait use has no effect.',
109+
77,
110+
],
111+
]);
112+
}
113+
114+
#[RequiresPhp('>= 8.1.0')]
115+
public function testMisplacedIgnoreVarInEnum(): void
116+
{
117+
$this->analyse([__DIR__ . '/data/misplaced-phpstan-ignore-var-enum.php'], [
118+
[
119+
'PHPDoc tag @phpstan-ignore-var above an enum case has no effect.',
120+
9,
121+
],
107122
]);
108123
}
109124

‎tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ public function testRule(): void
5252
public function testPhpStanIgnoreVar(): void
5353
{
5454
$this->analyse([__DIR__ . '/data/phpstan-ignore-var.php'], [
55-
[
56-
'PHPDoc tag @var with type string is not subtype of native type int.',
57-
48,
58-
],
5955
[
6056
'PHPDoc tag @var with type string is not subtype of native type int.',
6157
57,

‎tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ public function testPhpStanIgnoreVar(): void
184184
'PHPDoc tag @var with type string is not subtype of native type int.',
185185
38,
186186
],
187+
[
188+
'PHPDoc tag @var with type string is not subtype of native type int.',
189+
48,
190+
],
187191
[
188192
'PHPDoc tag @phpstan-ignore-var without a @var tag has no effect.',
189193
73,
@@ -200,9 +204,33 @@ public function testPhpStanIgnoreVar(): void
200204
'PHPDoc tag @phpstan-ignore-var without a @var tag has no effect.',
201205
98,
202206
],
207+
[
208+
'PHPDoc tag @var with type string is not subtype of native type 1.',
209+
121,
210+
],
211+
[
212+
'PHPDoc tag @var with type string is not subtype of native type int.',
213+
127,
214+
],
215+
[
216+
'PHPDoc tag @var with type string is not subtype of native type int.',
217+
136,
218+
],
203219
]);
204220
}
205221

222+
#[RequiresPhp('>= 8.0.0')]
223+
public function testMisplacedPhpStanIgnoreVar(): void
224+
{
225+
$this->analyse([__DIR__ . '/data/misplaced-phpstan-ignore-var.php'], []);
226+
}
227+
228+
#[RequiresPhp('>= 8.1.0')]
229+
public function testMisplacedPhpStanIgnoreVarInEnum(): void
230+
{
231+
$this->analyse([__DIR__ . '/data/misplaced-phpstan-ignore-var-enum.php'], []);
232+
}
233+
206234
public function testBug3515(): void
207235
{
208236
$this->analyse([__DIR__ . '/data/bug-3515.php'], []);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php // lint >= 8.1
2+
3+
namespace MisplacedPhpStanIgnoreVarEnum;
4+
5+
enum Suit
6+
{
7+
8+
/**
9+
* @phpstan-ignore-var
10+
*/
11+
case Hearts;
12+
13+
}

‎tests/PHPStan/Rules/PhpDoc/data/misplaced-phpstan-ignore-var.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,14 @@ trait Baz
6868
{
6969

7070
}
71+
72+
class UsesTrait
73+
{
74+
75+
/**
76+
* @var int $x
77+
* @phpstan-ignore-var
78+
*/
79+
use Baz;
80+
81+
}

‎tests/PHPStan/Rules/PhpDoc/data/phpstan-ignore-var.php‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,42 @@ public function doBar(): int
9898
return 1;
9999
}
100100

101+
public function doBaz(object $o): void
102+
{
103+
/**
104+
* @var array<int $o
105+
* @phpstan-ignore-var
106+
*/
107+
$o->doFoo();
108+
}
109+
110+
}
111+
112+
class ReportedOnce
113+
{
114+
115+
public function doFoo(int $other): void
116+
{
117+
/**
118+
* @var string $a
119+
* @phpstan-ignore-var
120+
*/
121+
[$a, $b] = [1, 2];
122+
123+
/**
124+
* @var string $other
125+
* @phpstan-ignore-var
126+
*/
127+
$x = 1;
128+
}
129+
130+
public function doBar(int $i): int
131+
{
132+
/**
133+
* @var string $i
134+
* @phpstan-ignore-var
135+
*/
136+
return $i;
137+
}
138+
101139
}

0 commit comments

Comments
 (0)