mirror of
https://github.com/nikic/PHP-Parser.git
synced 2025-01-17 15:18:17 +01:00
a3b0541c71
This uses the same semantics as arrays from leaveNode(), i.e. the returned nodes will not be visited by other visitors. This is open to change though (but probably should change for both enterNode() and leaveNode() if it does change?)
297 lines
11 KiB
PHP
297 lines
11 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
namespace PhpParser;
|
|
|
|
class NodeTraverser implements NodeTraverserInterface {
|
|
/**
|
|
* If NodeVisitor::enterNode() returns DONT_TRAVERSE_CHILDREN, child nodes
|
|
* of the current node will not be traversed for any visitors.
|
|
*
|
|
* For subsequent visitors enterNode() will still be called on the current
|
|
* node and leaveNode() will also be invoked for the current node.
|
|
*/
|
|
public const DONT_TRAVERSE_CHILDREN = 1;
|
|
|
|
/**
|
|
* If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns
|
|
* STOP_TRAVERSAL, traversal is aborted.
|
|
*
|
|
* The afterTraverse() method will still be invoked.
|
|
*/
|
|
public const STOP_TRAVERSAL = 2;
|
|
|
|
/**
|
|
* If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs
|
|
* in an array, it will be removed from the array.
|
|
*
|
|
* For subsequent visitors leaveNode() will still be invoked for the
|
|
* removed node.
|
|
*/
|
|
public const REMOVE_NODE = 3;
|
|
|
|
/**
|
|
* If NodeVisitor::enterNode() returns DONT_TRAVERSE_CURRENT_AND_CHILDREN, child nodes
|
|
* of the current node will not be traversed for any visitors.
|
|
*
|
|
* For subsequent visitors enterNode() will not be called as well.
|
|
* leaveNode() will be invoked for visitors that has enterNode() method invoked.
|
|
*/
|
|
public const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4;
|
|
|
|
/** @var NodeVisitor[] Visitors */
|
|
protected $visitors = [];
|
|
|
|
/** @var bool Whether traversal should be stopped */
|
|
protected $stopTraversal;
|
|
|
|
public function __construct() {
|
|
// for BC
|
|
}
|
|
|
|
/**
|
|
* Adds a visitor.
|
|
*
|
|
* @param NodeVisitor $visitor Visitor to add
|
|
*/
|
|
public function addVisitor(NodeVisitor $visitor) {
|
|
$this->visitors[] = $visitor;
|
|
}
|
|
|
|
/**
|
|
* Removes an added visitor.
|
|
*
|
|
* @param NodeVisitor $visitor
|
|
*/
|
|
public function removeVisitor(NodeVisitor $visitor) {
|
|
foreach ($this->visitors as $index => $storedVisitor) {
|
|
if ($storedVisitor === $visitor) {
|
|
unset($this->visitors[$index]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Traverses an array of nodes using the registered visitors.
|
|
*
|
|
* @param Node[] $nodes Array of nodes
|
|
*
|
|
* @return Node[] Traversed array of nodes
|
|
*/
|
|
public function traverse(array $nodes): array {
|
|
$this->stopTraversal = false;
|
|
|
|
foreach ($this->visitors as $visitor) {
|
|
if (null !== $return = $visitor->beforeTraverse($nodes)) {
|
|
$nodes = $return;
|
|
}
|
|
}
|
|
|
|
$nodes = $this->traverseArray($nodes);
|
|
|
|
foreach ($this->visitors as $visitor) {
|
|
if (null !== $return = $visitor->afterTraverse($nodes)) {
|
|
$nodes = $return;
|
|
}
|
|
}
|
|
|
|
return $nodes;
|
|
}
|
|
|
|
/**
|
|
* Recursively traverse a node.
|
|
*
|
|
* @param Node $node Node to traverse.
|
|
*
|
|
* @return Node Result of traversal (may be original node or new one)
|
|
*/
|
|
protected function traverseNode(Node $node): Node {
|
|
foreach ($node->getSubNodeNames() as $name) {
|
|
$subNode =& $node->$name;
|
|
|
|
if (\is_array($subNode)) {
|
|
$subNode = $this->traverseArray($subNode);
|
|
if ($this->stopTraversal) {
|
|
break;
|
|
}
|
|
} elseif ($subNode instanceof Node) {
|
|
$traverseChildren = true;
|
|
$breakVisitorIndex = null;
|
|
|
|
foreach ($this->visitors as $visitorIndex => $visitor) {
|
|
$return = $visitor->enterNode($subNode);
|
|
if (null !== $return) {
|
|
if ($return instanceof Node) {
|
|
$this->ensureReplacementReasonable($subNode, $return);
|
|
$subNode = $return;
|
|
} elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
|
|
$traverseChildren = false;
|
|
} elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
|
|
$traverseChildren = false;
|
|
$breakVisitorIndex = $visitorIndex;
|
|
break;
|
|
} elseif (self::STOP_TRAVERSAL === $return) {
|
|
$this->stopTraversal = true;
|
|
break 2;
|
|
} else {
|
|
throw new \LogicException(
|
|
'enterNode() returned invalid value of type ' . gettype($return)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($traverseChildren) {
|
|
$subNode = $this->traverseNode($subNode);
|
|
if ($this->stopTraversal) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ($this->visitors as $visitorIndex => $visitor) {
|
|
$return = $visitor->leaveNode($subNode);
|
|
|
|
if (null !== $return) {
|
|
if ($return instanceof Node) {
|
|
$this->ensureReplacementReasonable($subNode, $return);
|
|
$subNode = $return;
|
|
} elseif (self::STOP_TRAVERSAL === $return) {
|
|
$this->stopTraversal = true;
|
|
break 2;
|
|
} elseif (\is_array($return)) {
|
|
throw new \LogicException(
|
|
'leaveNode() may only return an array ' .
|
|
'if the parent structure is an array'
|
|
);
|
|
} else {
|
|
throw new \LogicException(
|
|
'leaveNode() returned invalid value of type ' . gettype($return)
|
|
);
|
|
}
|
|
}
|
|
|
|
if ($breakVisitorIndex === $visitorIndex) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* Recursively traverse array (usually of nodes).
|
|
*
|
|
* @param array $nodes Array to traverse
|
|
*
|
|
* @return array Result of traversal (may be original array or changed one)
|
|
*/
|
|
protected function traverseArray(array $nodes): array {
|
|
$doNodes = [];
|
|
|
|
foreach ($nodes as $i => &$node) {
|
|
if ($node instanceof Node) {
|
|
$traverseChildren = true;
|
|
$breakVisitorIndex = null;
|
|
|
|
foreach ($this->visitors as $visitorIndex => $visitor) {
|
|
$return = $visitor->enterNode($node);
|
|
if (null !== $return) {
|
|
if ($return instanceof Node) {
|
|
$this->ensureReplacementReasonable($node, $return);
|
|
$node = $return;
|
|
} else if (\is_array($return)) {
|
|
$doNodes[] = [$i, $return];
|
|
continue 2;
|
|
} else if (self::REMOVE_NODE === $return) {
|
|
$doNodes[] = [$i, []];
|
|
continue 2;
|
|
} elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
|
|
$traverseChildren = false;
|
|
} elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
|
|
$traverseChildren = false;
|
|
$breakVisitorIndex = $visitorIndex;
|
|
break;
|
|
} elseif (self::STOP_TRAVERSAL === $return) {
|
|
$this->stopTraversal = true;
|
|
break 2;
|
|
} else {
|
|
throw new \LogicException(
|
|
'enterNode() returned invalid value of type ' . gettype($return)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($traverseChildren) {
|
|
$node = $this->traverseNode($node);
|
|
if ($this->stopTraversal) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ($this->visitors as $visitorIndex => $visitor) {
|
|
$return = $visitor->leaveNode($node);
|
|
|
|
if (null !== $return) {
|
|
if ($return instanceof Node) {
|
|
$this->ensureReplacementReasonable($node, $return);
|
|
$node = $return;
|
|
} elseif (\is_array($return)) {
|
|
$doNodes[] = [$i, $return];
|
|
break;
|
|
} elseif (self::REMOVE_NODE === $return) {
|
|
$doNodes[] = [$i, []];
|
|
break;
|
|
} elseif (self::STOP_TRAVERSAL === $return) {
|
|
$this->stopTraversal = true;
|
|
break 2;
|
|
} elseif (false === $return) {
|
|
throw new \LogicException(
|
|
'bool(false) return from leaveNode() no longer supported. ' .
|
|
'Return NodeTraverser::REMOVE_NODE instead'
|
|
);
|
|
} else {
|
|
throw new \LogicException(
|
|
'leaveNode() returned invalid value of type ' . gettype($return)
|
|
);
|
|
}
|
|
}
|
|
|
|
if ($breakVisitorIndex === $visitorIndex) {
|
|
break;
|
|
}
|
|
}
|
|
} elseif (\is_array($node)) {
|
|
throw new \LogicException('Invalid node structure: Contains nested arrays');
|
|
}
|
|
}
|
|
|
|
if (!empty($doNodes)) {
|
|
while (list($i, $replace) = array_pop($doNodes)) {
|
|
array_splice($nodes, $i, 1, $replace);
|
|
}
|
|
}
|
|
|
|
return $nodes;
|
|
}
|
|
|
|
private function ensureReplacementReasonable($old, $new) {
|
|
if ($old instanceof Node\Stmt && $new instanceof Node\Expr) {
|
|
throw new \LogicException(
|
|
"Trying to replace statement ({$old->getType()}) " .
|
|
"with expression ({$new->getType()}). Are you missing a " .
|
|
"Stmt_Expression wrapper?"
|
|
);
|
|
}
|
|
|
|
if ($old instanceof Node\Expr && $new instanceof Node\Stmt) {
|
|
throw new \LogicException(
|
|
"Trying to replace expression ({$old->getType()}) " .
|
|
"with statement ({$new->getType()})"
|
|
);
|
|
}
|
|
}
|
|
}
|