Skip to content

Commit 0bfca1d

Browse files
committed
Report misplaced @phpstan-ignore-var in more places and anchor the one-line form
- report the tag above a closure or arrow function passed as an argument or written as an array item, whose PHPDoc php-parser attaches to the argument or the item - report it above `elseif`, `else` and `catch`, where no `@var` applies, instead of checking the ignored tags there - recognise it in a `@var` description only at the start, so a description that mentions it keeps the `@var` applied - cover an ignored `@var` over a variable that may not be defined
1 parent 612020e commit 0bfca1d

8 files changed

Lines changed: 136 additions & 8 deletions

‎src/PhpDoc/PhpDocNodeResolver.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,13 +738,14 @@ public function resolveAreVarTagsIgnored(PhpDocNode $phpDocNode): bool
738738

739739
/**
740740
* phpdoc-parser reads @phpstan-ignore-var written on the line of a @var tag, as in
741-
* a one-line PHPDoc, as the description of that tag.
741+
* a one-line PHPDoc, as the description of that tag. It counts only at the start of
742+
* the description, so a description that mentions the tag keeps the @var applied.
742743
*/
743744
public static function isIgnoreVarInVarTagDescription(PhpDocTagNode $phpDocTag): bool
744745
{
745746
return in_array($phpDocTag->name, self::VAR_TAG_NAMES, true)
746747
&& $phpDocTag->value instanceof VarTagValueNode
747-
&& preg_match('~@phpstan-ignore-var\b~', $phpDocTag->value->description) === 1;
748+
&& preg_match('~^@phpstan-ignore-var\b~', $phpDocTag->value->description) === 1;
748749
}
749750

750751
public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool

‎src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public function __construct(
8787

8888
public function getNodeTypes(): array
8989
{
90-
return [Node\Stmt::class, Node\PropertyHook::class, Node\Expr\Closure::class, Node\Expr\ArrowFunction::class];
90+
return [Node\Stmt::class, Node\PropertyHook::class, Node\Expr\Closure::class, Node\Expr\ArrowFunction::class, Node\Arg::class, Node\ArrayItem::class];
9191
}
9292

9393
public function getNodeType(): string
@@ -104,10 +104,19 @@ public function processNode(Node $node, Scope $scope): array
104104
// closure PHPDocs are only checked for @phpstan-ignore-var, which has no effect there
105105
if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) {
106106
return array_merge(
107-
$this->processMisplacedIgnoreVar($node, $node->getDocComment(), $node instanceof Node\Expr\Closure ? 'above a closure' : 'above an arrow function'),
107+
$this->processMisplacedIgnoreVar($node, $node->getDocComment(), self::describeClosure($node)),
108108
$this->processParameters($node),
109109
);
110110
}
111+
// php-parser gives the PHPDoc of a closure passed as an argument or written as an
112+
// array item to the argument or the item
113+
if ($node instanceof Node\Arg || $node instanceof Node\ArrayItem) {
114+
if (!$node->value instanceof Node\Expr\Closure && !$node->value instanceof Node\Expr\ArrowFunction) {
115+
return [];
116+
}
117+
118+
return $this->processMisplacedIgnoreVar($node, $node->getDocComment(), self::describeClosure($node->value));
119+
}
111120
if (!$node instanceof Node\Stmt && !$node instanceof Node\PropertyHook) {
112121
return [];
113122
}
@@ -204,6 +213,11 @@ private static function isIgnoreVarTag(PhpDocTagNode $phpDocTag): bool
204213
return $phpDocTag->name === '@phpstan-ignore-var' || PhpDocNodeResolver::isIgnoreVarInVarTagDescription($phpDocTag);
205214
}
206215

216+
private static function describeClosure(Node\Expr\Closure|Node\Expr\ArrowFunction $node): string
217+
{
218+
return $node instanceof Node\Expr\Closure ? 'above a closure' : 'above an arrow function';
219+
}
220+
207221
/**
208222
* Null for statements with inline @var tags, the only place @phpstan-ignore-var has an effect.
209223
*/
@@ -245,6 +259,16 @@ private static function describeMisplacement(Node $node, Scope $scope): ?string
245259
if ($node instanceof Node\Stmt\TraitUse) {
246260
return 'above a trait use';
247261
}
262+
// NodeScopeResolver processes only the statements inside these, so no @var above them applies
263+
if ($node instanceof Node\Stmt\ElseIf_) {
264+
return 'above an elseif branch';
265+
}
266+
if ($node instanceof Node\Stmt\Else_) {
267+
return 'above an else branch';
268+
}
269+
if ($node instanceof Node\Stmt\Catch_) {
270+
return 'above a catch block';
271+
}
248272
if ($node instanceof Node\Stmt\Nop && $scope->isInClass() && $scope->getFunction() === null) {
249273
return 'at the end of a class body';
250274
}

‎src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,14 @@ public function processNode(Node $node, Scope $scope): array
7878
$ignoredVarTags = [];
7979
$errors = [];
8080
$function = $scope->getFunction();
81-
// inline @var tags apply in functions and outside classes; elsewhere (above declarations,
82-
// enum cases and trait uses, at the end of a class body) InvalidPHPStanDocTagRule reports
83-
// the @phpstan-ignore-var tag as misplaced
84-
$canIgnoreVarTags = !$node instanceof VirtualNode && !($scope->isInClass() && $function === null);
81+
// inline @var tags apply to statements in functions and outside classes; elsewhere (above
82+
// declarations, enum cases, trait uses, elseif, else and catch, at the end of a class body)
83+
// InvalidPHPStanDocTagRule reports the @phpstan-ignore-var tag as misplaced
84+
$canIgnoreVarTags = !$node instanceof VirtualNode
85+
&& !$node instanceof Node\Stmt\ElseIf_
86+
&& !$node instanceof Node\Stmt\Else_
87+
&& !$node instanceof Node\Stmt\Catch_
88+
&& !($scope->isInClass() && $function === null);
8589
foreach ($node->getComments() as $comment) {
8690
if (!$comment instanceof Doc) {
8791
continue;

‎tests/PHPStan/Analyser/nsrt/phpstan-ignore-var.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,14 @@ function doOneLine(): void
8181
$times = getTimes(3);
8282
assertType('iterable<(int|string), DateTime>', $times);
8383
}
84+
85+
function doDescription(): void
86+
{
87+
/** @var list<\DateTime> $times @phpstan-ignore-var because the IDE needs it */
88+
$times = getTimes(3);
89+
assertType('iterable<(int|string), DateTime>', $times);
90+
91+
/** @var list<int> $ids unlike a tag followed by @phpstan-ignore-var, this one applies */
92+
$ids = [1, 2];
93+
assertType('list<int>', $ids);
94+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,30 @@ public function testMisplacedIgnoreVar(): void
132132
'PHPDoc tag @phpstan-ignore-var at the end of a class body has no effect.',
133133
106,
134134
],
135+
[
136+
'PHPDoc tag @phpstan-ignore-var above a closure has no effect.',
137+
112,
138+
],
139+
[
140+
'PHPDoc tag @phpstan-ignore-var above an arrow function has no effect.',
141+
115,
142+
],
143+
[
144+
'PHPDoc tag @phpstan-ignore-var above an arrow function has no effect.',
145+
117,
146+
],
147+
[
148+
'PHPDoc tag @phpstan-ignore-var above an elseif branch has no effect.',
149+
129,
150+
],
151+
[
152+
'PHPDoc tag @phpstan-ignore-var above an else branch has no effect.',
153+
135,
154+
],
155+
[
156+
'PHPDoc tag @phpstan-ignore-var above a catch block has no effect.',
157+
146,
158+
],
135159
]);
136160
}
137161

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ public function testPhpStanIgnoreVar(): void
220220
'PHPDoc tag @var with type string is not subtype of native type int.',
221221
167,
222222
],
223+
[
224+
'PHPDoc tag @var with type string is not subtype of native type 1.',
225+
185,
226+
],
223227
]);
224228
}
225229

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,45 @@ public function doFoo(
106106
* @phpstan-ignore-var
107107
*/
108108
}
109+
110+
function closuresAsArgumentsAndArrayItems(array $a): void
111+
{
112+
array_map(/** @phpstan-ignore-var */ function (int $x): int {
113+
return $x;
114+
}, $a);
115+
usort($a, /** @phpstan-ignore-var */ static fn (int $x, int $y): int => $x <=> $y);
116+
$callbacks = [
117+
/** @phpstan-ignore-var */
118+
fn (): int => 1,
119+
];
120+
}
121+
122+
function elseIfElseAndCatch(int $x, bool $a, bool $b): void
123+
{
124+
if ($a) {
125+
echo 1;
126+
}
127+
/**
128+
* @var string $x
129+
* @phpstan-ignore-var
130+
*/
131+
elseif ($b) {
132+
echo 2;
133+
}
134+
/**
135+
* @phpstan-ignore-var
136+
*/
137+
else {
138+
echo 3;
139+
}
140+
141+
try {
142+
echo 4;
143+
}
144+
/**
145+
* @var string $x
146+
* @phpstan-ignore-var
147+
*/
148+
catch (\Exception $e) {
149+
}
150+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,21 @@ public function doBaz(int $i): void
168168
}
169169

170170
}
171+
172+
class MaybeDefined
173+
{
174+
175+
public function doFoo(bool $b): void
176+
{
177+
if ($b) {
178+
$m = 1;
179+
}
180+
181+
/**
182+
* @var string $m
183+
* @phpstan-ignore-var
184+
*/
185+
echo 'x';
186+
}
187+
188+
}

0 commit comments

Comments
 (0)