Skip to content

Commit e655bb9

Browse files
authored
[perf] Decouple skipper from node run, drop clone-probe double run (#8714)
1 parent 56954f6 commit e655bb9

7 files changed

Lines changed: 45 additions & 54 deletions

File tree

‎rules-tests/Renaming/Rector/Name/RenameClassRector/SkipUsedTrackingTest.php‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,25 @@
1717
*/
1818
final class SkipUsedTrackingTest extends AbstractRectorTestCase
1919
{
20-
public function testMarksSkipUsedOnlyWhenRuleWouldChangeFile(): void
20+
public function testMarksSkipUsedForEveryMatchedFile(): void
2121
{
22-
// file uses OldClass → rule would rename it → its skip is what prevents the change
22+
// file uses OldClass → rule matches the path and is skipped → its skip mask is marked used
2323
$this->doTestFile(__DIR__ . '/FixtureSkipUsedTracking/skip_used_renames_old_class.php.inc');
2424

2525
// doTestFile() only cleans up the last processed temp file, so remove this one explicitly
2626
FileSystem::delete(__DIR__ . '/FixtureSkipUsedTracking/skip_used_renames_old_class.php');
2727

28-
// file does not use OldClass → rule would not change it → its skip is unnecessary
28+
// file does not use OldClass, but its path still matches a skip mask → skip is applied and marked used
2929
$this->doTestFile(__DIR__ . '/FixtureSkipUsedTracking/skip_unused_no_old_class.php.inc');
3030

3131
$skipper = $this->make(Skipper::class);
3232
$usedSkips = $skipper->provideUsedSkips();
3333

3434
$usedPaths = $usedSkips[RenameClassRector::class] ?? [];
3535

36-
// both skip masks match a processed file, so both are marked used
36+
// the rule is skipped once per file whose path matches, so every matched mask is marked used
3737
$this->assertContains('*skip_used_renames_old_class*', $usedPaths);
38-
39-
// the skip on a file the rule would not have touched is never marked used
40-
$this->assertNotContains('*skip_unused_no_old_class*', $usedPaths);
38+
$this->assertContains('*skip_unused_no_old_class*', $usedPaths);
4139
}
4240

4341
public function provideConfigFilePath(): string

‎rules-tests/Renaming/Rector/Name/RenameClassRector/config/skip_used_tracking_configured_rule.php‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
OldClass::class => NewClass::class,
1313
]);
1414

15-
// both paths are skipped, but only the first file actually uses OldClass, so only its skip
16-
// prevents a real change - the second one would not have changed anyway
15+
// both paths are skipped; the rule is skipped once per matching file, so both masks are marked used
1716
$rectorConfig->skip([
1817
RenameClassRector::class => ['*skip_used_renames_old_class*', '*skip_unused_no_old_class*'],
1918
]);

‎src/Application/FileProcessor.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function processFile(File $file, Configuration $configuration): FileProce
6363
$file->changeHasChanged(false);
6464

6565
// 1. change nodes with Rector Rules
66-
$newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts());
66+
$newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts(), $file);
6767

6868
// 2. apply post rectors
6969
$postNewStmts = $this->postFileProcessor->traverse($newStmts, $file);

‎src/Application/RectorRegistry.php‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use PhpParser\Node;
88
use Rector\Configuration\ConfigurationRuleFilter;
99
use Rector\Contract\Rector\RectorInterface;
10+
use Rector\Skipper\Skipper\Skipper;
11+
use Rector\ValueObject\Application\File;
1012
use Rector\VersionBonding\ComposerPackageConstraintFilter;
1113
use Rector\VersionBonding\PhpVersionedFilter;
1214

@@ -34,6 +36,7 @@ public function __construct(
3436
private readonly PhpVersionedFilter $phpVersionedFilter,
3537
private readonly ComposerPackageConstraintFilter $composerPackageConstraintFilter,
3638
private readonly ConfigurationRuleFilter $configurationRuleFilter,
39+
private readonly Skipper $skipper,
3740
private array $rectors,
3841
) {
3942
}
@@ -75,6 +78,28 @@ public function forNode(Node $node): array
7578
return $this->cache[$nodeClass];
7679
}
7780

81+
/**
82+
* Filters out rectors skipped for this file before they ever run, so the skip no longer needs a
83+
* second refactor() probe on a clone to learn whether it mattered.
84+
*
85+
* Known limit: a configured skip is marked used as soon as its path matches the file, even if the
86+
* rule would not have changed it. This drops precise unused-skip reporting in exchange for not
87+
* running the rule twice per skipped node.
88+
*
89+
* @return RectorInterface[]
90+
*/
91+
public function forNodeAndFile(Node $node, File $file): array
92+
{
93+
$rectors = $this->forNode($node);
94+
95+
$filePath = $file->getFilePath();
96+
97+
return array_filter(
98+
$rectors,
99+
fn (RectorInterface $rector): bool => ! $this->skipper->shouldSkipRectorAndFile($rector, $filePath)
100+
);
101+
}
102+
78103
/**
79104
* Filtering must happen lazily, after the configuration is set in ProcessCommand::execute(),
80105
* otherwise options like --only are not yet available and get ignored.

‎src/PhpParser/NodeTraverser/RectorNodeTraverser.php‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PhpParser\NodeVisitor;
1212
use Rector\Application\RectorRegistry;
1313
use Rector\Rector\RectorRunner;
14+
use Rector\ValueObject\Application\File;
1415

1516
final class RectorNodeTraverser
1617
{
@@ -26,19 +27,19 @@ public function __construct(
2627
* @param Node[] $nodes
2728
* @return Node[]
2829
*/
29-
public function traverse(array $nodes): array
30+
public function traverse(array $nodes, File $file): array
3031
{
3132
$this->stopTraversal = false;
3233

33-
return $this->traverseArray($nodes);
34+
return $this->traverseArray($nodes, $file);
3435
}
3536

36-
private function traverseNode(Node $node): void
37+
private function traverseNode(Node $node, File $file): void
3738
{
3839
foreach ($node->getSubNodeNames() as $name) {
3940
$subNode = $node->{$name};
4041
if (\is_array($subNode)) {
41-
$node->{$name} = $this->traverseArray($subNode);
42+
$node->{$name} = $this->traverseArray($subNode, $file);
4243
if ($this->stopTraversal) {
4344
break;
4445
}
@@ -50,7 +51,7 @@ private function traverseNode(Node $node): void
5051
continue;
5152
}
5253

53-
$currentNodeVisitors = $this->rectorRegistry->forNode($subNode);
54+
$currentNodeVisitors = $this->rectorRegistry->forNodeAndFile($subNode, $file);
5455

5556
foreach ($currentNodeVisitors as $currentNodeVisitor) {
5657
$return = $this->rectorRunner->run($currentNodeVisitor, $subNode);
@@ -74,7 +75,8 @@ private function traverseNode(Node $node): void
7475
}
7576
}
7677

77-
$this->traverseNode($subNode);
78+
$this->traverseNode($subNode, $file);
79+
7880
if ($this->stopTraversal) {
7981
break;
8082
}
@@ -85,7 +87,7 @@ private function traverseNode(Node $node): void
8587
* @param array<Node|null> $nodes The null can be in case of empty list(, , )
8688
* @return Node[]
8789
*/
88-
private function traverseArray(array $nodes): array
90+
private function traverseArray(array $nodes, File $file): array
8991
{
9092
$doNodes = [];
9193
foreach ($nodes as $i => $node) {
@@ -97,7 +99,7 @@ private function traverseArray(array $nodes): array
9799
continue;
98100
}
99101

100-
$currentNodeVisitors = $this->rectorRegistry->forNode($node);
102+
$currentNodeVisitors = $this->rectorRegistry->forNodeAndFile($node, $file);
101103

102104
foreach ($currentNodeVisitors as $currentNodeVisitor) {
103105
$return = $this->rectorRunner->run($currentNodeVisitor, $node);
@@ -123,7 +125,7 @@ private function traverseArray(array $nodes): array
123125
}
124126
}
125127

126-
$this->traverseNode($node);
128+
$this->traverseNode($node, $file);
127129
if ($this->stopTraversal) {
128130
break;
129131
}

‎src/Rector/RectorRunner.php‎

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
namespace Rector\Rector;
66

77
use PhpParser\Node;
8-
use PhpParser\NodeTraverser;
98
use PhpParser\NodeVisitor;
10-
use PhpParser\NodeVisitor\CloningVisitor;
11-
use PhpParser\NodeVisitorAbstract;
129
use PHPStan\Analyser\MutatingScope;
1310
use Rector\Application\ChangedNodeScopeRefresher;
1411
use Rector\Application\Provider\CurrentFileProvider;
@@ -18,7 +15,6 @@
1815
use Rector\NodeDecorator\CreatedByRuleDecorator;
1916
use Rector\NodeTypeResolver\Node\AttributeKey;
2017
use Rector\Skipper\Skipper\Skipper;
21-
use Rector\Skipper\ValueObject\SkipMatch;
2218
use Rector\ValueObject\Application\File;
2319

2420
/**
@@ -62,18 +58,6 @@ public function run(RectorInterface $rector, Node $node): int|Node|null|array
6258
return null;
6359
}
6460

65-
// class/path skip is configured for this rule and file: run the rule on a deep clone to learn
66-
// whether it would actually have changed anything. Only a skip that prevents a real change
67-
// counts as used; the original node is left untouched, so the file stays skipped either way.
68-
$skipMatch = $this->skipper->matchSkip($rector, $filePath);
69-
if ($skipMatch instanceof SkipMatch) {
70-
if ($rector->refactor($this->cloneNode($node)) !== null) {
71-
$this->skipper->markSkipUsed($skipMatch);
72-
}
73-
74-
return null;
75-
}
76-
7761
// ensure origNode pulled before refactor to avoid changed during refactor, ref https://3v4l.org/YMEGN
7862
$originalNode = $node->getAttribute(AttributeKey::ORIGINAL_NODE) ?? $node;
7963

@@ -119,23 +103,6 @@ private function getFile(): File
119103
return $file;
120104
}
121105

122-
/**
123-
* Deep clone, so a skipped rule can be probed on the clone without mutating the real node.
124-
*/
125-
private function cloneNode(Node $node): Node
126-
{
127-
$phpDocInfoRemovingNodeVisitor = new class() extends NodeVisitorAbstract {
128-
public function enterNode(Node $node): Node
129-
{
130-
$node->setAttribute(AttributeKey::PHP_DOC_INFO, null);
131-
return $node;
132-
}
133-
};
134-
135-
$nodeTraverser = new NodeTraverser(new CloningVisitor(), $phpDocInfoRemovingNodeVisitor);
136-
return $nodeTraverser->traverse([$node])[0];
137-
}
138-
139106
/**
140107
* @param Node|Node[] $refactoredNode
141108
* @return Node|Node[]

‎tests/PhpParser/NodeTraverser/StopTraverseOnTypeChange/StopTraverseOnTypeChangeTest.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ protected function setUp(): void
3838
public function testGetVisitorsForNodeWhenNoVisitorsAvailable(): void
3939
{
4040
// must be cloned + Scope set to allow node replacement
41-
$nodes = $this->testingParser->parseFileToDecoratedNodes(__DIR__ . '/Fixture/SimpleClass.php');
41+
$file = $this->testingParser->parseFilePathToFile(__DIR__ . '/Fixture/SimpleClass.php');
4242

43-
$changedNodes = $this->rectorNodeTraverser->traverse($nodes);
43+
$changedNodes = $this->rectorNodeTraverser->traverse($file->getNewStmts(), $file);
4444

4545
$nodeFinder = new NodeFinder();
4646
$classes = $nodeFinder->findInstanceOf($changedNodes, Class_::class);

0 commit comments

Comments
 (0)