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