From b02f8ac07dc4015119412ee17252582a3622c1c7 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Wed, 23 Nov 2016 22:25:17 +0100 Subject: [PATCH] Add support for dumping positions in NodeDumper --- lib/PhpParser/NodeDumper.php | 65 +++++++++++++++++++++++++++---- test/PhpParser/NodeDumperTest.php | 37 +++++++++++++++++- 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/lib/PhpParser/NodeDumper.php b/lib/PhpParser/NodeDumper.php index 36b36e516e..9946eb853a 100644 --- a/lib/PhpParser/NodeDumper.php +++ b/lib/PhpParser/NodeDumper.php @@ -11,27 +11,46 @@ class NodeDumper { private $dumpComments; + private $dumpPositions; + private $code; /** * Constructs a NodeDumper. * - * @param array $options Boolean option 'dumpComments' controls whether comments should be - * dumped + * 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 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) { + public function dump($node, $code = null) { + $this->code = $code; + return $this->dumpRecursive($node); + } + + protected function dumpRecursive($node) { if ($node instanceof Node) { - $r = $node->getType() . '('; + $r = $node->getType(); + if ($this->dumpPositions && null !== $p = $this->dumpPosition($node)) { + $r .= $p; + } + $r .= '('; foreach ($node->getSubNodeNames() as $key) { $r .= "\n " . $key . ': '; @@ -55,12 +74,12 @@ public function dump($node) { $r .= $value; } } else { - $r .= str_replace("\n", "\n ", $this->dump($value)); + $r .= str_replace("\n", "\n ", $this->dumpRecursive($value)); } } if ($this->dumpComments && $comments = $node->getAttribute('comments')) { - $r .= "\n comments: " . str_replace("\n", "\n ", $this->dump($comments)); + $r .= "\n comments: " . str_replace("\n", "\n ", $this->dumpRecursive($comments)); } } elseif (is_array($node)) { $r = 'array('; @@ -77,7 +96,7 @@ public function dump($node) { } elseif (is_scalar($value)) { $r .= $value; } else { - $r .= str_replace("\n", "\n ", $this->dump($value)); + $r .= str_replace("\n", "\n ", $this->dumpRecursive($value)); } } } elseif ($node instanceof Comment) { @@ -144,4 +163,34 @@ protected function dumpUseType($type) { } return $map[$type] . ' (' . $type . ')'; } + + protected function dumpPosition(Node $node) { + if (!$node->hasAttribute('startLine') || !$node->hasAttribute('endLine')) { + return null; + } + + $start = $node->getAttribute('startLine'); + $end = $node->getAttribute('endLine'); + if ($node->hasAttribute('startFilePos') && $node->hasAttribute('endFilePos') + && null !== $this->code + ) { + $start .= ':' . $this->toColumn($this->code, $node->getAttribute('startFilePos')); + $end .= ':' . $this->toColumn($this->code, $node->getAttribute('endFilePos')); + } + 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; + } } diff --git a/test/PhpParser/NodeDumperTest.php b/test/PhpParser/NodeDumperTest.php index 306bc20d54..4574a3126b 100644 --- a/test/PhpParser/NodeDumperTest.php +++ b/test/PhpParser/NodeDumperTest.php @@ -2,6 +2,8 @@ namespace PhpParser; +use SebastianBergmann\Diff\Parser; + class NodeDumperTest extends \PHPUnit_Framework_TestCase { private function canonicalize($string) { @@ -10,7 +12,6 @@ private function canonicalize($string) { /** * @dataProvider provideTestDump - * @covers PhpParser\NodeDumper::dump */ public function testDump($node, $dump) { $dumper = new NodeDumper; @@ -61,6 +62,40 @@ public function provideTestDump() { ); } + public function testDumpWithPositions() { + $parser = (new ParserFactory)->create( + ParserFactory::ONLY_PHP7, + new Lexer(['usedAttributes' => ['startLine', 'endLine', 'startFilePos', 'endFilePos']]) + ); + $dumper = new NodeDumper(['dumpPositions' => true]); + + $code = "parse($code); + $dump = $dumper->dump($stmts, $code); + + $this->assertSame($this->canonicalize($expected), $this->canonicalize($dump)); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Can only dump nodes and arrays.