1: <?php declare(strict_types=1);
2:
3: namespace PhpParser;
4:
5: class NodeTraverser implements NodeTraverserInterface
6: {
7: /**
8: * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CHILDREN, child nodes
9: * of the current node will not be traversed for any visitors.
10: *
11: * For subsequent visitors enterNode() will still be called on the current
12: * node and leaveNode() will also be invoked for the current node.
13: */
14: const DONT_TRAVERSE_CHILDREN = 1;
15:
16: /**
17: * If NodeVisitor::enterNode() or NodeVisitor::leaveNode() returns
18: * STOP_TRAVERSAL, traversal is aborted.
19: *
20: * The afterTraverse() method will still be invoked.
21: */
22: const STOP_TRAVERSAL = 2;
23:
24: /**
25: * If NodeVisitor::leaveNode() returns REMOVE_NODE for a node that occurs
26: * in an array, it will be removed from the array.
27: *
28: * For subsequent visitors leaveNode() will still be invoked for the
29: * removed node.
30: */
31: const REMOVE_NODE = 3;
32:
33: /**
34: * If NodeVisitor::enterNode() returns DONT_TRAVERSE_CURRENT_AND_CHILDREN, child nodes
35: * of the current node will not be traversed for any visitors.
36: *
37: * For subsequent visitors enterNode() will not be called as well.
38: * leaveNode() will be invoked for visitors that has enterNode() method invoked.
39: */
40: const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4;
41:
42: /** @var NodeVisitor[] Visitors */
43: protected $visitors = [];
44:
45: /** @var bool Whether traversal should be stopped */
46: protected $stopTraversal;
47:
48: public function __construct() {
49: // for BC
50: }
51:
52: /**
53: * Adds a visitor.
54: *
55: * @param NodeVisitor $visitor Visitor to add
56: */
57: public function addVisitor(NodeVisitor $visitor) {
58: $this->visitors[] = $visitor;
59: }
60:
61: /**
62: * Removes an added visitor.
63: *
64: * @param NodeVisitor $visitor
65: */
66: public function removeVisitor(NodeVisitor $visitor) {
67: foreach ($this->visitors as $index => $storedVisitor) {
68: if ($storedVisitor === $visitor) {
69: unset($this->visitors[$index]);
70: break;
71: }
72: }
73: }
74:
75: /**
76: * Traverses an array of nodes using the registered visitors.
77: *
78: * @param Node[] $nodes Array of nodes
79: *
80: * @return Node[] Traversed array of nodes
81: */
82: public function traverse(array $nodes) : array {
83: $this->stopTraversal = false;
84:
85: foreach ($this->visitors as $visitor) {
86: if (null !== $return = $visitor->beforeTraverse($nodes)) {
87: $nodes = $return;
88: }
89: }
90:
91: $nodes = $this->traverseArray($nodes);
92:
93: foreach ($this->visitors as $visitor) {
94: if (null !== $return = $visitor->afterTraverse($nodes)) {
95: $nodes = $return;
96: }
97: }
98:
99: return $nodes;
100: }
101:
102: /**
103: * Recursively traverse a node.
104: *
105: * @param Node $node Node to traverse.
106: *
107: * @return Node Result of traversal (may be original node or new one)
108: */
109: protected function traverseNode(Node $node) : Node {
110: foreach ($node->getSubNodeNames() as $name) {
111: $subNode =& $node->$name;
112:
113: if (\is_array($subNode)) {
114: $subNode = $this->traverseArray($subNode);
115: if ($this->stopTraversal) {
116: break;
117: }
118: } elseif ($subNode instanceof Node) {
119: $traverseChildren = true;
120: $breakVisitorIndex = null;
121:
122: foreach ($this->visitors as $visitorIndex => $visitor) {
123: $return = $visitor->enterNode($subNode);
124: if (null !== $return) {
125: if ($return instanceof Node) {
126: $this->ensureReplacementReasonable($subNode, $return);
127: $subNode = $return;
128: } elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
129: $traverseChildren = false;
130: } elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
131: $traverseChildren = false;
132: $breakVisitorIndex = $visitorIndex;
133: break;
134: } elseif (self::STOP_TRAVERSAL === $return) {
135: $this->stopTraversal = true;
136: break 2;
137: } else {
138: throw new \LogicException(
139: 'enterNode() returned invalid value of type ' . gettype($return)
140: );
141: }
142: }
143: }
144:
145: if ($traverseChildren) {
146: $subNode = $this->traverseNode($subNode);
147: if ($this->stopTraversal) {
148: break;
149: }
150: }
151:
152: foreach ($this->visitors as $visitorIndex => $visitor) {
153: $return = $visitor->leaveNode($subNode);
154:
155: if (null !== $return) {
156: if ($return instanceof Node) {
157: $this->ensureReplacementReasonable($subNode, $return);
158: $subNode = $return;
159: } elseif (self::STOP_TRAVERSAL === $return) {
160: $this->stopTraversal = true;
161: break 2;
162: } elseif (\is_array($return)) {
163: throw new \LogicException(
164: 'leaveNode() may only return an array ' .
165: 'if the parent structure is an array'
166: );
167: } else {
168: throw new \LogicException(
169: 'leaveNode() returned invalid value of type ' . gettype($return)
170: );
171: }
172: }
173:
174: if ($breakVisitorIndex === $visitorIndex) {
175: break;
176: }
177: }
178: }
179: }
180:
181: return $node;
182: }
183:
184: /**
185: * Recursively traverse array (usually of nodes).
186: *
187: * @param array $nodes Array to traverse
188: *
189: * @return array Result of traversal (may be original array or changed one)
190: */
191: protected function traverseArray(array $nodes) : array {
192: $doNodes = [];
193:
194: foreach ($nodes as $i => &$node) {
195: if ($node instanceof Node) {
196: $traverseChildren = true;
197: $breakVisitorIndex = null;
198:
199: foreach ($this->visitors as $visitorIndex => $visitor) {
200: $return = $visitor->enterNode($node);
201: if (null !== $return) {
202: if ($return instanceof Node) {
203: $this->ensureReplacementReasonable($node, $return);
204: $node = $return;
205: } elseif (self::DONT_TRAVERSE_CHILDREN === $return) {
206: $traverseChildren = false;
207: } elseif (self::DONT_TRAVERSE_CURRENT_AND_CHILDREN === $return) {
208: $traverseChildren = false;
209: $breakVisitorIndex = $visitorIndex;
210: break;
211: } elseif (self::STOP_TRAVERSAL === $return) {
212: $this->stopTraversal = true;
213: break 2;
214: } else {
215: throw new \LogicException(
216: 'enterNode() returned invalid value of type ' . gettype($return)
217: );
218: }
219: }
220: }
221:
222: if ($traverseChildren) {
223: $node = $this->traverseNode($node);
224: if ($this->stopTraversal) {
225: break;
226: }
227: }
228:
229: foreach ($this->visitors as $visitorIndex => $visitor) {
230: $return = $visitor->leaveNode($node);
231:
232: if (null !== $return) {
233: if ($return instanceof Node) {
234: $this->ensureReplacementReasonable($node, $return);
235: $node = $return;
236: } elseif (\is_array($return)) {
237: $doNodes[] = [$i, $return];
238: break;
239: } elseif (self::REMOVE_NODE === $return) {
240: $doNodes[] = [$i, []];
241: break;
242: } elseif (self::STOP_TRAVERSAL === $return) {
243: $this->stopTraversal = true;
244: break 2;
245: } elseif (false === $return) {
246: throw new \LogicException(
247: 'bool(false) return from leaveNode() no longer supported. ' .
248: 'Return NodeTraverser::REMOVE_NODE instead'
249: );
250: } else {
251: throw new \LogicException(
252: 'leaveNode() returned invalid value of type ' . gettype($return)
253: );
254: }
255: }
256:
257: if ($breakVisitorIndex === $visitorIndex) {
258: break;
259: }
260: }
261: } elseif (\is_array($node)) {
262: throw new \LogicException('Invalid node structure: Contains nested arrays');
263: }
264: }
265:
266: if (!empty($doNodes)) {
267: while (list($i, $replace) = array_pop($doNodes)) {
268: array_splice($nodes, $i, 1, $replace);
269: }
270: }
271:
272: return $nodes;
273: }
274:
275: private function ensureReplacementReasonable($old, $new) {
276: if ($old instanceof Node\Stmt && $new instanceof Node\Expr) {
277: throw new \LogicException(
278: "Trying to replace statement ({$old->getType()}) " .
279: "with expression ({$new->getType()}). Are you missing a " .
280: "Stmt_Expression wrapper?"
281: );
282: }
283:
284: if ($old instanceof Node\Expr && $new instanceof Node\Stmt) {
285: throw new \LogicException(
286: "Trying to replace expression ({$old->getType()}) " .
287: "with statement ({$new->getType()})"
288: );
289: }
290: }
291: }
292: