You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
10 KiB

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