Skip to content

Commit c5ca230

Browse files
authored
Bleeding Edge - PhpDocParser: add config for lines in its AST & enable ignoring errors within phpdocs
1 parent 5d67f0c commit c5ca230

13 files changed

+205
-48
lines changed

conf/bleedingEdge.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ parameters:
1616
checkUnresolvableParameterTypes: true
1717
readOnlyByPhpDoc: true
1818
phpDocParserRequireWhitespaceBeforeDescription: true
19+
phpDocParserIncludeLines: true
20+
enableIgnoreErrorsWithinPhpDocs: true
1921
runtimeReflectionRules: true
2022
notAnalysedTrait: true
2123
curlSetOptTypes: true

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ parameters:
5151
checkUnresolvableParameterTypes: false
5252
readOnlyByPhpDoc: false
5353
phpDocParserRequireWhitespaceBeforeDescription: false
54+
phpDocParserIncludeLines: false
55+
enableIgnoreErrorsWithinPhpDocs: false
5456
runtimeReflectionRules: false
5557
notAnalysedTrait: false
5658
curlSetOptTypes: false
@@ -389,6 +391,8 @@ services:
389391
arguments:
390392
requireWhitespaceBeforeDescription: %featureToggles.phpDocParserRequireWhitespaceBeforeDescription%
391393
preserveTypeAliasesWithInvalidTypes: true
394+
usedAttributes:
395+
lines: %featureToggles.phpDocParserIncludeLines%
392396

393397
-
394398
class: PHPStan\PhpDoc\ConstExprParserFactory
@@ -1824,6 +1828,7 @@ services:
18241828
arguments:
18251829
parser: @currentPhpVersionPhpParser
18261830
lexer: @currentPhpVersionLexer
1831+
enableIgnoreErrorsWithinPhpDocs: %featureToggles.enableIgnoreErrorsWithinPhpDocs%
18271832
autowired: no
18281833

18291834
currentPhpVersionSimpleParser:

conf/parametersSchema.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ parametersSchema:
4646
checkUnresolvableParameterTypes: bool()
4747
readOnlyByPhpDoc: bool()
4848
phpDocParserRequireWhitespaceBeforeDescription: bool()
49+
phpDocParserIncludeLines: bool()
50+
enableIgnoreErrorsWithinPhpDocs: bool()
4951
runtimeReflectionRules: bool()
5052
notAnalysedTrait: bool()
5153
curlSetOptTypes: bool()

src/Parser/RichParser.php

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
use PHPStan\ShouldNotHappenException;
1313
use function array_filter;
1414
use function is_string;
15-
use function str_contains;
15+
use function strpos;
16+
use function substr;
1617
use function substr_count;
1718
use const ARRAY_FILTER_USE_KEY;
1819
use const T_COMMENT;
@@ -28,6 +29,7 @@ public function __construct(
2829
private Lexer $lexer,
2930
private NameResolver $nameResolver,
3031
private Container $container,
32+
private bool $enableIgnoreErrorsWithinPhpDocs,
3133
)
3234
{
3335
}
@@ -103,14 +105,48 @@ private function getLinesToIgnore(array $tokens): array
103105

104106
$text = $token[1];
105107
$line = $token[2];
106-
if (str_contains($text, '@phpstan-ignore-next-line')) {
107-
$line++;
108-
} elseif (!str_contains($text, '@phpstan-ignore-line')) {
109-
continue;
108+
109+
if ($this->enableIgnoreErrorsWithinPhpDocs) {
110+
$lines = $lines +
111+
$this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true) +
112+
$this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line');
113+
114+
} else {
115+
if (strpos($text, '@phpstan-ignore-next-line') !== false) {
116+
$line++;
117+
} elseif (strpos($text, '@phpstan-ignore-line') === false) {
118+
continue;
119+
}
120+
121+
$line += substr_count($token[1], "\n");
122+
$lines[$line] = null;
110123
}
124+
}
125+
126+
return $lines;
127+
}
111128

112-
$line += substr_count($token[1], "\n");
129+
/**
130+
* @return array<int, null>
131+
*/
132+
private function getLinesToIgnoreForTokenByIgnoreComment(
133+
string $tokenText,
134+
int $tokenLine,
135+
string $ignoreComment,
136+
bool $ignoreNextLine = false,
137+
): array
138+
{
139+
$lines = [];
140+
$positionsOfIgnoreComment = [];
141+
$offset = 0;
142+
143+
while (($pos = strpos($tokenText, $ignoreComment, $offset)) !== false) {
144+
$positionsOfIgnoreComment[] = $pos;
145+
$offset = $pos + 1;
146+
}
113147

148+
foreach ($positionsOfIgnoreComment as $pos) {
149+
$line = $tokenLine + substr_count(substr($tokenText, 0, $pos), "\n") + ($ignoreNextLine ? 1 : 0);
114150
$lines[$line] = null;
115151
}
116152

src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ public function processNode(Node $node, Scope $scope): array
114114
$errors[] = RuleErrorBuilder::message(sprintf(
115115
'Unknown PHPDoc tag: %s',
116116
$phpDocTag->name,
117-
))->build();
117+
))
118+
->line(PhpDocLineHelper::detectLine($node, $phpDocTag))
119+
->build();
118120
}
119121

120122
return $errors;

src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ public function processNode(Node $node, Scope $scope): array
9090
$phpDocTag->name,
9191
$phpDocTag->value->alias,
9292
$this->trimExceptionMessage($phpDocTag->value->type->getException()->getMessage()),
93-
))->build();
93+
))
94+
->line(PhpDocLineHelper::detectLine($node, $phpDocTag))
95+
->build();
9496

9597
continue;
9698
} elseif (!($phpDocTag->value instanceof InvalidTagValueNode)) {
@@ -102,7 +104,9 @@ public function processNode(Node $node, Scope $scope): array
102104
$phpDocTag->name,
103105
$phpDocTag->value->value,
104106
$this->trimExceptionMessage($phpDocTag->value->exception->getMessage()),
105-
))->build();
107+
))
108+
->line(PhpDocLineHelper::detectLine($node, $phpDocTag))
109+
->build();
106110
}
107111

108112
return $errors;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PhpParser\Node as PhpParserNode;
6+
use PHPStan\PhpDocParser\Ast\Node as PhpDocNode;
7+
8+
class PhpDocLineHelper
9+
{
10+
11+
/**
12+
* This method returns exact line of e.g. `@param` tag in PHPDoc so that it can be used for precise error reporting
13+
* - exact position is available only when bleedingEdge is enabled
14+
* - otherwise, it falls back to given node start line
15+
*/
16+
public static function detectLine(PhpParserNode $node, PhpDocNode $phpDocNode): int
17+
{
18+
$phpDocTagLine = $phpDocNode->getAttribute('startLine');
19+
$phpDoc = $node->getDocComment();
20+
21+
if ($phpDocTagLine === null || $phpDoc === null) {
22+
return $node->getLine();
23+
}
24+
25+
return $phpDoc->getStartLine() + $phpDocTagLine - 1;
26+
}
27+
28+
}

tests/PHPStan/Analyser/AnalyserTest.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void
474474
__DIR__ . '/data/ignore-next-line.php',
475475
], true);
476476
$this->assertCount($reportUnmatchedIgnoredErrors ? 4 : 3, $result);
477-
foreach ([10, 30, 34] as $i => $line) {
477+
foreach ([10, 20, 24] as $i => $line) {
478478
$this->assertArrayHasKey($i, $result);
479479
$this->assertInstanceOf(Error::class, $result[$i]);
480480
$this->assertSame('Fail.', $result[$i]->getMessage());
@@ -487,8 +487,20 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void
487487

488488
$this->assertArrayHasKey(3, $result);
489489
$this->assertInstanceOf(Error::class, $result[3]);
490-
$this->assertSame('No error to ignore is reported on line 38.', $result[3]->getMessage());
491-
$this->assertSame(38, $result[3]->getLine());
490+
$this->assertSame('No error to ignore is reported on line 28.', $result[3]->getMessage());
491+
$this->assertSame(28, $result[3]->getLine());
492+
}
493+
494+
public function testIgnoreNextLineLegacyBehaviour(): void
495+
{
496+
$result = $this->runAnalyser([], false, [__DIR__ . '/data/ignore-next-line-legacy.php'], true, false);
497+
498+
foreach ([10, 32, 36] as $i => $line) {
499+
$this->assertArrayHasKey($i, $result);
500+
$this->assertInstanceOf(Error::class, $result[$i]);
501+
$this->assertSame('Fail.', $result[$i]->getMessage());
502+
$this->assertSame($line, $result[$i]->getLine());
503+
}
492504
}
493505

494506
/**
@@ -577,9 +589,10 @@ private function runAnalyser(
577589
bool $reportUnmatchedIgnoredErrors,
578590
$filePaths,
579591
bool $onlyFiles,
592+
bool $enableIgnoreErrorsWithinPhpDocs = true,
580593
): array
581594
{
582-
$analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors);
595+
$analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors, $enableIgnoreErrorsWithinPhpDocs);
583596

584597
if (is_string($filePaths)) {
585598
$filePaths = [$filePaths];
@@ -610,7 +623,7 @@ private function runAnalyser(
610623
);
611624
}
612625

613-
private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser
626+
private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enableIgnoreErrorsWithinPhpDocs): Analyser
614627
{
615628
$ruleRegistry = new DirectRuleRegistry([
616629
new AlwaysFailRule(),
@@ -658,6 +671,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser
658671
$lexer,
659672
new NameResolver(),
660673
self::getContainer(),
674+
$enableIgnoreErrorsWithinPhpDocs,
661675
),
662676
new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper),
663677
new RuleErrorTransformer(),
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace IgnoreNextLineLegacy;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(): void
9+
{
10+
fail(); // reported
11+
12+
// @phpstan-ignore-next-line
13+
fail();
14+
15+
/* @phpstan-ignore-next-line */
16+
fail();
17+
18+
/** @phpstan-ignore-next-line */
19+
fail();
20+
21+
/*
22+
* @phpstan-ignore-next-line
23+
*/
24+
fail();
25+
26+
/**
27+
* @phpstan-ignore-next-line
28+
*
29+
* This is the legacy behaviour, the next line is meant as next-non-comment-line
30+
*/
31+
fail();
32+
fail(); // reported
33+
34+
// @phpstan-ignore-next-line
35+
if (fail()) {
36+
fail(); // reported
37+
}
38+
}
39+
40+
}

tests/PHPStan/Analyser/data/ignore-next-line.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,6 @@ public function doFoo(): void
1717

1818
/** @phpstan-ignore-next-line */
1919
fail();
20-
21-
/*
22-
* @phpstan-ignore-next-line
23-
*/
24-
fail();
25-
26-
/**
27-
* @phpstan-ignore-next-line
28-
*/
29-
fail();
3020
fail(); // reported
3121

3222
// @phpstan-ignore-next-line

0 commit comments

Comments
 (0)