xref: /PHP-Parser/lib/PhpParser/NodeDumper.php (revision b493c51c)
1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5use PhpParser\Node\Expr\Array_;
6use PhpParser\Node\Expr\Include_;
7use PhpParser\Node\Expr\List_;
8use PhpParser\Node\Scalar\Int_;
9use PhpParser\Node\Scalar\InterpolatedString;
10use PhpParser\Node\Scalar\String_;
11use PhpParser\Node\Stmt\GroupUse;
12use PhpParser\Node\Stmt\Use_;
13use PhpParser\Node\UseItem;
14
15class 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