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