Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"require": {
"php": "^8.2",
"clue/ndjson-react": "^1.3",
"composer/pcre": "^3.3.0",
"composer/pcre": "^3.3.2",
"composer/semver": "^3.4",
"composer/xdebug-handler": "^3.0.5",
"doctrine/inflector": "^2.1",
Expand Down Expand Up @@ -50,7 +50,7 @@
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-webmozart-assert": "^2.0",
"phpunit/phpunit": "^11.5",
"rector/jack": "^0.4.0",
"rector/jack": "^0.4",
"rector/release-notes-generator": "^0.5",
"rector/swiss-knife": "^2.3",
"rector/type-perfect": "^2.1",
Expand Down
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,11 @@ parameters:
- rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php
- rules/Php81/Enum/AttributeName.php

-
identifier: symplify.seeAnnotationToTest
paths:
- tests/PhpParser/NodeTraverser/StopTraverseOnTypeChange/Class_

# deprecated rule
- '#Rule Rector\\Php81\\Rector\\Array_\\FirstClassCallableRector must implements Rector\\VersionBonding\\Contract\\MinPhpVersionInterface#'
- '#Register "Rector\\Php81\\Rector\\Array_\\FirstClassCallableRector" service to "php81\.php" config set#'
Expand Down
16 changes: 16 additions & 0 deletions src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,22 @@ protected function traverseNode(Node $node): void
$traverseChildren = true;
$visitorIndex = -1;
$currentNodeVisitors = $this->getVisitorsForNode($subNode);

foreach ($currentNodeVisitors as $visitorIndex => $visitor) {
$return = $visitor->enterNode($subNode);
if ($return !== null) {
if ($return instanceof Node) {
$originalSubNodeClass = $subNode::class;

$this->ensureReplacementReasonable($subNode, $return);
$subNode = $return;
$node->{$name} = $return;

if ($originalSubNodeClass !== $subNode::class) {
// stop traversing as node type changed and visitors won't work
return;
Comment on lines +116 to +117
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should continue 2; as well, as currentNodeVisitors is inside inner loop, so next subnode should also allow to be visited, to ensure nothing left behind, while keep the performance :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

} elseif ($return === NodeVisitor::DONT_TRAVERSE_CHILDREN) {
$traverseChildren = false;
} elseif ($return === NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN) {
Expand Down Expand Up @@ -177,12 +186,19 @@ protected function traverseArray(array $nodes): array
$traverseChildren = true;
$visitorIndex = -1;
$currentNodeVisitors = $this->getVisitorsForNode($node);

foreach ($currentNodeVisitors as $visitorIndex => $visitor) {
$return = $visitor->enterNode($node);
if ($return !== null) {
if ($return instanceof Node) {
$originalNodeNodeClass = $node::class;
$this->ensureReplacementReasonable($node, $return);
$nodes[$i] = $node = $return;

if ($originalNodeNodeClass !== $return::class) {
// stop traversing as node type changed and visitors won't work
return $nodes;
}
} elseif (\is_array($return)) {
$doNodes[] = [$i, $return];
continue 2;
Expand Down
2 changes: 2 additions & 0 deletions src/PhpParser/NodeTraverser/RectorNodeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function getVisitorsForNode(Node $node): array

if (! isset($this->visitorsPerNodeClass[$nodeClass])) {
$this->visitorsPerNodeClass[$nodeClass] = [];

/** @var RectorInterface $visitor */
foreach ($this->visitors as $visitor) {
foreach ($visitor->getNodeTypes() as $nodeType) {
Expand Down Expand Up @@ -94,6 +95,7 @@ private function prepareNodeVisitors(): void

// filer out by version
$this->visitors = $this->phpVersionedFilter->filter($this->rectors);

// filter by configuration
$this->visitors = $this->configurationRuleFilter->filter($this->visitors);

Expand Down
16 changes: 0 additions & 16 deletions src/Rector/AbstractRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,6 @@ public function beforeTraverse(array $nodes): ?array
*/
final public function enterNode(Node $node): int|Node|null
{
if (! $this->isMatchingNodeType($node)) {
return null;
}

if (is_a($this, HTMLAverseRectorInterface::class, true) && $this->file->containsHTML()) {
return null;
}
Expand Down Expand Up @@ -317,16 +313,4 @@ private function refreshScopeNodes(array | Node $node, string $filePath, ?Mutati
$this->changedNodeScopeRefresher->refresh($node, $filePath, $mutatingScope);
}
}

private function isMatchingNodeType(Node $node): bool
{
$nodeClass = $node::class;
foreach ($this->getNodeTypes() as $nodeType) {
if (is_a($nodeClass, $nodeType, true)) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\PhpParser\NodeTraverser\StopTraverseOnTypeChange\Class_;

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Trait_;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class RuleChangingClassToTraitRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Change node from class to trait', []);
}

public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): Trait_
{
$trait = new Trait_('SomeTrait');
$trait->namespacedName = new Name('SomeNamespace\SomeTrait');

return $trait;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\PhpParser\NodeTraverser\StopTraverseOnTypeChange\Class_;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;

final class RuleCheckingClassRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Make sure input is class', []);
}

public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
* @return Class_
*/
public function refactor(Node $node): Node
{
Assert::isInstanceOf($node, Class_::class);

return $node;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\PhpParser\NodeTraverser\StopTraverseOnTypeChange\Fixture;

final class SimpleClass
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\PhpParser\NodeTraverser\StopTraverseOnTypeChange;

use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeFinder;
use Rector\PhpParser\NodeTraverser\RectorNodeTraverser;
use Rector\Testing\PHPUnit\AbstractLazyTestCase;
use Rector\Testing\TestingParser\TestingParser;
use Rector\Tests\PhpParser\NodeTraverser\StopTraverseOnTypeChange\Class_\RuleChangingClassToTraitRector;
use Rector\Tests\PhpParser\NodeTraverser\StopTraverseOnTypeChange\Class_\RuleCheckingClassRector;

final class StopTraverseOnTypeChangeTest extends AbstractLazyTestCase
{
private RectorNodeTraverser $rectorNodeTraverser;

private TestingParser $testingParser;

protected function setUp(): void
{
parent::setUp();

$this->rectorNodeTraverser = $this->make(RectorNodeTraverser::class);

$this->rectorNodeTraverser->refreshPhpRectors([
$this->make(RuleChangingClassToTraitRector::class),
$this->make(RuleCheckingClassRector::class),
]);

$this->testingParser = $this->make(TestingParser::class);
}

public function testGetVisitorsForNodeWhenNoVisitorsAvailable(): void
{
// must be cloned + Scope set to allow node replacement
$nodes = $this->testingParser->parseFileToDecoratedNodes(__DIR__ . '/Fixture/SimpleClass.php');

$changedNodes = $this->rectorNodeTraverser->traverse($nodes);

$nodeFinder = new NodeFinder();
$classes = $nodeFinder->findInstanceOf($changedNodes, Class_::class);
$this->assertCount(0, $classes);

$traits = $nodeFinder->findInstanceOf($changedNodes, Trait_::class);
$this->assertCount(1, $traits);
}
}