From afe1628a7221697726c9d677eb6c74fe394c792e Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Sun, 21 May 2023 15:41:41 +0200 Subject: [PATCH] Add support for NodeVisitor::REPLACE_WITH_NULL Fixes #716. --- lib/PhpParser/NodeTraverser.php | 12 +++++++ lib/PhpParser/NodeVisitor.php | 23 +++++++++---- test/PhpParser/NodeTraverserTest.php | 49 ++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/lib/PhpParser/NodeTraverser.php b/lib/PhpParser/NodeTraverser.php index eff2a371..05e0a2c6 100644 --- a/lib/PhpParser/NodeTraverser.php +++ b/lib/PhpParser/NodeTraverser.php @@ -116,6 +116,9 @@ class NodeTraverser implements NodeTraverserInterface { } elseif (NodeVisitor::STOP_TRAVERSAL === $return) { $this->stopTraversal = true; break 2; + } elseif (NodeVisitor::REPLACE_WITH_NULL === $return) { + $subNode = null; + continue 2; } else { throw new \LogicException( 'enterNode() returned invalid value of type ' . gettype($return) @@ -142,6 +145,9 @@ class NodeTraverser implements NodeTraverserInterface { } elseif (NodeVisitor::STOP_TRAVERSAL === $return) { $this->stopTraversal = true; break 2; + } elseif (NodeVisitor::REPLACE_WITH_NULL === $return) { + $subNode = null; + break; } elseif (\is_array($return)) { throw new \LogicException( 'leaveNode() may only return an array ' . @@ -195,6 +201,9 @@ class NodeTraverser implements NodeTraverserInterface { } elseif (NodeVisitor::STOP_TRAVERSAL === $return) { $this->stopTraversal = true; break 2; + } elseif (NodeVisitor::REPLACE_WITH_NULL === $return) { + throw new \LogicException( + 'REPLACE_WITH_NULL can not be used if the parent structure is an array'); } else { throw new \LogicException( 'enterNode() returned invalid value of type ' . gettype($return) @@ -227,6 +236,9 @@ class NodeTraverser implements NodeTraverserInterface { } elseif (NodeVisitor::STOP_TRAVERSAL === $return) { $this->stopTraversal = true; break 2; + } elseif (NodeVisitor::REPLACE_WITH_NULL === $return) { + throw new \LogicException( + 'REPLACE_WITH_NULL can not be used if the parent structure is an array'); } else { throw new \LogicException( 'leaveNode() returned invalid value of type ' . gettype($return) diff --git a/lib/PhpParser/NodeVisitor.php b/lib/PhpParser/NodeVisitor.php index 8acadba7..0ec4f7be 100644 --- a/lib/PhpParser/NodeVisitor.php +++ b/lib/PhpParser/NodeVisitor.php @@ -38,6 +38,13 @@ interface NodeVisitor { */ public const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4; + /** + * If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns REPLACE_WITH_NULL, + * the node will be replaced with null. This is not a legal return value if the node is part + * of an array, rather than another node. + */ + public const REPLACE_WITH_NULL = 5; + /** * Called once before traversal. * @@ -59,14 +66,16 @@ interface NodeVisitor { * => $node stays as-is * * array (of Nodes) * => The return value is merged into the parent array (at the position of the $node) - * * NodeTraverser::REMOVE_NODE + * * NodeVisitor::REMOVE_NODE * => $node is removed from the parent array - * * NodeTraverser::DONT_TRAVERSE_CHILDREN + * * NodeVisitor::REPLACE_WITH_NULL + * => $node is replaced with null + * * NodeVisitor::DONT_TRAVERSE_CHILDREN * => Children of $node are not traversed. $node stays as-is - * * NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN + * * NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN * => Further visitors for the current node are skipped, and its children are not * traversed. $node stays as-is. - * * NodeTraverser::STOP_TRAVERSAL + * * NodeVisitor::STOP_TRAVERSAL * => Traversal is aborted. $node stays as-is * * otherwise * => $node is set to the return value @@ -83,9 +92,11 @@ interface NodeVisitor { * Return value semantics: * * null * => $node stays as-is - * * NodeTraverser::REMOVE_NODE + * * NodeVisitor::REMOVE_NODE * => $node is removed from the parent array - * * NodeTraverser::STOP_TRAVERSAL + * * NodeVisitor::REPLACE_WITH_NULL + * => $node is replaced with null + * * NodeVisitor::STOP_TRAVERSAL * => Traversal is aborted. $node stays as-is * * array (of Nodes) * => The return value is merged into the parent array (at the position of the $node) diff --git a/test/PhpParser/NodeTraverserTest.php b/test/PhpParser/NodeTraverserTest.php index 50a16cb9..6af99622 100644 --- a/test/PhpParser/NodeTraverserTest.php +++ b/test/PhpParser/NodeTraverserTest.php @@ -3,7 +3,10 @@ namespace PhpParser; use PhpParser\Node\Expr; +use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Stmt\Else_; +use PhpParser\Node\Stmt\If_; class NodeTraverserTest extends \PHPUnit\Framework\TestCase { public function testNonModifying() { @@ -343,6 +346,44 @@ class NodeTraverserTest extends \PHPUnit\Framework\TestCase { ], $visitor->trace); } + public function testReplaceWithNull() { + $one = new Int_(1); + $else1 = new Else_(); + $else2 = new Else_(); + $if1 = new If_($one, ['else' => $else1]); + $if2 = new If_($one, ['else' => $else2]); + $stmts = [$if1, $if2]; + $visitor1 = new NodeVisitorForTesting([ + ['enterNode', $else1, NodeVisitor::REPLACE_WITH_NULL], + ['leaveNode', $else2, NodeVisitor::REPLACE_WITH_NULL], + ]); + $visitor2 = new NodeVisitorForTesting(); + $traverser = new NodeTraverser(); + $traverser->addVisitor($visitor1); + $traverser->addVisitor($visitor2); + $newStmts = $traverser->traverse($stmts); + $this->assertEquals([ + new If_($one), + new If_($one), + ], $newStmts); + $this->assertEquals([ + ['beforeTraverse', $stmts], + ['enterNode', $if1], + ['enterNode', $one], + // We never see the if1 Else node. + ['leaveNode', $one], + ['leaveNode', $if1], + ['enterNode', $if2], + ['enterNode', $one], + ['leaveNode', $one], + // We do see the if2 Else node, as it will only be replaced afterwards. + ['enterNode', $else2], + ['leaveNode', $else2], + ['leaveNode', $if2], + ['afterTraverse', $stmts], + ], $visitor2->trace); + } + public function testRemovingVisitor() { $visitor1 = new class () extends NodeVisitorAbstract {}; $visitor2 = new class () extends NodeVisitorAbstract {}; @@ -415,6 +456,12 @@ class NodeTraverserTest extends \PHPUnit\Framework\TestCase { $visitor8 = new NodeVisitorForTesting([ ['enterNode', $num, new Node\Stmt\Return_()], ]); + $visitor9 = new NodeVisitorForTesting([ + ['enterNode', $expr, NodeVisitor::REPLACE_WITH_NULL], + ]); + $visitor10 = new NodeVisitorForTesting([ + ['leaveNode', $expr, NodeVisitor::REPLACE_WITH_NULL], + ]); return [ [$stmts, $visitor1, 'enterNode() returned invalid value of type string'], @@ -425,6 +472,8 @@ class NodeTraverserTest extends \PHPUnit\Framework\TestCase { [$stmts, $visitor6, 'leaveNode() returned invalid value of type bool'], [$stmts, $visitor7, 'Trying to replace statement (Stmt_Expression) with expression (Scalar_Int). Are you missing a Stmt_Expression wrapper?'], [$stmts, $visitor8, 'Trying to replace expression (Scalar_Int) with statement (Stmt_Return)'], + [$stmts, $visitor9, 'REPLACE_WITH_NULL can not be used if the parent structure is an array'], + [$stmts, $visitor10, 'REPLACE_WITH_NULL can not be used if the parent structure is an array'], ]; } }