1: | <?php declare(strict_types=1); |
2: | |
3: | namespace PhpParser; |
4: | |
5: | use PhpParser\Node\Expr\Include_; |
6: | use PhpParser\Node\Stmt\Class_; |
7: | use PhpParser\Node\Stmt\GroupUse; |
8: | use PhpParser\Node\Stmt\Use_; |
9: | use PhpParser\Node\Stmt\UseUse; |
10: | |
11: | class NodeDumper |
12: | { |
13: | private $dumpComments; |
14: | private $dumpPositions; |
15: | private $code; |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | |
23: | |
24: | |
25: | |
26: | |
27: | public function __construct(array $options = []) { |
28: | $this->dumpComments = !empty($options['dumpComments']); |
29: | $this->dumpPositions = !empty($options['dumpPositions']); |
30: | } |
31: | |
32: | |
33: | |
34: | |
35: | |
36: | |
37: | |
38: | |
39: | |
40: | |
41: | |
42: | public function dump($node, ?string $code = null) : string { |
43: | $this->code = $code; |
44: | return $this->dumpRecursive($node); |
45: | } |
46: | |
47: | protected function dumpRecursive($node) { |
48: | if ($node instanceof Node) { |
49: | $r = $node->getType(); |
50: | if ($this->dumpPositions && null !== $p = $this->dumpPosition($node)) { |
51: | $r .= $p; |
52: | } |
53: | $r .= '('; |
54: | |
55: | foreach ($node->getSubNodeNames() as $key) { |
56: | $r .= "\n " . $key . ': '; |
57: | |
58: | $value = $node->$key; |
59: | if (null === $value) { |
60: | $r .= 'null'; |
61: | } elseif (false === $value) { |
62: | $r .= 'false'; |
63: | } elseif (true === $value) { |
64: | $r .= 'true'; |
65: | } elseif (is_scalar($value)) { |
66: | if ('flags' === $key || 'newModifier' === $key) { |
67: | $r .= $this->dumpFlags($value); |
68: | } elseif ('type' === $key && $node instanceof Include_) { |
69: | $r .= $this->dumpIncludeType($value); |
70: | } elseif ('type' === $key |
71: | && ($node instanceof Use_ || $node instanceof UseUse || $node instanceof GroupUse)) { |
72: | $r .= $this->dumpUseType($value); |
73: | } else { |
74: | $r .= $value; |
75: | } |
76: | } else { |
77: | $r .= str_replace("\n", "\n ", $this->dumpRecursive($value)); |
78: | } |
79: | } |
80: | |
81: | if ($this->dumpComments && $comments = $node->getComments()) { |
82: | $r .= "\n comments: " . str_replace("\n", "\n ", $this->dumpRecursive($comments)); |
83: | } |
84: | } elseif (is_array($node)) { |
85: | $r = 'array('; |
86: | |
87: | foreach ($node as $key => $value) { |
88: | $r .= "\n " . $key . ': '; |
89: | |
90: | if (null === $value) { |
91: | $r .= 'null'; |
92: | } elseif (false === $value) { |
93: | $r .= 'false'; |
94: | } elseif (true === $value) { |
95: | $r .= 'true'; |
96: | } elseif (is_scalar($value)) { |
97: | $r .= $value; |
98: | } else { |
99: | $r .= str_replace("\n", "\n ", $this->dumpRecursive($value)); |
100: | } |
101: | } |
102: | } elseif ($node instanceof Comment) { |
103: | return $node->getReformattedText(); |
104: | } else { |
105: | throw new \InvalidArgumentException('Can only dump nodes and arrays.'); |
106: | } |
107: | |
108: | return $r . "\n)"; |
109: | } |
110: | |
111: | protected function dumpFlags($flags) { |
112: | $strs = []; |
113: | if ($flags & Class_::MODIFIER_PUBLIC) { |
114: | $strs[] = 'MODIFIER_PUBLIC'; |
115: | } |
116: | if ($flags & Class_::MODIFIER_PROTECTED) { |
117: | $strs[] = 'MODIFIER_PROTECTED'; |
118: | } |
119: | if ($flags & Class_::MODIFIER_PRIVATE) { |
120: | $strs[] = 'MODIFIER_PRIVATE'; |
121: | } |
122: | if ($flags & Class_::MODIFIER_ABSTRACT) { |
123: | $strs[] = 'MODIFIER_ABSTRACT'; |
124: | } |
125: | if ($flags & Class_::MODIFIER_STATIC) { |
126: | $strs[] = 'MODIFIER_STATIC'; |
127: | } |
128: | if ($flags & Class_::MODIFIER_FINAL) { |
129: | $strs[] = 'MODIFIER_FINAL'; |
130: | } |
131: | if ($flags & Class_::MODIFIER_READONLY) { |
132: | $strs[] = 'MODIFIER_READONLY'; |
133: | } |
134: | |
135: | if ($strs) { |
136: | return implode(' | ', $strs) . ' (' . $flags . ')'; |
137: | } else { |
138: | return $flags; |
139: | } |
140: | } |
141: | |
142: | protected function dumpIncludeType($type) { |
143: | $map = [ |
144: | Include_::TYPE_INCLUDE => 'TYPE_INCLUDE', |
145: | Include_::TYPE_INCLUDE_ONCE => 'TYPE_INCLUDE_ONCE', |
146: | Include_::TYPE_REQUIRE => 'TYPE_REQUIRE', |
147: | Include_::TYPE_REQUIRE_ONCE => 'TYPE_REQUIRE_ONCE', |
148: | ]; |
149: | |
150: | if (!isset($map[$type])) { |
151: | return $type; |
152: | } |
153: | return $map[$type] . ' (' . $type . ')'; |
154: | } |
155: | |
156: | protected function dumpUseType($type) { |
157: | $map = [ |
158: | Use_::TYPE_UNKNOWN => 'TYPE_UNKNOWN', |
159: | Use_::TYPE_NORMAL => 'TYPE_NORMAL', |
160: | Use_::TYPE_FUNCTION => 'TYPE_FUNCTION', |
161: | Use_::TYPE_CONSTANT => 'TYPE_CONSTANT', |
162: | ]; |
163: | |
164: | if (!isset($map[$type])) { |
165: | return $type; |
166: | } |
167: | return $map[$type] . ' (' . $type . ')'; |
168: | } |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | |
176: | |
177: | protected function dumpPosition(Node $node) { |
178: | if (!$node->hasAttribute('startLine') || !$node->hasAttribute('endLine')) { |
179: | return null; |
180: | } |
181: | |
182: | $start = $node->getStartLine(); |
183: | $end = $node->getEndLine(); |
184: | if ($node->hasAttribute('startFilePos') && $node->hasAttribute('endFilePos') |
185: | && null !== $this->code |
186: | ) { |
187: | $start .= ':' . $this->toColumn($this->code, $node->getStartFilePos()); |
188: | $end .= ':' . $this->toColumn($this->code, $node->getEndFilePos()); |
189: | } |
190: | return "[$start - $end]"; |
191: | } |
192: | |
193: | |
194: | private function toColumn($code, $pos) { |
195: | if ($pos > strlen($code)) { |
196: | throw new \RuntimeException('Invalid position information'); |
197: | } |
198: | |
199: | $lineStartPos = strrpos($code, "\n", $pos - strlen($code)); |
200: | if (false === $lineStartPos) { |
201: | $lineStartPos = -1; |
202: | } |
203: | |
204: | return $pos - $lineStartPos; |
205: | } |
206: | } |
207: | |