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