1: | <?php declare(strict_types=1); |
2: | |
3: | namespace PhpParser; |
4: | |
5: | class NodeTraverser implements NodeTraverserInterface { |
6: | |
7: | |
8: | |
9: | public const DONT_TRAVERSE_CHILDREN = NodeVisitor::DONT_TRAVERSE_CHILDREN; |
10: | |
11: | |
12: | |
13: | |
14: | public const STOP_TRAVERSAL = NodeVisitor::STOP_TRAVERSAL; |
15: | |
16: | |
17: | |
18: | |
19: | public const REMOVE_NODE = NodeVisitor::REMOVE_NODE; |
20: | |
21: | |
22: | |
23: | |
24: | public const DONT_TRAVERSE_CURRENT_AND_CHILDREN = NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; |
25: | |
26: | |
27: | protected array $visitors = []; |
28: | |
29: | |
30: | protected bool $stopTraversal; |
31: | |
32: | |
33: | |
34: | |
35: | |
36: | |
37: | public function __construct(NodeVisitor ...$visitors) { |
38: | $this->visitors = $visitors; |
39: | } |
40: | |
41: | |
42: | |
43: | |
44: | |
45: | |
46: | public function addVisitor(NodeVisitor $visitor): void { |
47: | $this->visitors[] = $visitor; |
48: | } |
49: | |
50: | |
51: | |
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: | |
62: | |
63: | |
64: | |
65: | |
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: | |
90: | |
91: | |
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: | |
170: | |
171: | |
172: | |
173: | |
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: | |