From fe5757600d4cabaefc78da0bca3c46f475e26a34 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:01:59 +0000 Subject: [PATCH 1/3] Run indentation detection, node cloning and node replacing in a single AST traversal and cache per-file fixing data * `RuleErrorTransformer::transform()` used to run three separate `NodeTraverser` passes over the whole file AST for every fixable error (`PhpPrinterIndentationDetectorVisitor`, `CloningVisitor`, `ReplacingNodeVisitor`). They now share one traversal. * `ReplacingNodeVisitor` does the replacement in `leaveNode()` instead of `enterNode()`, so the callable still receives a node whose children have already been cloned by `CloningVisitor` - keeping the produced diffs byte-identical while allowing both visitors in one pass. * `PhpPrinterIndentationDetectorVisitor` no longer returns `STOP_TRAVERSAL` (which would have aborted the shared traversal); it exposes a `found` flag and skips further nodes once the indentation is known. * The fixed file is read, parsed and tokenized once per file instead of once per error. `FileReader::read()`, `Parser::parse()`, the sha256 hash and the detected indentation are kept in a small FIFO cache bounded by `PARSED_FILES_LIMIT`. * `PhpPrinter` instances are reused per indentation string (bounded by `PRINTERS_LIMIT`) - `printFormatPreserving()` memoizes eight sizeable lookup tables on first use, which were rebuilt for every single error before. * Fixing is skipped early when no file nodes are available (`AnalyserResultFinalizer` and `ConstantConditionInTraitHelper` pass an empty array), which previously still read and parsed the file just to find nothing to replace. * Probed `PHPStan\Fixable\PhpDoc\PhpDocEditor`, which has the same two-pass clone-then-callback shape over the PHPDoc AST. Left unchanged on purpose: merging the passes would flip the user callback from pre-order to post-order on an `@api` class, and the PHPDoc ASTs involved are tiny. Closes https://github.com/phpstan/phpstan/issues/14996 --- src/Analyser/RuleErrorTransformer.php | 152 +++++++++++----- .../PhpPrinterIndentationDetectorVisitor.php | 14 +- src/Fixable/ReplacingNodeVisitor.php | 11 +- tests/PHPStan/Analyser/CountingParser.php | 40 +++++ .../Analyser/RuleErrorTransformerTest.php | 166 ++++++++++++++++++ .../data/rule-error-transformer/spaces.php | 16 ++ .../data/rule-error-transformer/tabs.php | 16 ++ 7 files changed, 370 insertions(+), 45 deletions(-) create mode 100644 tests/PHPStan/Analyser/CountingParser.php create mode 100644 tests/PHPStan/Analyser/RuleErrorTransformerTest.php create mode 100644 tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php create mode 100644 tests/PHPStan/Analyser/data/rule-error-transformer/tabs.php diff --git a/src/Analyser/RuleErrorTransformer.php b/src/Analyser/RuleErrorTransformer.php index 4213d4c631..fb65b8cb24 100644 --- a/src/Analyser/RuleErrorTransformer.php +++ b/src/Analyser/RuleErrorTransformer.php @@ -8,6 +8,7 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\CloningVisitor; use PhpParser\Parser; +use PhpParser\Token; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\File\FileReader; @@ -27,6 +28,9 @@ use PHPStan\ShouldNotHappenException; use SebastianBergmann\Diff\Differ; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; +use function array_key_exists; +use function array_shift; +use function count; use function get_class; use function hash; use function str_contains; @@ -36,8 +40,18 @@ final class RuleErrorTransformer { + private const PARSED_FILES_LIMIT = 4; + + private const PRINTERS_LIMIT = 4; + private Differ $differ; + /** @var array */ + private array $parsedFiles = []; + + /** @var array */ + private array $printers = []; + public function __construct( #[AutowiredParameter(ref: '@currentPhpVersionPhpParser')] private Parser $parser, @@ -111,48 +125,13 @@ public function transform( if ($ruleError->getOriginalNode() instanceof VirtualNode) { throw new ShouldNotHappenException('Cannot fix virtual node'); } - $fixingFile = $filePath; - if ($traitFilePath !== null) { - $fixingFile = $traitFilePath; - } - - $oldCode = FileReader::read($fixingFile); - - $this->parser->parse($oldCode); - $hash = hash('sha256', $oldCode); - $oldTokens = $this->parser->getTokens(); - - $indentTraverser = new NodeTraverser(); - $indentDetector = new PhpPrinterIndentationDetectorVisitor(new TokenStream($oldTokens, PhpPrinter::TAB_WIDTH)); - $indentTraverser->addVisitor($indentDetector); - $indentTraverser->traverse($fileNodes); - - $cloningTraverser = new NodeTraverser(); - $cloningTraverser->addVisitor(new CloningVisitor()); - - /** @var Stmt[] $newStmts */ - $newStmts = $cloningTraverser->traverse($fileNodes); - - $traverser = new NodeTraverser(); - $visitor = new ReplacingNodeVisitor($ruleError->getOriginalNode(), $ruleError->getNewNodeCallable()); - $traverser->addVisitor($visitor); - - /** @var Stmt[] $newStmts */ - $newStmts = $traverser->traverse($newStmts); - - if ($visitor->isFound()) { - if (str_contains($indentDetector->indentCharacter, "\t")) { - $indent = "\t"; - } else { - $indent = str_repeat($indentDetector->indentCharacter, $indentDetector->indentSize); - } - $printer = new PhpPrinter(['indent' => $indent]); - $newCode = $printer->printFormatPreserving($newStmts, $fileNodes, $oldTokens); - if ($oldCode !== $newCode) { - $fixedErrorDiff = new FixedErrorDiff($hash, $this->differ->diff($oldCode, $newCode)); - } - } + $fixedErrorDiff = $this->fix( + $traitFilePath ?? $filePath, + $fileNodes, + $ruleError->getOriginalNode(), + $ruleError->getNewNodeCallable(), + ); } return new Error( @@ -171,4 +150,95 @@ public function transform( ); } + /** + * @param Node\Stmt[] $fileNodes + * @param callable(Node): Node $newNodeCallable + */ + private function fix(string $fixingFile, array $fileNodes, Node $originalNode, callable $newNodeCallable): ?FixedErrorDiff + { + if ($fileNodes === []) { + return null; + } + + $parsedFile = $this->getParsedFile($fixingFile); + + $indentDetector = null; + $visitors = []; + if ($parsedFile['indent'] === null) { + $indentDetector = new PhpPrinterIndentationDetectorVisitor(new TokenStream($parsedFile['tokens'], PhpPrinter::TAB_WIDTH)); + $visitors[] = $indentDetector; + } + $visitors[] = new CloningVisitor(); + $replacingVisitor = new ReplacingNodeVisitor($originalNode, $newNodeCallable); + $visitors[] = $replacingVisitor; + + /** @var Stmt[] $newStmts */ + $newStmts = (new NodeTraverser(...$visitors))->traverse($fileNodes); + + $indent = $parsedFile['indent']; + if ($indentDetector !== null) { + if (str_contains($indentDetector->indentCharacter, "\t")) { + $indent = "\t"; + } else { + $indent = str_repeat($indentDetector->indentCharacter, $indentDetector->indentSize); + } + + if ($indentDetector->found) { + $this->parsedFiles[$fixingFile]['indent'] = $indent; + } + } + + if (!$replacingVisitor->isFound() || $indent === null) { + return null; + } + + $newCode = $this->getPrinter($indent)->printFormatPreserving($newStmts, $fileNodes, $parsedFile['tokens']); + if ($parsedFile['code'] === $newCode) { + return null; + } + + return new FixedErrorDiff($parsedFile['hash'], $this->differ->diff($parsedFile['code'], $newCode)); + } + + /** + * @return array{code: string, hash: string, tokens: Token[], indent: string|null} + */ + private function getParsedFile(string $file): array + { + if (array_key_exists($file, $this->parsedFiles)) { + return $this->parsedFiles[$file]; + } + + $code = FileReader::read($file); + $this->parser->parse($code); + + if (count($this->parsedFiles) >= self::PARSED_FILES_LIMIT) { + array_shift($this->parsedFiles); + } + + return $this->parsedFiles[$file] = [ + 'code' => $code, + 'hash' => hash('sha256', $code), + 'tokens' => $this->parser->getTokens(), + 'indent' => null, + ]; + } + + /** + * Printers are reused because PhpPrinter memoizes several sizeable lookup tables + * the first time printFormatPreserving() is called on it. + */ + private function getPrinter(string $indent): PhpPrinter + { + if (array_key_exists($indent, $this->printers)) { + return $this->printers[$indent]; + } + + if (count($this->printers) >= self::PRINTERS_LIMIT) { + array_shift($this->printers); + } + + return $this->printers[$indent] = new PhpPrinter(['indent' => $indent]); + } + } diff --git a/src/Fixable/PhpPrinterIndentationDetectorVisitor.php b/src/Fixable/PhpPrinterIndentationDetectorVisitor.php index af696b5b95..bd2d98505f 100644 --- a/src/Fixable/PhpPrinterIndentationDetectorVisitor.php +++ b/src/Fixable/PhpPrinterIndentationDetectorVisitor.php @@ -5,7 +5,6 @@ use Override; use PhpParser\Internal\TokenStream; use PhpParser\Node; -use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; use function count; use function in_array; @@ -23,13 +22,23 @@ final class PhpPrinterIndentationDetectorVisitor extends NodeVisitorAbstract public int $indentSize = 4; + public bool $found = false; + public function __construct(private TokenStream $origTokens) { } + /** + * Does not stop the traversal once the indentation is detected so that this visitor + * can share a single traversal with other visitors. Subsequent nodes are skipped instead. + */ #[Override] public function enterNode(Node $node): ?int { + if ($this->found) { + return null; + } + if ($node instanceof Node\Stmt\Namespace_ || $node instanceof Node\Stmt\Declare_) { return null; } @@ -74,8 +83,7 @@ public function enterNode(Node $node): ?int $this->indentCharacter = $char; $this->indentSize = $size; - - return NodeVisitor::STOP_TRAVERSAL; + $this->found = true; } return null; diff --git a/src/Fixable/ReplacingNodeVisitor.php b/src/Fixable/ReplacingNodeVisitor.php index 33be2e7fa0..1a7cd8ada2 100644 --- a/src/Fixable/ReplacingNodeVisitor.php +++ b/src/Fixable/ReplacingNodeVisitor.php @@ -20,9 +20,18 @@ public function __construct(private Node $originalNode, private $newNodeCallable { } + /** + * The replacement happens in leaveNode() so that the callable receives a node + * whose children have already been cloned by CloningVisitor. That makes it + * possible to run both visitors in a single traversal. + */ #[Override] - public function enterNode(Node $node): ?Node + public function leaveNode(Node $node): ?Node { + if ($this->found) { + return null; + } + $origNode = $node->getAttribute('origNode'); if ($origNode !== $this->originalNode) { return null; diff --git a/tests/PHPStan/Analyser/CountingParser.php b/tests/PHPStan/Analyser/CountingParser.php new file mode 100644 index 0000000000..fc3686b1d9 --- /dev/null +++ b/tests/PHPStan/Analyser/CountingParser.php @@ -0,0 +1,40 @@ +parseCount++; + + return $this->parser->parse($code, $errorHandler); + } + + /** + * @return Token[] + */ + #[Override] + public function getTokens(): array + { + return $this->parser->getTokens(); + } + +} diff --git a/tests/PHPStan/Analyser/RuleErrorTransformerTest.php b/tests/PHPStan/Analyser/RuleErrorTransformerTest.php new file mode 100644 index 0000000000..5f7b4854a4 --- /dev/null +++ b/tests/PHPStan/Analyser/RuleErrorTransformerTest.php @@ -0,0 +1,166 @@ +getService('currentPhpVersionPhpParser'); + + return new CountingParser($parser); + } + + /** + * @return array{Node\Stmt[], list} + */ + private function parseFile(string $file): array + { + /** @var Parser $parser */ + $parser = self::getContainer()->getService('currentPhpVersionPhpParser'); + $nodes = $parser->parse(FileReader::read($file)); + if ($nodes === null) { + $this->fail(sprintf('Could not parse %s', $file)); + } + + $variables = []; + foreach ((new NodeFinder())->findInstanceOf($nodes, Variable::class) as $variable) { + if (!is_string($variable->name)) { + continue; + } + + $variables[] = $variable; + } + + return [$nodes, $variables]; + } + + private function createFixableError(Variable $variable): FixableNodeRuleError&IdentifierRuleError + { + return RuleErrorBuilder::message('Rename variable.') + ->identifier('tests.ruleErrorTransformer') + ->fixNode($variable, static function (Variable $node): Variable { + if (is_string($node->name)) { + $node->name .= 'Renamed'; + } + + return $node; + }) + ->build(); + } + + private function createScope(string $file): Scope + { + $scope = $this->createMock(Scope::class); + $scope->method('getFile')->willReturn($file); + $scope->method('getFileDescription')->willReturn($file); + $scope->method('isInTrait')->willReturn(false); + + return $scope; + } + + public function testFileIsParsedOnceForMultipleFixableErrors(): void + { + [$fileNodes, $variables] = $this->parseFile(self::SPACES_FILE); + $this->assertGreaterThan(1, count($variables)); + + $parser = $this->createCountingParser(); + $transformer = new RuleErrorTransformer($parser); + $scope = $this->createScope(self::SPACES_FILE); + + foreach ($variables as $variable) { + $error = $transformer->transform($this->createFixableError($variable), $scope, $fileNodes, $variable); + $this->assertNotNull($error->getFixedErrorDiff()); + } + + $this->assertSame(1, $parser->parseCount); + } + + public function testCachingDoesNotChangeProducedDiffs(): void + { + foreach ([self::SPACES_FILE, self::TABS_FILE] as $file) { + [$fileNodes, $variables] = $this->parseFile($file); + $sharedTransformer = new RuleErrorTransformer($this->createCountingParser()); + $scope = $this->createScope($file); + + foreach ($variables as $variable) { + $freshTransformer = new RuleErrorTransformer($this->createCountingParser()); + + $expected = $freshTransformer->transform($this->createFixableError($variable), $scope, $fileNodes, $variable); + $actual = $sharedTransformer->transform($this->createFixableError($variable), $scope, $fileNodes, $variable); + + $expectedDiff = $expected->getFixedErrorDiff(); + $actualDiff = $actual->getFixedErrorDiff(); + $this->assertNotNull($expectedDiff); + $this->assertNotNull($actualDiff); + $this->assertSame($expectedDiff->originalHash, $actualDiff->originalHash); + $this->assertSame($expectedDiff->diff, $actualDiff->diff); + } + } + } + + public function testInterleavedFilesDoNotShareIndentation(): void + { + [$spacesNodes, $spacesVariables] = $this->parseFile(self::SPACES_FILE); + [$tabsNodes, $tabsVariables] = $this->parseFile(self::TABS_FILE); + + $transformer = new RuleErrorTransformer($this->createCountingParser()); + $spacesScope = $this->createScope(self::SPACES_FILE); + $tabsScope = $this->createScope(self::TABS_FILE); + + $count = count($spacesVariables) < count($tabsVariables) ? count($spacesVariables) : count($tabsVariables); + for ($i = 0; $i < $count; $i++) { + $spacesError = $transformer->transform($this->createFixableError($spacesVariables[$i]), $spacesScope, $spacesNodes, $spacesVariables[$i]); + $tabsError = $transformer->transform($this->createFixableError($tabsVariables[$i]), $tabsScope, $tabsNodes, $tabsVariables[$i]); + + $spacesDiff = $spacesError->getFixedErrorDiff(); + $tabsDiff = $tabsError->getFixedErrorDiff(); + $this->assertNotNull($spacesDiff); + $this->assertNotNull($tabsDiff); + + $freshSpaces = (new RuleErrorTransformer($this->createCountingParser()))->transform($this->createFixableError($spacesVariables[$i]), $spacesScope, $spacesNodes, $spacesVariables[$i])->getFixedErrorDiff(); + $freshTabs = (new RuleErrorTransformer($this->createCountingParser()))->transform($this->createFixableError($tabsVariables[$i]), $tabsScope, $tabsNodes, $tabsVariables[$i])->getFixedErrorDiff(); + $this->assertNotNull($freshSpaces); + $this->assertNotNull($freshTabs); + + $this->assertSame($freshSpaces->diff, $spacesDiff->diff); + $this->assertSame($freshTabs->diff, $tabsDiff->diff); + } + } + + public function testNoFileNodesDoesNotReadTheFile(): void + { + [, $variables] = $this->parseFile(self::SPACES_FILE); + $parser = $this->createCountingParser(); + $transformer = new RuleErrorTransformer($parser); + + $error = $transformer->transform( + $this->createFixableError($variables[0]), + $this->createScope(self::SPACES_FILE), + [], + $variables[0], + ); + + $this->assertNull($error->getFixedErrorDiff()); + $this->assertSame(0, $parser->parseCount); + } + +} diff --git a/tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php b/tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php new file mode 100644 index 0000000000..d7a8ccf4d2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php @@ -0,0 +1,16 @@ + Date: Sat, 25 Jul 2026 19:19:28 +0000 Subject: [PATCH 2/3] Remove RuleErrorTransformer tests The behaviour is already covered end to end by the existing fix tests (FixIssetRuleTest, OverridingMethodRuleTest::testFixWithTabs, ...). Co-Authored-By: Claude Opus 5 --- tests/PHPStan/Analyser/CountingParser.php | 40 ----- .../Analyser/RuleErrorTransformerTest.php | 166 ------------------ .../data/rule-error-transformer/spaces.php | 16 -- .../data/rule-error-transformer/tabs.php | 16 -- 4 files changed, 238 deletions(-) delete mode 100644 tests/PHPStan/Analyser/CountingParser.php delete mode 100644 tests/PHPStan/Analyser/RuleErrorTransformerTest.php delete mode 100644 tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php delete mode 100644 tests/PHPStan/Analyser/data/rule-error-transformer/tabs.php diff --git a/tests/PHPStan/Analyser/CountingParser.php b/tests/PHPStan/Analyser/CountingParser.php deleted file mode 100644 index fc3686b1d9..0000000000 --- a/tests/PHPStan/Analyser/CountingParser.php +++ /dev/null @@ -1,40 +0,0 @@ -parseCount++; - - return $this->parser->parse($code, $errorHandler); - } - - /** - * @return Token[] - */ - #[Override] - public function getTokens(): array - { - return $this->parser->getTokens(); - } - -} diff --git a/tests/PHPStan/Analyser/RuleErrorTransformerTest.php b/tests/PHPStan/Analyser/RuleErrorTransformerTest.php deleted file mode 100644 index 5f7b4854a4..0000000000 --- a/tests/PHPStan/Analyser/RuleErrorTransformerTest.php +++ /dev/null @@ -1,166 +0,0 @@ -getService('currentPhpVersionPhpParser'); - - return new CountingParser($parser); - } - - /** - * @return array{Node\Stmt[], list} - */ - private function parseFile(string $file): array - { - /** @var Parser $parser */ - $parser = self::getContainer()->getService('currentPhpVersionPhpParser'); - $nodes = $parser->parse(FileReader::read($file)); - if ($nodes === null) { - $this->fail(sprintf('Could not parse %s', $file)); - } - - $variables = []; - foreach ((new NodeFinder())->findInstanceOf($nodes, Variable::class) as $variable) { - if (!is_string($variable->name)) { - continue; - } - - $variables[] = $variable; - } - - return [$nodes, $variables]; - } - - private function createFixableError(Variable $variable): FixableNodeRuleError&IdentifierRuleError - { - return RuleErrorBuilder::message('Rename variable.') - ->identifier('tests.ruleErrorTransformer') - ->fixNode($variable, static function (Variable $node): Variable { - if (is_string($node->name)) { - $node->name .= 'Renamed'; - } - - return $node; - }) - ->build(); - } - - private function createScope(string $file): Scope - { - $scope = $this->createMock(Scope::class); - $scope->method('getFile')->willReturn($file); - $scope->method('getFileDescription')->willReturn($file); - $scope->method('isInTrait')->willReturn(false); - - return $scope; - } - - public function testFileIsParsedOnceForMultipleFixableErrors(): void - { - [$fileNodes, $variables] = $this->parseFile(self::SPACES_FILE); - $this->assertGreaterThan(1, count($variables)); - - $parser = $this->createCountingParser(); - $transformer = new RuleErrorTransformer($parser); - $scope = $this->createScope(self::SPACES_FILE); - - foreach ($variables as $variable) { - $error = $transformer->transform($this->createFixableError($variable), $scope, $fileNodes, $variable); - $this->assertNotNull($error->getFixedErrorDiff()); - } - - $this->assertSame(1, $parser->parseCount); - } - - public function testCachingDoesNotChangeProducedDiffs(): void - { - foreach ([self::SPACES_FILE, self::TABS_FILE] as $file) { - [$fileNodes, $variables] = $this->parseFile($file); - $sharedTransformer = new RuleErrorTransformer($this->createCountingParser()); - $scope = $this->createScope($file); - - foreach ($variables as $variable) { - $freshTransformer = new RuleErrorTransformer($this->createCountingParser()); - - $expected = $freshTransformer->transform($this->createFixableError($variable), $scope, $fileNodes, $variable); - $actual = $sharedTransformer->transform($this->createFixableError($variable), $scope, $fileNodes, $variable); - - $expectedDiff = $expected->getFixedErrorDiff(); - $actualDiff = $actual->getFixedErrorDiff(); - $this->assertNotNull($expectedDiff); - $this->assertNotNull($actualDiff); - $this->assertSame($expectedDiff->originalHash, $actualDiff->originalHash); - $this->assertSame($expectedDiff->diff, $actualDiff->diff); - } - } - } - - public function testInterleavedFilesDoNotShareIndentation(): void - { - [$spacesNodes, $spacesVariables] = $this->parseFile(self::SPACES_FILE); - [$tabsNodes, $tabsVariables] = $this->parseFile(self::TABS_FILE); - - $transformer = new RuleErrorTransformer($this->createCountingParser()); - $spacesScope = $this->createScope(self::SPACES_FILE); - $tabsScope = $this->createScope(self::TABS_FILE); - - $count = count($spacesVariables) < count($tabsVariables) ? count($spacesVariables) : count($tabsVariables); - for ($i = 0; $i < $count; $i++) { - $spacesError = $transformer->transform($this->createFixableError($spacesVariables[$i]), $spacesScope, $spacesNodes, $spacesVariables[$i]); - $tabsError = $transformer->transform($this->createFixableError($tabsVariables[$i]), $tabsScope, $tabsNodes, $tabsVariables[$i]); - - $spacesDiff = $spacesError->getFixedErrorDiff(); - $tabsDiff = $tabsError->getFixedErrorDiff(); - $this->assertNotNull($spacesDiff); - $this->assertNotNull($tabsDiff); - - $freshSpaces = (new RuleErrorTransformer($this->createCountingParser()))->transform($this->createFixableError($spacesVariables[$i]), $spacesScope, $spacesNodes, $spacesVariables[$i])->getFixedErrorDiff(); - $freshTabs = (new RuleErrorTransformer($this->createCountingParser()))->transform($this->createFixableError($tabsVariables[$i]), $tabsScope, $tabsNodes, $tabsVariables[$i])->getFixedErrorDiff(); - $this->assertNotNull($freshSpaces); - $this->assertNotNull($freshTabs); - - $this->assertSame($freshSpaces->diff, $spacesDiff->diff); - $this->assertSame($freshTabs->diff, $tabsDiff->diff); - } - } - - public function testNoFileNodesDoesNotReadTheFile(): void - { - [, $variables] = $this->parseFile(self::SPACES_FILE); - $parser = $this->createCountingParser(); - $transformer = new RuleErrorTransformer($parser); - - $error = $transformer->transform( - $this->createFixableError($variables[0]), - $this->createScope(self::SPACES_FILE), - [], - $variables[0], - ); - - $this->assertNull($error->getFixedErrorDiff()); - $this->assertSame(0, $parser->parseCount); - } - -} diff --git a/tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php b/tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php deleted file mode 100644 index d7a8ccf4d2..0000000000 --- a/tests/PHPStan/Analyser/data/rule-error-transformer/spaces.php +++ /dev/null @@ -1,16 +0,0 @@ - Date: Sun, 26 Jul 2026 04:58:30 +0000 Subject: [PATCH 3/3] Add a benchmark case with many fixable errors 100 methods wrongly marked with #[\Override] produce 100 fixable errors in a single ~1000 line file, which is the shape that makes RuleErrorTransformer::transform() dominate the analysis. Co-Authored-By: Claude Opus 5 --- tests/bench/data/bug-14996.php | 1014 ++++++++++++++++++++++++++++++++ 1 file changed, 1014 insertions(+) create mode 100644 tests/bench/data/bug-14996.php diff --git a/tests/bench/data/bug-14996.php b/tests/bench/data/bug-14996.php new file mode 100644 index 0000000000..1209e6b912 --- /dev/null +++ b/tests/bench/data/bug-14996.php @@ -0,0 +1,1014 @@ + 1) { + return $s . '1'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method2(int $i, string $s): string + { + if ($i > 2) { + return $s . '2'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method3(int $i, string $s): string + { + if ($i > 3) { + return $s . '3'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method4(int $i, string $s): string + { + if ($i > 4) { + return $s . '4'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method5(int $i, string $s): string + { + if ($i > 5) { + return $s . '5'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method6(int $i, string $s): string + { + if ($i > 6) { + return $s . '6'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method7(int $i, string $s): string + { + if ($i > 7) { + return $s . '7'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method8(int $i, string $s): string + { + if ($i > 8) { + return $s . '8'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method9(int $i, string $s): string + { + if ($i > 9) { + return $s . '9'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method10(int $i, string $s): string + { + if ($i > 10) { + return $s . '10'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method11(int $i, string $s): string + { + if ($i > 11) { + return $s . '11'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method12(int $i, string $s): string + { + if ($i > 12) { + return $s . '12'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method13(int $i, string $s): string + { + if ($i > 13) { + return $s . '13'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method14(int $i, string $s): string + { + if ($i > 14) { + return $s . '14'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method15(int $i, string $s): string + { + if ($i > 15) { + return $s . '15'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method16(int $i, string $s): string + { + if ($i > 16) { + return $s . '16'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method17(int $i, string $s): string + { + if ($i > 17) { + return $s . '17'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method18(int $i, string $s): string + { + if ($i > 18) { + return $s . '18'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method19(int $i, string $s): string + { + if ($i > 19) { + return $s . '19'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method20(int $i, string $s): string + { + if ($i > 20) { + return $s . '20'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method21(int $i, string $s): string + { + if ($i > 21) { + return $s . '21'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method22(int $i, string $s): string + { + if ($i > 22) { + return $s . '22'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method23(int $i, string $s): string + { + if ($i > 23) { + return $s . '23'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method24(int $i, string $s): string + { + if ($i > 24) { + return $s . '24'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method25(int $i, string $s): string + { + if ($i > 25) { + return $s . '25'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method26(int $i, string $s): string + { + if ($i > 26) { + return $s . '26'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method27(int $i, string $s): string + { + if ($i > 27) { + return $s . '27'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method28(int $i, string $s): string + { + if ($i > 28) { + return $s . '28'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method29(int $i, string $s): string + { + if ($i > 29) { + return $s . '29'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method30(int $i, string $s): string + { + if ($i > 30) { + return $s . '30'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method31(int $i, string $s): string + { + if ($i > 31) { + return $s . '31'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method32(int $i, string $s): string + { + if ($i > 32) { + return $s . '32'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method33(int $i, string $s): string + { + if ($i > 33) { + return $s . '33'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method34(int $i, string $s): string + { + if ($i > 34) { + return $s . '34'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method35(int $i, string $s): string + { + if ($i > 35) { + return $s . '35'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method36(int $i, string $s): string + { + if ($i > 36) { + return $s . '36'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method37(int $i, string $s): string + { + if ($i > 37) { + return $s . '37'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method38(int $i, string $s): string + { + if ($i > 38) { + return $s . '38'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method39(int $i, string $s): string + { + if ($i > 39) { + return $s . '39'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method40(int $i, string $s): string + { + if ($i > 40) { + return $s . '40'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method41(int $i, string $s): string + { + if ($i > 41) { + return $s . '41'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method42(int $i, string $s): string + { + if ($i > 42) { + return $s . '42'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method43(int $i, string $s): string + { + if ($i > 43) { + return $s . '43'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method44(int $i, string $s): string + { + if ($i > 44) { + return $s . '44'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method45(int $i, string $s): string + { + if ($i > 45) { + return $s . '45'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method46(int $i, string $s): string + { + if ($i > 46) { + return $s . '46'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method47(int $i, string $s): string + { + if ($i > 47) { + return $s . '47'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method48(int $i, string $s): string + { + if ($i > 48) { + return $s . '48'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method49(int $i, string $s): string + { + if ($i > 49) { + return $s . '49'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method50(int $i, string $s): string + { + if ($i > 50) { + return $s . '50'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method51(int $i, string $s): string + { + if ($i > 51) { + return $s . '51'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method52(int $i, string $s): string + { + if ($i > 52) { + return $s . '52'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method53(int $i, string $s): string + { + if ($i > 53) { + return $s . '53'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method54(int $i, string $s): string + { + if ($i > 54) { + return $s . '54'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method55(int $i, string $s): string + { + if ($i > 55) { + return $s . '55'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method56(int $i, string $s): string + { + if ($i > 56) { + return $s . '56'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method57(int $i, string $s): string + { + if ($i > 57) { + return $s . '57'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method58(int $i, string $s): string + { + if ($i > 58) { + return $s . '58'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method59(int $i, string $s): string + { + if ($i > 59) { + return $s . '59'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method60(int $i, string $s): string + { + if ($i > 60) { + return $s . '60'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method61(int $i, string $s): string + { + if ($i > 61) { + return $s . '61'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method62(int $i, string $s): string + { + if ($i > 62) { + return $s . '62'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method63(int $i, string $s): string + { + if ($i > 63) { + return $s . '63'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method64(int $i, string $s): string + { + if ($i > 64) { + return $s . '64'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method65(int $i, string $s): string + { + if ($i > 65) { + return $s . '65'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method66(int $i, string $s): string + { + if ($i > 66) { + return $s . '66'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method67(int $i, string $s): string + { + if ($i > 67) { + return $s . '67'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method68(int $i, string $s): string + { + if ($i > 68) { + return $s . '68'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method69(int $i, string $s): string + { + if ($i > 69) { + return $s . '69'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method70(int $i, string $s): string + { + if ($i > 70) { + return $s . '70'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method71(int $i, string $s): string + { + if ($i > 71) { + return $s . '71'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method72(int $i, string $s): string + { + if ($i > 72) { + return $s . '72'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method73(int $i, string $s): string + { + if ($i > 73) { + return $s . '73'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method74(int $i, string $s): string + { + if ($i > 74) { + return $s . '74'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method75(int $i, string $s): string + { + if ($i > 75) { + return $s . '75'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method76(int $i, string $s): string + { + if ($i > 76) { + return $s . '76'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method77(int $i, string $s): string + { + if ($i > 77) { + return $s . '77'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method78(int $i, string $s): string + { + if ($i > 78) { + return $s . '78'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method79(int $i, string $s): string + { + if ($i > 79) { + return $s . '79'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method80(int $i, string $s): string + { + if ($i > 80) { + return $s . '80'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method81(int $i, string $s): string + { + if ($i > 81) { + return $s . '81'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method82(int $i, string $s): string + { + if ($i > 82) { + return $s . '82'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method83(int $i, string $s): string + { + if ($i > 83) { + return $s . '83'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method84(int $i, string $s): string + { + if ($i > 84) { + return $s . '84'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method85(int $i, string $s): string + { + if ($i > 85) { + return $s . '85'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method86(int $i, string $s): string + { + if ($i > 86) { + return $s . '86'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method87(int $i, string $s): string + { + if ($i > 87) { + return $s . '87'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method88(int $i, string $s): string + { + if ($i > 88) { + return $s . '88'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method89(int $i, string $s): string + { + if ($i > 89) { + return $s . '89'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method90(int $i, string $s): string + { + if ($i > 90) { + return $s . '90'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method91(int $i, string $s): string + { + if ($i > 91) { + return $s . '91'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method92(int $i, string $s): string + { + if ($i > 92) { + return $s . '92'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method93(int $i, string $s): string + { + if ($i > 93) { + return $s . '93'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method94(int $i, string $s): string + { + if ($i > 94) { + return $s . '94'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method95(int $i, string $s): string + { + if ($i > 95) { + return $s . '95'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method96(int $i, string $s): string + { + if ($i > 96) { + return $s . '96'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method97(int $i, string $s): string + { + if ($i > 97) { + return $s . '97'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method98(int $i, string $s): string + { + if ($i > 98) { + return $s . '98'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method99(int $i, string $s): string + { + if ($i > 99) { + return $s . '99'; + } + + return strrev($s) . $i; + } + + #[\Override] + public function method100(int $i, string $s): string + { + if ($i > 100) { + return $s . '100'; + } + + return strrev($s) . $i; + } + +}