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