1: | <?php declare(strict_types=1); |
2: | |
3: | namespace PhpParser; |
4: | |
5: | class NodeTraverser implements NodeTraverserInterface |
6: | { |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | const DONT_TRAVERSE_CHILDREN = 1; |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | const STOP_TRAVERSAL = 2; |
23: | |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | const REMOVE_NODE = 3; |
32: | |
33: | |
34: | |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | const DONT_TRAVERSE_CURRENT_AND_CHILDREN = 4; |
41: | |
42: | |
43: | protected $visitors = []; |
44: | |
45: | |
46: | protected $stopTraversal; |
47: | |
48: | public function __construct() { |
49: | |
50: | } |
51: | |
52: | |
53: | |
54: | |
55: | |
56: | |
57: | public function addVisitor(NodeVisitor $visitor) { |
58: | $this->visitors[] = $visitor; |
59: | } |
60: | |
61: | |
62: | |
63: | |
64: | |
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: | |
77: | |
78: | |
79: | |
80: | |
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: | |
104: | |
105: | |
106: | |
107: | |
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: | |
186: | |
187: | |
188: | |
189: | |
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: | |