You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
6.6 KiB
203 lines
6.6 KiB
<?php declare(strict_types=1); |
|
|
|
namespace PhpParser; |
|
|
|
use PhpParser\Node\Expr\Include_; |
|
use PhpParser\Node\Stmt\Class_; |
|
use PhpParser\Node\Stmt\GroupUse; |
|
use PhpParser\Node\Stmt\Use_; |
|
use PhpParser\Node\Stmt\UseUse; |
|
|
|
class NodeDumper |
|
{ |
|
private $dumpComments; |
|
private $dumpPositions; |
|
private $code; |
|
|
|
/** |
|
* 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(). |
|
* |
|
* @param array $options Options (see description) |
|
*/ |
|
public function __construct(array $options = []) { |
|
$this->dumpComments = !empty($options['dumpComments']); |
|
$this->dumpPositions = !empty($options['dumpPositions']); |
|
} |
|
|
|
/** |
|
* 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; |
|
return $this->dumpRecursive($node); |
|
} |
|
|
|
protected function dumpRecursive($node) { |
|
if ($node instanceof Node) { |
|
$r = $node->getType(); |
|
if ($this->dumpPositions && null !== $p = $this->dumpPosition($node)) { |
|
$r .= $p; |
|
} |
|
$r .= '('; |
|
|
|
foreach ($node->getSubNodeNames() as $key) { |
|
$r .= "\n " . $key . ': '; |
|
|
|
$value = $node->$key; |
|
if (null === $value) { |
|
$r .= 'null'; |
|
} elseif (false === $value) { |
|
$r .= 'false'; |
|
} elseif (true === $value) { |
|
$r .= 'true'; |
|
} elseif (is_scalar($value)) { |
|
if ('flags' === $key || 'newModifier' === $key) { |
|
$r .= $this->dumpFlags($value); |
|
} elseif ('type' === $key && $node instanceof Include_) { |
|
$r .= $this->dumpIncludeType($value); |
|
} elseif ('type' === $key |
|
&& ($node instanceof Use_ || $node instanceof UseUse || $node instanceof GroupUse)) { |
|
$r .= $this->dumpUseType($value); |
|
} else { |
|
$r .= $value; |
|
} |
|
} else { |
|
$r .= str_replace("\n", "\n ", $this->dumpRecursive($value)); |
|
} |
|
} |
|
|
|
if ($this->dumpComments && $comments = $node->getComments()) { |
|
$r .= "\n comments: " . str_replace("\n", "\n ", $this->dumpRecursive($comments)); |
|
} |
|
} elseif (is_array($node)) { |
|
$r = 'array('; |
|
|
|
foreach ($node as $key => $value) { |
|
$r .= "\n " . $key . ': '; |
|
|
|
if (null === $value) { |
|
$r .= 'null'; |
|
} elseif (false === $value) { |
|
$r .= 'false'; |
|
} elseif (true === $value) { |
|
$r .= 'true'; |
|
} elseif (is_scalar($value)) { |
|
$r .= $value; |
|
} else { |
|
$r .= str_replace("\n", "\n ", $this->dumpRecursive($value)); |
|
} |
|
} |
|
} elseif ($node instanceof Comment) { |
|
return $node->getReformattedText(); |
|
} else { |
|
throw new \InvalidArgumentException('Can only dump nodes and arrays.'); |
|
} |
|
|
|
return $r . "\n)"; |
|
} |
|
|
|
protected function dumpFlags($flags) { |
|
$strs = []; |
|
if ($flags & Class_::MODIFIER_PUBLIC) { |
|
$strs[] = 'MODIFIER_PUBLIC'; |
|
} |
|
if ($flags & Class_::MODIFIER_PROTECTED) { |
|
$strs[] = 'MODIFIER_PROTECTED'; |
|
} |
|
if ($flags & Class_::MODIFIER_PRIVATE) { |
|
$strs[] = 'MODIFIER_PRIVATE'; |
|
} |
|
if ($flags & Class_::MODIFIER_ABSTRACT) { |
|
$strs[] = 'MODIFIER_ABSTRACT'; |
|
} |
|
if ($flags & Class_::MODIFIER_STATIC) { |
|
$strs[] = 'MODIFIER_STATIC'; |
|
} |
|
if ($flags & Class_::MODIFIER_FINAL) { |
|
$strs[] = 'MODIFIER_FINAL'; |
|
} |
|
|
|
if ($strs) { |
|
return implode(' | ', $strs) . ' (' . $flags . ')'; |
|
} else { |
|
return $flags; |
|
} |
|
} |
|
|
|
protected function dumpIncludeType($type) { |
|
$map = [ |
|
Include_::TYPE_INCLUDE => 'TYPE_INCLUDE', |
|
Include_::TYPE_INCLUDE_ONCE => 'TYPE_INCLUDE_ONCE', |
|
Include_::TYPE_REQUIRE => 'TYPE_REQUIRE', |
|
Include_::TYPE_REQUIRE_ONCE => 'TYPE_REQUIRE_ONCE', |
|
]; |
|
|
|
if (!isset($map[$type])) { |
|
return $type; |
|
} |
|
return $map[$type] . ' (' . $type . ')'; |
|
} |
|
|
|
protected function dumpUseType($type) { |
|
$map = [ |
|
Use_::TYPE_UNKNOWN => 'TYPE_UNKNOWN', |
|
Use_::TYPE_NORMAL => 'TYPE_NORMAL', |
|
Use_::TYPE_FUNCTION => 'TYPE_FUNCTION', |
|
Use_::TYPE_CONSTANT => 'TYPE_CONSTANT', |
|
]; |
|
|
|
if (!isset($map[$type])) { |
|
return $type; |
|
} |
|
return $map[$type] . ' (' . $type . ')'; |
|
} |
|
|
|
/** |
|
* 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) { |
|
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($code, $pos) { |
|
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; |
|
} |
|
}
|
|
|