From 8b6260c21bacbfd653d26b9a8abef7996fd3fe46 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 8 Jan 2024 12:24:44 +0100 Subject: [PATCH] PHPDoc tag `@phpstan-ignore-next-line` works for first line below the PHPDoc even in bleeding edge --- src/Parser/RichParser.php | 35 ++++++++++++++++--- tests/PHPStan/Analyser/AnalyserTest.php | 10 +++--- .../data/ignore-next-line-unmatched.php | 6 ++++ .../Analyser/data/ignore-next-line.php | 25 +++++++++++++ 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index d1d1be9e5b..da9b8b83a0 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -11,12 +11,17 @@ use PHPStan\File\FileReader; use PHPStan\ShouldNotHappenException; use function array_filter; +use function count; +use function implode; use function is_string; +use function preg_match_all; +use function sprintf; use function str_contains; use function strpos; use function substr; use function substr_count; use const ARRAY_FILTER_USE_KEY; +use const PREG_OFFSET_CAPTURE; use const T_COMMENT; use const T_DOC_COMMENT; @@ -25,6 +30,10 @@ class RichParser implements Parser public const VISITOR_SERVICE_TAG = 'phpstan.parser.richParserNodeVisitor'; + private const PHPDOC_TAG_REGEX = '(@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+)'; + + private const PHPDOC_DOCTRINE_TAG_REGEX = '(@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*)'; + public function __construct( private \PhpParser\Parser $parser, private Lexer $lexer, @@ -107,11 +116,27 @@ private function getLinesToIgnore(array $tokens): array $text = $token[1]; $line = $token[2]; - if ($this->enableIgnoreErrorsWithinPhpDocs) { - $lines = $lines + - $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true) + - $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line'); - + if ($this->enableIgnoreErrorsWithinPhpDocs && $type === T_DOC_COMMENT) { + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line'); + if (str_contains($text, '@phpstan-ignore-next-line')) { + $pattern = sprintf('~%s~si', implode('|', [self::PHPDOC_TAG_REGEX, self::PHPDOC_DOCTRINE_TAG_REGEX])); + $r = preg_match_all($pattern, $text, $pregMatches, PREG_OFFSET_CAPTURE); + if ($r !== false) { + $c = count($pregMatches[0]); + if ($c > 0) { + [$lastMatchTag, $lastMatchOffset] = $pregMatches[0][$c - 1]; + if ($lastMatchTag === '@phpstan-ignore-next-line') { + // this will let us ignore errors outside of PHPDoc + // and also cut off the PHPDoc text before the last tag + $lineToIgnore = $line + 1 + substr_count($text, "\n"); + $lines[$lineToIgnore] = null; + $text = substr($text, 0, $lastMatchOffset); + } + } + } + + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true); + } } else { if (str_contains($text, '@phpstan-ignore-next-line')) { $line++; diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 4adcf2f277..6d7e63f927 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -467,11 +467,11 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasN public function testIgnoreNextLine(): void { - $result = $this->runAnalyser([], true, [ + $result = $this->runAnalyser([], false, [ __DIR__ . '/data/ignore-next-line.php', ], true); - $this->assertCount(3, $result); - foreach ([10, 20, 24] as $i => $line) { + $this->assertCount(5, $result); + foreach ([10, 20, 24, 31, 50] as $i => $line) { $this->assertArrayHasKey($i, $result); $this->assertInstanceOf(Error::class, $result[$i]); $this->assertSame('Fail.', $result[$i]->getMessage()); @@ -484,8 +484,8 @@ public function testIgnoreNextLineUnmatched(): void $result = $this->runAnalyser([], true, [ __DIR__ . '/data/ignore-next-line-unmatched.php', ], true); - $this->assertCount(1, $result); - foreach ([11] as $i => $line) { + $this->assertCount(2, $result); + foreach ([11, 15] as $i => $line) { $this->assertArrayHasKey($i, $result); $this->assertInstanceOf(Error::class, $result[$i]); $this->assertStringContainsString('No error to ignore is reported on line', $result[$i]->getMessage()); diff --git a/tests/PHPStan/Analyser/data/ignore-next-line-unmatched.php b/tests/PHPStan/Analyser/data/ignore-next-line-unmatched.php index f5ef74a320..bc427537de 100644 --- a/tests/PHPStan/Analyser/data/ignore-next-line-unmatched.php +++ b/tests/PHPStan/Analyser/data/ignore-next-line-unmatched.php @@ -9,6 +9,12 @@ public function doFoo(): void { /** @phpstan-ignore-next-line */ succ(); // reported as unmatched + + /** + * @phpstan-ignore-next-line + * @var int + */ + succ(); // not reported as unmatched because phpstan-ignore-next-line is not last } } diff --git a/tests/PHPStan/Analyser/data/ignore-next-line.php b/tests/PHPStan/Analyser/data/ignore-next-line.php index 69a76be96e..602455d95d 100644 --- a/tests/PHPStan/Analyser/data/ignore-next-line.php +++ b/tests/PHPStan/Analyser/data/ignore-next-line.php @@ -23,6 +23,31 @@ public function doFoo(): void if (fail()) { fail(); // reported } + + /** + * @phpstan-ignore-next-line + */ + fail(); + fail(); // reported + + /** + * @noinspection PhpStrictTypeCheckingInspection + * @phpstan-ignore-next-line + */ + fail(); // not reported because the ignore tag is valid + + /** + * @phpstan-ignore-next-line Some very loooooooooooooooooooooooooooooooooooooooooooon + * coooooooooooooooooooooooooooooooooooooooooooooooooomment + * on many lines. + */ + fail(); // not reported because the ignore tag is valid + + /** + * @phpstan-ignore-next-line + * @var int + */ + fail(); // reported becase ignore tag in PHPDoc is not last } }