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