NodeDumper.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <?php declare(strict_types=1);
  2. namespace PhpParser;
  3. use PhpParser\Node\Expr\Array_;
  4. use PhpParser\Node\Expr\Include_;
  5. use PhpParser\Node\Expr\List_;
  6. use PhpParser\Node\Scalar\Int_;
  7. use PhpParser\Node\Scalar\InterpolatedString;
  8. use PhpParser\Node\Scalar\String_;
  9. use PhpParser\Node\Stmt\GroupUse;
  10. use PhpParser\Node\Stmt\Use_;
  11. use PhpParser\Node\UseItem;
  12. class NodeDumper {
  13. private bool $dumpComments;
  14. private bool $dumpPositions;
  15. private bool $dumpOtherAttributes;
  16. private ?string $code;
  17. private string $res;
  18. private string $nl;
  19. private const IGNORE_ATTRIBUTES = [
  20. 'comments' => true,
  21. 'startLine' => true,
  22. 'endLine' => true,
  23. 'startFilePos' => true,
  24. 'endFilePos' => true,
  25. 'startTokenPos' => true,
  26. 'endTokenPos' => true,
  27. ];
  28. /**
  29. * Constructs a NodeDumper.
  30. *
  31. * Supported options:
  32. * * bool dumpComments: Whether comments should be dumped.
  33. * * bool dumpPositions: Whether line/offset information should be dumped. To dump offset
  34. * information, the code needs to be passed to dump().
  35. * * bool dumpOtherAttributes: Whether non-comment, non-position attributes should be dumped.
  36. *
  37. * @param array $options Options (see description)
  38. */
  39. public function __construct(array $options = []) {
  40. $this->dumpComments = !empty($options['dumpComments']);
  41. $this->dumpPositions = !empty($options['dumpPositions']);
  42. $this->dumpOtherAttributes = !empty($options['dumpOtherAttributes']);
  43. }
  44. /**
  45. * Dumps a node or array.
  46. *
  47. * @param array|Node $node Node or array to dump
  48. * @param string|null $code Code corresponding to dumped AST. This only needs to be passed if
  49. * the dumpPositions option is enabled and the dumping of node offsets
  50. * is desired.
  51. *
  52. * @return string Dumped value
  53. */
  54. public function dump($node, ?string $code = null): string {
  55. $this->code = $code;
  56. $this->res = '';
  57. $this->nl = "\n";
  58. $this->dumpRecursive($node, false);
  59. return $this->res;
  60. }
  61. /** @param mixed $node */
  62. protected function dumpRecursive($node, bool $indent = true): void {
  63. if ($indent) {
  64. $this->nl .= " ";
  65. }
  66. if ($node instanceof Node) {
  67. $this->res .= $node->getType();
  68. if ($this->dumpPositions && null !== $p = $this->dumpPosition($node)) {
  69. $this->res .= $p;
  70. }
  71. $this->res .= '(';
  72. foreach ($node->getSubNodeNames() as $key) {
  73. $this->res .= "$this->nl " . $key . ': ';
  74. $value = $node->$key;
  75. if (\is_int($value)) {
  76. if ('flags' === $key || 'newModifier' === $key) {
  77. $this->res .= $this->dumpFlags($value);
  78. continue;
  79. }
  80. if ('type' === $key && $node instanceof Include_) {
  81. $this->res .= $this->dumpIncludeType($value);
  82. continue;
  83. }
  84. if ('type' === $key
  85. && ($node instanceof Use_ || $node instanceof UseItem || $node instanceof GroupUse)) {
  86. $this->res .= $this->dumpUseType($value);
  87. continue;
  88. }
  89. }
  90. $this->dumpRecursive($value);
  91. }
  92. if ($this->dumpComments && $comments = $node->getComments()) {
  93. $this->res .= "$this->nl comments: ";
  94. $this->dumpRecursive($comments);
  95. }
  96. if ($this->dumpOtherAttributes) {
  97. foreach ($node->getAttributes() as $key => $value) {
  98. if (isset(self::IGNORE_ATTRIBUTES[$key])) {
  99. continue;
  100. }
  101. $this->res .= "$this->nl $key: ";
  102. if (\is_int($value)) {
  103. if ('kind' === $key) {
  104. if ($node instanceof Int_) {
  105. $this->res .= $this->dumpIntKind($value);
  106. continue;
  107. }
  108. if ($node instanceof String_ || $node instanceof InterpolatedString) {
  109. $this->res .= $this->dumpStringKind($value);
  110. continue;
  111. }
  112. if ($node instanceof Array_) {
  113. $this->res .= $this->dumpArrayKind($value);
  114. continue;
  115. }
  116. if ($node instanceof List_) {
  117. $this->res .= $this->dumpListKind($value);
  118. continue;
  119. }
  120. }
  121. }
  122. $this->dumpRecursive($value);
  123. }
  124. }
  125. $this->res .= "$this->nl)";
  126. } elseif (\is_array($node)) {
  127. $this->res .= 'array(';
  128. foreach ($node as $key => $value) {
  129. $this->res .= "$this->nl " . $key . ': ';
  130. $this->dumpRecursive($value);
  131. }
  132. $this->res .= "$this->nl)";
  133. } elseif ($node instanceof Comment) {
  134. $this->res .= \str_replace("\n", $this->nl, $node->getReformattedText());
  135. } elseif (\is_string($node)) {
  136. $this->res .= \str_replace("\n", $this->nl, (string)$node);
  137. } elseif (\is_int($node) || \is_float($node)) {
  138. $this->res .= $node;
  139. } elseif (null === $node) {
  140. $this->res .= 'null';
  141. } elseif (false === $node) {
  142. $this->res .= 'false';
  143. } elseif (true === $node) {
  144. $this->res .= 'true';
  145. } else {
  146. throw new \InvalidArgumentException('Can only dump nodes and arrays.');
  147. }
  148. if ($indent) {
  149. $this->nl = \substr($this->nl, 0, -4);
  150. }
  151. }
  152. protected function dumpFlags(int $flags): string {
  153. $strs = [];
  154. if ($flags & Modifiers::PUBLIC) {
  155. $strs[] = 'PUBLIC';
  156. }
  157. if ($flags & Modifiers::PROTECTED) {
  158. $strs[] = 'PROTECTED';
  159. }
  160. if ($flags & Modifiers::PRIVATE) {
  161. $strs[] = 'PRIVATE';
  162. }
  163. if ($flags & Modifiers::ABSTRACT) {
  164. $strs[] = 'ABSTRACT';
  165. }
  166. if ($flags & Modifiers::STATIC) {
  167. $strs[] = 'STATIC';
  168. }
  169. if ($flags & Modifiers::FINAL) {
  170. $strs[] = 'FINAL';
  171. }
  172. if ($flags & Modifiers::READONLY) {
  173. $strs[] = 'READONLY';
  174. }
  175. if ($strs) {
  176. return implode(' | ', $strs) . ' (' . $flags . ')';
  177. } else {
  178. return (string) $flags;
  179. }
  180. }
  181. /** @param array<int, string> $map */
  182. private function dumpEnum(int $value, array $map): string {
  183. if (!isset($map[$value])) {
  184. return (string) $value;
  185. }
  186. return $map[$value] . ' (' . $value . ')';
  187. }
  188. private function dumpIncludeType(int $type): string {
  189. return $this->dumpEnum($type, [
  190. Include_::TYPE_INCLUDE => 'TYPE_INCLUDE',
  191. Include_::TYPE_INCLUDE_ONCE => 'TYPE_INCLUDE_ONCE',
  192. Include_::TYPE_REQUIRE => 'TYPE_REQUIRE',
  193. Include_::TYPE_REQUIRE_ONCE => 'TYPE_REQUIRE_ONCE',
  194. ]);
  195. }
  196. private function dumpUseType(int $type): string {
  197. return $this->dumpEnum($type, [
  198. Use_::TYPE_UNKNOWN => 'TYPE_UNKNOWN',
  199. Use_::TYPE_NORMAL => 'TYPE_NORMAL',
  200. Use_::TYPE_FUNCTION => 'TYPE_FUNCTION',
  201. Use_::TYPE_CONSTANT => 'TYPE_CONSTANT',
  202. ]);
  203. }
  204. private function dumpIntKind(int $kind): string {
  205. return $this->dumpEnum($kind, [
  206. Int_::KIND_BIN => 'KIND_BIN',
  207. Int_::KIND_OCT => 'KIND_OCT',
  208. Int_::KIND_DEC => 'KIND_DEC',
  209. Int_::KIND_HEX => 'KIND_HEX',
  210. ]);
  211. }
  212. private function dumpStringKind(int $kind): string {
  213. return $this->dumpEnum($kind, [
  214. String_::KIND_SINGLE_QUOTED => 'KIND_SINGLE_QUOTED',
  215. String_::KIND_DOUBLE_QUOTED => 'KIND_DOUBLE_QUOTED',
  216. String_::KIND_HEREDOC => 'KIND_HEREDOC',
  217. String_::KIND_NOWDOC => 'KIND_NOWDOC',
  218. ]);
  219. }
  220. private function dumpArrayKind(int $kind): string {
  221. return $this->dumpEnum($kind, [
  222. Array_::KIND_LONG => 'KIND_LONG',
  223. Array_::KIND_SHORT => 'KIND_SHORT',
  224. ]);
  225. }
  226. private function dumpListKind(int $kind): string {
  227. return $this->dumpEnum($kind, [
  228. List_::KIND_LIST => 'KIND_LIST',
  229. List_::KIND_ARRAY => 'KIND_ARRAY',
  230. ]);
  231. }
  232. /**
  233. * Dump node position, if possible.
  234. *
  235. * @param Node $node Node for which to dump position
  236. *
  237. * @return string|null Dump of position, or null if position information not available
  238. */
  239. protected function dumpPosition(Node $node): ?string {
  240. if (!$node->hasAttribute('startLine') || !$node->hasAttribute('endLine')) {
  241. return null;
  242. }
  243. $start = $node->getStartLine();
  244. $end = $node->getEndLine();
  245. if ($node->hasAttribute('startFilePos') && $node->hasAttribute('endFilePos')
  246. && null !== $this->code
  247. ) {
  248. $start .= ':' . $this->toColumn($this->code, $node->getStartFilePos());
  249. $end .= ':' . $this->toColumn($this->code, $node->getEndFilePos());
  250. }
  251. return "[$start - $end]";
  252. }
  253. // Copied from Error class
  254. private function toColumn(string $code, int $pos): int {
  255. if ($pos > strlen($code)) {
  256. throw new \RuntimeException('Invalid position information');
  257. }
  258. $lineStartPos = strrpos($code, "\n", $pos - strlen($code));
  259. if (false === $lineStartPos) {
  260. $lineStartPos = -1;
  261. }
  262. return $pos - $lineStartPos;
  263. }
  264. }