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;
|
|
}
|
|
}
|