From c5ca23017beb507777565e41d9551405c398010c Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 3 Jan 2024 19:48:56 +0100 Subject: [PATCH] Bleeding Edge - PhpDocParser: add config for lines in its AST & enable ignoring errors within phpdocs --- conf/bleedingEdge.neon | 2 + conf/config.neon | 5 ++ conf/parametersSchema.neon | 2 + src/Parser/RichParser.php | 48 ++++++++++++++++--- src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 4 +- .../PhpDoc/InvalidPhpDocTagValueRule.php | 8 +++- src/Rules/PhpDoc/PhpDocLineHelper.php | 28 +++++++++++ tests/PHPStan/Analyser/AnalyserTest.php | 24 ++++++++-- .../Analyser/data/ignore-next-line-legacy.php | 40 ++++++++++++++++ .../Analyser/data/ignore-next-line.php | 10 ---- .../PhpDoc/InvalidPHPStanDocTagRuleTest.php | 10 ++-- .../PhpDoc/InvalidPhpDocTagValueRuleTest.php | 44 +++++++++-------- .../PhpDoc/data/ignore-line-within-phpdoc.php | 28 +++++++++++ 13 files changed, 205 insertions(+), 48 deletions(-) create mode 100644 src/Rules/PhpDoc/PhpDocLineHelper.php create mode 100644 tests/PHPStan/Analyser/data/ignore-next-line-legacy.php create mode 100644 tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 595a9ad95c..dab2046772 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -16,6 +16,8 @@ parameters: checkUnresolvableParameterTypes: true readOnlyByPhpDoc: true phpDocParserRequireWhitespaceBeforeDescription: true + phpDocParserIncludeLines: true + enableIgnoreErrorsWithinPhpDocs: true runtimeReflectionRules: true notAnalysedTrait: true curlSetOptTypes: true diff --git a/conf/config.neon b/conf/config.neon index bce725068d..1a4a35e444 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -51,6 +51,8 @@ parameters: checkUnresolvableParameterTypes: false readOnlyByPhpDoc: false phpDocParserRequireWhitespaceBeforeDescription: false + phpDocParserIncludeLines: false + enableIgnoreErrorsWithinPhpDocs: false runtimeReflectionRules: false notAnalysedTrait: false curlSetOptTypes: false @@ -389,6 +391,8 @@ services: arguments: requireWhitespaceBeforeDescription: %featureToggles.phpDocParserRequireWhitespaceBeforeDescription% preserveTypeAliasesWithInvalidTypes: true + usedAttributes: + lines: %featureToggles.phpDocParserIncludeLines% - class: PHPStan\PhpDoc\ConstExprParserFactory @@ -1824,6 +1828,7 @@ services: arguments: parser: @currentPhpVersionPhpParser lexer: @currentPhpVersionLexer + enableIgnoreErrorsWithinPhpDocs: %featureToggles.enableIgnoreErrorsWithinPhpDocs% autowired: no currentPhpVersionSimpleParser: diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 54e35d1130..a929614b64 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -46,6 +46,8 @@ parametersSchema: checkUnresolvableParameterTypes: bool() readOnlyByPhpDoc: bool() phpDocParserRequireWhitespaceBeforeDescription: bool() + phpDocParserIncludeLines: bool() + enableIgnoreErrorsWithinPhpDocs: bool() runtimeReflectionRules: bool() notAnalysedTrait: bool() curlSetOptTypes: bool() diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 92fe2893c2..710dd120fc 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -12,7 +12,8 @@ use PHPStan\ShouldNotHappenException; use function array_filter; use function is_string; -use function str_contains; +use function strpos; +use function substr; use function substr_count; use const ARRAY_FILTER_USE_KEY; use const T_COMMENT; @@ -28,6 +29,7 @@ public function __construct( private Lexer $lexer, private NameResolver $nameResolver, private Container $container, + private bool $enableIgnoreErrorsWithinPhpDocs, ) { } @@ -103,14 +105,48 @@ private function getLinesToIgnore(array $tokens): array $text = $token[1]; $line = $token[2]; - if (str_contains($text, '@phpstan-ignore-next-line')) { - $line++; - } elseif (!str_contains($text, '@phpstan-ignore-line')) { - continue; + + if ($this->enableIgnoreErrorsWithinPhpDocs) { + $lines = $lines + + $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-next-line', true) + + $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line'); + + } else { + if (strpos($text, '@phpstan-ignore-next-line') !== false) { + $line++; + } elseif (strpos($text, '@phpstan-ignore-line') === false) { + continue; + } + + $line += substr_count($token[1], "\n"); + $lines[$line] = null; } + } + + return $lines; + } - $line += substr_count($token[1], "\n"); + /** + * @return array + */ + private function getLinesToIgnoreForTokenByIgnoreComment( + string $tokenText, + int $tokenLine, + string $ignoreComment, + bool $ignoreNextLine = false, + ): array + { + $lines = []; + $positionsOfIgnoreComment = []; + $offset = 0; + + while (($pos = strpos($tokenText, $ignoreComment, $offset)) !== false) { + $positionsOfIgnoreComment[] = $pos; + $offset = $pos + 1; + } + foreach ($positionsOfIgnoreComment as $pos) { + $line = $tokenLine + substr_count(substr($tokenText, 0, $pos), "\n") + ($ignoreNextLine ? 1 : 0); $lines[$line] = null; } diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index d4b5b66f85..573e018ca3 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -114,7 +114,9 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Unknown PHPDoc tag: %s', $phpDocTag->name, - ))->build(); + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->build(); } return $errors; diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index 06abe3fd09..d17efa9b15 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -90,7 +90,9 @@ public function processNode(Node $node, Scope $scope): array $phpDocTag->name, $phpDocTag->value->alias, $this->trimExceptionMessage($phpDocTag->value->type->getException()->getMessage()), - ))->build(); + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->build(); continue; } elseif (!($phpDocTag->value instanceof InvalidTagValueNode)) { @@ -102,7 +104,9 @@ public function processNode(Node $node, Scope $scope): array $phpDocTag->name, $phpDocTag->value->value, $this->trimExceptionMessage($phpDocTag->value->exception->getMessage()), - ))->build(); + )) + ->line(PhpDocLineHelper::detectLine($node, $phpDocTag)) + ->build(); } return $errors; diff --git a/src/Rules/PhpDoc/PhpDocLineHelper.php b/src/Rules/PhpDoc/PhpDocLineHelper.php new file mode 100644 index 0000000000..d1d4b52e8b --- /dev/null +++ b/src/Rules/PhpDoc/PhpDocLineHelper.php @@ -0,0 +1,28 @@ +getAttribute('startLine'); + $phpDoc = $node->getDocComment(); + + if ($phpDocTagLine === null || $phpDoc === null) { + return $node->getLine(); + } + + return $phpDoc->getStartLine() + $phpDocTagLine - 1; + } + +} diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index ef1ee421c3..d70ce00938 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -474,7 +474,7 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void __DIR__ . '/data/ignore-next-line.php', ], true); $this->assertCount($reportUnmatchedIgnoredErrors ? 4 : 3, $result); - foreach ([10, 30, 34] as $i => $line) { + foreach ([10, 20, 24] as $i => $line) { $this->assertArrayHasKey($i, $result); $this->assertInstanceOf(Error::class, $result[$i]); $this->assertSame('Fail.', $result[$i]->getMessage()); @@ -487,8 +487,20 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void $this->assertArrayHasKey(3, $result); $this->assertInstanceOf(Error::class, $result[3]); - $this->assertSame('No error to ignore is reported on line 38.', $result[3]->getMessage()); - $this->assertSame(38, $result[3]->getLine()); + $this->assertSame('No error to ignore is reported on line 28.', $result[3]->getMessage()); + $this->assertSame(28, $result[3]->getLine()); + } + + public function testIgnoreNextLineLegacyBehaviour(): void + { + $result = $this->runAnalyser([], false, [__DIR__ . '/data/ignore-next-line-legacy.php'], true, false); + + foreach ([10, 32, 36] as $i => $line) { + $this->assertArrayHasKey($i, $result); + $this->assertInstanceOf(Error::class, $result[$i]); + $this->assertSame('Fail.', $result[$i]->getMessage()); + $this->assertSame($line, $result[$i]->getLine()); + } } /** @@ -577,9 +589,10 @@ private function runAnalyser( bool $reportUnmatchedIgnoredErrors, $filePaths, bool $onlyFiles, + bool $enableIgnoreErrorsWithinPhpDocs = true, ): array { - $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors); + $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors, $enableIgnoreErrorsWithinPhpDocs); if (is_string($filePaths)) { $filePaths = [$filePaths]; @@ -610,7 +623,7 @@ private function runAnalyser( ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser + private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enableIgnoreErrorsWithinPhpDocs): Analyser { $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), @@ -658,6 +671,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser $lexer, new NameResolver(), self::getContainer(), + $enableIgnoreErrorsWithinPhpDocs, ), new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), new RuleErrorTransformer(), diff --git a/tests/PHPStan/Analyser/data/ignore-next-line-legacy.php b/tests/PHPStan/Analyser/data/ignore-next-line-legacy.php new file mode 100644 index 0000000000..fa8f2a50fd --- /dev/null +++ b/tests/PHPStan/Analyser/data/ignore-next-line-legacy.php @@ -0,0 +1,40 @@ +analyse([__DIR__ . '/data/invalid-type-type-alias.php'], [ [ 'PHPDoc tag @phpstan-type InvalidFoo has invalid value: Unexpected token "{", expected TOKEN_PHPDOC_EOL at offset 65 on line 3', - 12, + 7, ], ]); } + public function testIgnoreWithinPhpDoc(): void + { + $this->checkAllInvalidPhpDocs = true; + $this->analyse([__DIR__ . '/data/ignore-line-within-phpdoc.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php new file mode 100644 index 0000000000..13b81b9f00 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/ignore-line-within-phpdoc.php @@ -0,0 +1,28 @@ +