300 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
| <?php declare(strict_types=1);
 | |
| 
 | |
| namespace PhpParser;
 | |
| 
 | |
| use PhpParser\Node\Expr\Array_;
 | |
| use PhpParser\Node\Expr\Include_;
 | |
| use PhpParser\Node\Expr\List_;
 | |
| use PhpParser\Node\Scalar\Int_;
 | |
| use PhpParser\Node\Scalar\InterpolatedString;
 | |
| use PhpParser\Node\Scalar\String_;
 | |
| use PhpParser\Node\Stmt\GroupUse;
 | |
| use PhpParser\Node\Stmt\Use_;
 | |
| use PhpParser\Node\UseItem;
 | |
| 
 | |
| class NodeDumper {
 | |
|     private bool $dumpComments;
 | |
|     private bool $dumpPositions;
 | |
|     private bool $dumpOtherAttributes;
 | |
|     private ?string $code;
 | |
|     private string $res;
 | |
|     private string $nl;
 | |
| 
 | |
|     private const IGNORE_ATTRIBUTES = [
 | |
|         'comments' => true,
 | |
|         'startLine' => true,
 | |
|         'endLine' => true,
 | |
|         'startFilePos' => true,
 | |
|         'endFilePos' => true,
 | |
|         'startTokenPos' => true,
 | |
|         'endTokenPos' => true,
 | |
|     ];
 | |
| 
 | |
|     /**
 | |
|      * Constructs a NodeDumper.
 | |
|      *
 | |
|      * Supported options:
 | |
|      *  * bool dumpComments: Whether comments should be dumped.
 | |
|      *  * bool dumpPositions: Whether line/offset information should be dumped. To dump offset
 | |
|      *                        information, the code needs to be passed to dump().
 | |
|      *  * bool dumpOtherAttributes: Whether non-comment, non-position attributes should be dumped.
 | |
|      *
 | |
|      * @param array $options Options (see description)
 | |
|      */
 | |
|     public function __construct(array $options = []) {
 | |
|         $this->dumpComments = !empty($options['dumpComments']);
 | |
|         $this->dumpPositions = !empty($options['dumpPositions']);
 | |
|         $this->dumpOtherAttributes = !empty($options['dumpOtherAttributes']);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Dumps a node or array.
 | |
|      *
 | |
|      * @param array|Node $node Node or array to dump
 | |
|      * @param string|null $code Code corresponding to dumped AST. This only needs to be passed if
 | |
|      *                          the dumpPositions option is enabled and the dumping of node offsets
 | |
|      *                          is desired.
 | |
|      *
 | |
|      * @return string Dumped value
 | |
|      */
 | |
|     public function dump($node, ?string $code = null): string {
 | |
|         $this->code = $code;
 | |
|         $this->res = '';
 | |
|         $this->nl = "\n";
 | |
|         $this->dumpRecursive($node, false);
 | |
|         return $this->res;
 | |
|     }
 | |
| 
 | |
|     /** @param mixed $node */
 | |
|     protected function dumpRecursive($node, bool $indent = true): void {
 | |
|         if ($indent) {
 | |
|             $this->nl .= "    ";
 | |
|         }
 | |
|         if ($node instanceof Node) {
 | |
|             $this->res .= $node->getType();
 | |
|             if ($this->dumpPositions && null !== $p = $this->dumpPosition($node)) {
 | |
|                 $this->res .= $p;
 | |
|             }
 | |
|             $this->res .= '(';
 | |
| 
 | |
|             foreach ($node->getSubNodeNames() as $key) {
 | |
|                 $this->res .= "$this->nl    " . $key . ': ';
 | |
| 
 | |
|                 $value = $node->$key;
 | |
|                 if (\is_int($value)) {
 | |
|                     if ('flags' === $key || 'newModifier' === $key) {
 | |
|                         $this->res .= $this->dumpFlags($value);
 | |
|                         continue;
 | |
|                     }
 | |
|                     if ('type' === $key && $node instanceof Include_) {
 | |
|                         $this->res .= $this->dumpIncludeType($value);
 | |
|                         continue;
 | |
|                     }
 | |
|                     if ('type' === $key
 | |
|                             && ($node instanceof Use_ || $node instanceof UseItem || $node instanceof GroupUse)) {
 | |
|                         $this->res .= $this->dumpUseType($value);
 | |
|                         continue;
 | |
|                     }
 | |
|                 }
 | |
|                 $this->dumpRecursive($value);
 | |
|             }
 | |
| 
 | |
|             if ($this->dumpComments && $comments = $node->getComments()) {
 | |
|                 $this->res .= "$this->nl    comments: ";
 | |
|                 $this->dumpRecursive($comments);
 | |
|             }
 | |
| 
 | |
|             if ($this->dumpOtherAttributes) {
 | |
|                 foreach ($node->getAttributes() as $key => $value) {
 | |
|                     if (isset(self::IGNORE_ATTRIBUTES[$key])) {
 | |
|                         continue;
 | |
|                     }
 | |
| 
 | |
|                     $this->res .= "$this->nl    $key: ";
 | |
|                     if (\is_int($value)) {
 | |
|                         if ('kind' === $key) {
 | |
|                             if ($node instanceof Int_) {
 | |
|                                 $this->res .= $this->dumpIntKind($value);
 | |
|                                 continue;
 | |
|                             }
 | |
|                             if ($node instanceof String_ || $node instanceof InterpolatedString) {
 | |
|                                 $this->res .= $this->dumpStringKind($value);
 | |
|                                 continue;
 | |
|                             }
 | |
|                             if ($node instanceof Array_) {
 | |
|                                 $this->res .= $this->dumpArrayKind($value);
 | |
|                                 continue;
 | |
|                             }
 | |
|                             if ($node instanceof List_) {
 | |
|                                 $this->res .= $this->dumpListKind($value);
 | |
|                                 continue;
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                     $this->dumpRecursive($value);
 | |
|                 }
 | |
|             }
 | |
|             $this->res .= "$this->nl)";
 | |
|         } elseif (\is_array($node)) {
 | |
|             $this->res .= 'array(';
 | |
|             foreach ($node as $key => $value) {
 | |
|                 $this->res .= "$this->nl    " . $key . ': ';
 | |
|                 $this->dumpRecursive($value);
 | |
|             }
 | |
|             $this->res .= "$this->nl)";
 | |
|         } elseif ($node instanceof Comment) {
 | |
|             $this->res .= \str_replace("\n", $this->nl, $node->getReformattedText());
 | |
|         } elseif (\is_string($node)) {
 | |
|             $this->res .= \str_replace("\n", $this->nl, (string)$node);
 | |
|         } elseif (\is_int($node) || \is_float($node)) {
 | |
|             $this->res .= $node;
 | |
|         } elseif (null === $node) {
 | |
|             $this->res .= 'null';
 | |
|         } elseif (false === $node) {
 | |
|             $this->res .= 'false';
 | |
|         } elseif (true === $node) {
 | |
|             $this->res .= 'true';
 | |
|         } else {
 | |
|             throw new \InvalidArgumentException('Can only dump nodes and arrays.');
 | |
|         }
 | |
|         if ($indent) {
 | |
|             $this->nl = \substr($this->nl, 0, -4);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     protected function dumpFlags(int $flags): string {
 | |
|         $strs = [];
 | |
|         if ($flags & Modifiers::PUBLIC) {
 | |
|             $strs[] = 'PUBLIC';
 | |
|         }
 | |
|         if ($flags & Modifiers::PROTECTED) {
 | |
|             $strs[] = 'PROTECTED';
 | |
|         }
 | |
|         if ($flags & Modifiers::PRIVATE) {
 | |
|             $strs[] = 'PRIVATE';
 | |
|         }
 | |
|         if ($flags & Modifiers::ABSTRACT) {
 | |
|             $strs[] = 'ABSTRACT';
 | |
|         }
 | |
|         if ($flags & Modifiers::STATIC) {
 | |
|             $strs[] = 'STATIC';
 | |
|         }
 | |
|         if ($flags & Modifiers::FINAL) {
 | |
|             $strs[] = 'FINAL';
 | |
|         }
 | |
|         if ($flags & Modifiers::READONLY) {
 | |
|             $strs[] = 'READONLY';
 | |
|         }
 | |
|         if ($flags & Modifiers::PUBLIC_SET) {
 | |
|             $strs[] = 'PUBLIC_SET';
 | |
|         }
 | |
|         if ($flags & Modifiers::PROTECTED_SET) {
 | |
|             $strs[] = 'PROTECTED_SET';
 | |
|         }
 | |
|         if ($flags & Modifiers::PRIVATE_SET) {
 | |
|             $strs[] = 'PRIVATE_SET';
 | |
|         }
 | |
| 
 | |
|         if ($strs) {
 | |
|             return implode(' | ', $strs) . ' (' . $flags . ')';
 | |
|         } else {
 | |
|             return (string) $flags;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /** @param array<int, string> $map */
 | |
|     private function dumpEnum(int $value, array $map): string {
 | |
|         if (!isset($map[$value])) {
 | |
|             return (string) $value;
 | |
|         }
 | |
|         return $map[$value] . ' (' . $value . ')';
 | |
|     }
 | |
| 
 | |
|     private function dumpIncludeType(int $type): string {
 | |
|         return $this->dumpEnum($type, [
 | |
|             Include_::TYPE_INCLUDE      => 'TYPE_INCLUDE',
 | |
|             Include_::TYPE_INCLUDE_ONCE => 'TYPE_INCLUDE_ONCE',
 | |
|             Include_::TYPE_REQUIRE      => 'TYPE_REQUIRE',
 | |
|             Include_::TYPE_REQUIRE_ONCE => 'TYPE_REQUIRE_ONCE',
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     private function dumpUseType(int $type): string {
 | |
|         return $this->dumpEnum($type, [
 | |
|             Use_::TYPE_UNKNOWN  => 'TYPE_UNKNOWN',
 | |
|             Use_::TYPE_NORMAL   => 'TYPE_NORMAL',
 | |
|             Use_::TYPE_FUNCTION => 'TYPE_FUNCTION',
 | |
|             Use_::TYPE_CONSTANT => 'TYPE_CONSTANT',
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     private function dumpIntKind(int $kind): string {
 | |
|         return $this->dumpEnum($kind, [
 | |
|             Int_::KIND_BIN => 'KIND_BIN',
 | |
|             Int_::KIND_OCT => 'KIND_OCT',
 | |
|             Int_::KIND_DEC => 'KIND_DEC',
 | |
|             Int_::KIND_HEX => 'KIND_HEX',
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     private function dumpStringKind(int $kind): string {
 | |
|         return $this->dumpEnum($kind, [
 | |
|             String_::KIND_SINGLE_QUOTED => 'KIND_SINGLE_QUOTED',
 | |
|             String_::KIND_DOUBLE_QUOTED => 'KIND_DOUBLE_QUOTED',
 | |
|             String_::KIND_HEREDOC => 'KIND_HEREDOC',
 | |
|             String_::KIND_NOWDOC => 'KIND_NOWDOC',
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     private function dumpArrayKind(int $kind): string {
 | |
|         return $this->dumpEnum($kind, [
 | |
|             Array_::KIND_LONG => 'KIND_LONG',
 | |
|             Array_::KIND_SHORT => 'KIND_SHORT',
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     private function dumpListKind(int $kind): string {
 | |
|         return $this->dumpEnum($kind, [
 | |
|             List_::KIND_LIST => 'KIND_LIST',
 | |
|             List_::KIND_ARRAY => 'KIND_ARRAY',
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Dump node position, if possible.
 | |
|      *
 | |
|      * @param Node $node Node for which to dump position
 | |
|      *
 | |
|      * @return string|null Dump of position, or null if position information not available
 | |
|      */
 | |
|     protected function dumpPosition(Node $node): ?string {
 | |
|         if (!$node->hasAttribute('startLine') || !$node->hasAttribute('endLine')) {
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
|         $start = $node->getStartLine();
 | |
|         $end = $node->getEndLine();
 | |
|         if ($node->hasAttribute('startFilePos') && $node->hasAttribute('endFilePos')
 | |
|             && null !== $this->code
 | |
|         ) {
 | |
|             $start .= ':' . $this->toColumn($this->code, $node->getStartFilePos());
 | |
|             $end .= ':' . $this->toColumn($this->code, $node->getEndFilePos());
 | |
|         }
 | |
|         return "[$start - $end]";
 | |
|     }
 | |
| 
 | |
|     // Copied from Error class
 | |
|     private function toColumn(string $code, int $pos): int {
 | |
|         if ($pos > strlen($code)) {
 | |
|             throw new \RuntimeException('Invalid position information');
 | |
|         }
 | |
| 
 | |
|         $lineStartPos = strrpos($code, "\n", $pos - strlen($code));
 | |
|         if (false === $lineStartPos) {
 | |
|             $lineStartPos = -1;
 | |
|         }
 | |
| 
 | |
|         return $pos - $lineStartPos;
 | |
|     }
 | |
| }
 |