Skip to content

Commit ff3e256

Browse files
committed
Support one-line @phpstan-ignore-var and tighten its checks
- recognise the tag on the line of a `@var` tag, as in `/** @var Foo $x @phpstan-ignore-var */` - do not check ignored `@var` tags above `global` and `static` statements against an earlier variable - report the tag above parameters, closures and arrow functions and at the end of a class body
1 parent 2de4c07 commit ff3e256

8 files changed

Lines changed: 203 additions & 35 deletions

‎src/PhpDoc/PhpDocNodeResolver.php‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
3131
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
3232
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
33+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
3334
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
35+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
3436
use PHPStan\Reflection\PassedByReference;
3537
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
3638
use PHPStan\Type\Generic\TemplateTypeFactory;
@@ -48,6 +50,7 @@
4850
use function count;
4951
use function in_array;
5052
use function method_exists;
53+
use function preg_match;
5154
use function str_starts_with;
5255
use function substr;
5356

@@ -720,7 +723,28 @@ public function resolveAllMethodsImpure(PhpDocNode $phpDocNode): bool
720723

721724
public function resolveAreVarTagsIgnored(PhpDocNode $phpDocNode): bool
722725
{
723-
return count($phpDocNode->getTagsByName('@phpstan-ignore-var')) > 0;
726+
if (count($phpDocNode->getTagsByName('@phpstan-ignore-var')) > 0) {
727+
return true;
728+
}
729+
730+
foreach ($phpDocNode->getTags() as $phpDocTag) {
731+
if (self::isIgnoreVarInVarTagDescription($phpDocTag)) {
732+
return true;
733+
}
734+
}
735+
736+
return false;
737+
}
738+
739+
/**
740+
* 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.
742+
*/
743+
public static function isIgnoreVarInVarTagDescription(PhpDocTagNode $phpDocTag): bool
744+
{
745+
return in_array($phpDocTag->name, self::VAR_TAG_NAMES, true)
746+
&& $phpDocTag->value instanceof VarTagValueNode
747+
&& preg_match('~@phpstan-ignore-var\b~', $phpDocTag->value->description) === 1;
724748
}
725749

726750
public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool

‎src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php‎

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PHPStan\Rules\PhpDoc;
44

5+
use PhpParser\Comment\Doc;
56
use PhpParser\Node;
67
use PhpParser\NodeAbstract;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\DependencyInjection\RegisteredRule;
910
use PHPStan\DependencyInjection\ValidatesStubFiles;
1011
use PHPStan\Node\VirtualNode;
12+
use PHPStan\PhpDoc\PhpDocNodeResolver;
1113
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1214
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
1315
use PHPStan\PhpDocParser\Lexer\Lexer;
@@ -16,8 +18,10 @@
1618
use PHPStan\Rules\IdentifierRuleError;
1719
use PHPStan\Rules\MultipleNodeTypesRule;
1820
use PHPStan\Rules\RuleErrorBuilder;
21+
use function array_merge;
1922
use function in_array;
2023
use function sprintf;
24+
use function str_contains;
2125
use 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();

‎src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ 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; above declarations, enum cases
82-
// and trait uses InvalidPHPStanDocTagRule reports @phpstan-ignore-var as misplaced
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
8384
$canIgnoreVarTags = !$node instanceof VirtualNode && !($scope->isInClass() && $function === null);
8485
foreach ($node->getComments() as $comment) {
8586
if (!$comment instanceof Doc) {
@@ -149,8 +150,9 @@ private static function hasVarTag(ResolvedPhpDocBlock $resolvedPhpDoc): bool
149150
*/
150151
private function processIgnoredVarTags(Node\Stmt $node, Scope $scope, array $varTags): array
151152
{
152-
// mirrored from StatementsHandler::processStmtVarAnnotation(), which is not called for foreach
153-
if ($node instanceof Node\Stmt\Foreach_) {
153+
// mirrored from NodeScopeResolver::processStmtNode(), which does not call
154+
// StatementsHandler::processStmtVarAnnotation() for these
155+
if ($node instanceof Node\Stmt\Foreach_ || $node instanceof Node\Stmt\Static_ || $node instanceof Node\Stmt\Global_) {
154156
return [];
155157
}
156158

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,10 @@ function doStatic(int $i): void
7474
assertType('array<int, \'x\'>', $cache);
7575
$cache[$i] = 'x';
7676
}
77+
78+
function doOneLine(): void
79+
{
80+
/** @var list<\DateTime> $times @phpstan-ignore-var */
81+
$times = getTimes(3);
82+
assertType('iterable<(int|string), DateTime>', $times);
83+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,30 @@ public function testMisplacedIgnoreVar(): void
108108
'PHPDoc tag @phpstan-ignore-var above a trait use has no effect.',
109109
77,
110110
],
111+
[
112+
'PHPDoc tag @phpstan-ignore-var above a property has no effect.',
113+
86,
114+
],
115+
[
116+
'PHPDoc tag @phpstan-ignore-var above a parameter has no effect.',
117+
92,
118+
],
119+
[
120+
'PHPDoc tag @phpstan-ignore-var above a closure has no effect.',
121+
97,
122+
],
123+
[
124+
'PHPDoc tag @phpstan-ignore-var above a parameter has no effect.',
125+
98,
126+
],
127+
[
128+
'PHPDoc tag @phpstan-ignore-var above an arrow function has no effect.',
129+
102,
130+
],
131+
[
132+
'PHPDoc tag @phpstan-ignore-var at the end of a class body has no effect.',
133+
106,
134+
],
111135
]);
112136
}
113137

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ public function testPhpStanIgnoreVar(): void
216216
'PHPDoc tag @var with type string is not subtype of native type int.',
217217
136,
218218
],
219+
[
220+
'PHPDoc tag @var with type string is not subtype of native type int.',
221+
167,
222+
],
219223
]);
220224
}
221225

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,30 @@ class UsesTrait
7979
use Baz;
8080

8181
}
82+
83+
class OneLineAndParameters
84+
{
85+
86+
/** @var list<int> @phpstan-ignore-var */
87+
public array $oneLine = [];
88+
89+
public function doFoo(
90+
/**
91+
* @var int
92+
* @phpstan-ignore-var
93+
*/
94+
$notPromoted,
95+
): void
96+
{
97+
$f = /** @phpstan-ignore-var */ function (
98+
/** @phpstan-ignore-var */
99+
$x,
100+
): void {
101+
};
102+
$g = /** @phpstan-ignore-var */ fn () => 1;
103+
}
104+
105+
/**
106+
* @phpstan-ignore-var
107+
*/
108+
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,34 @@ public function doBar(int $i): int
137137
}
138138

139139
}
140+
141+
class GlobalAndStatic
142+
{
143+
144+
public function doFoo(): void
145+
{
146+
$x = 1;
147+
/**
148+
* @var string $x
149+
* @phpstan-ignore-var
150+
*/
151+
global $x;
152+
}
153+
154+
public function doBar(): void
155+
{
156+
$n = 'a';
157+
/**
158+
* @var int $n
159+
* @phpstan-ignore-var
160+
*/
161+
static $n = 0;
162+
}
163+
164+
public function doBaz(int $i): void
165+
{
166+
/** @var string $s @phpstan-ignore-var */
167+
$s = $i;
168+
}
169+
170+
}

0 commit comments

Comments
 (0)