Skip to content

Commit

Permalink
Implement JsonDecoder
Browse files Browse the repository at this point in the history
Converts JSON representation back into node tree.
  • Loading branch information
nikic committed Aug 18, 2017
1 parent e2e99f2 commit 9373a8e
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 2 deletions.
18 changes: 16 additions & 2 deletions doc/3_Other_node_tree_representations.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -210,5 +210,19 @@ This will result in the following output (which includes attributes):
]
```

There is currently no mechanism to convert JSON back into a node tree. Furthermore, not all ASTs
can be JSON encoded. In particular, JSON only supports UTF-8 strings.
The JSON representation may be converted back into a node tree using the `JsonDecoder`:

This comment has been minimized.

Copy link
@zDCzz

zDCzz Aug 24, 2017

Hi


```php
<?php

$nodeDecoder = new PhpParser\NodeDecoder();
$ast = $nodeDecoder->decode($json);
```

Note that not all ASTs can be represented using JSON. In particular:

* JSON only supports UTF-8 strings.
* JSON does not support non-finite floating-point numbers. This can occur if the original source
code contains non-representable floating-pointing literals such as `1e1000`.

If the node tree is not representable in JSON, the initial `json_encode()` call will fail.
98 changes: 98 additions & 0 deletions lib/PhpParser/JsonDecoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php declare(strict_types=1);

namespace PhpParser;

class JsonDecoder {
/** @var \ReflectionClass[] Node type to reflection class map */
private $reflectionClassCache;

public function decode(string $json) {
$value = json_decode($json, true);
if (json_last_error()) {
throw new \RuntimeException('JSON decoding error: ' . json_last_error_msg());
}

return $this->decodeRecursive($value);
}

private function decodeRecursive($value) {
if (\is_array($value)) {
if (isset($value['nodeType'])) {
if ($value['nodeType'] === 'Comment' || $value['nodeType'] === 'Comment_Doc') {
return $this->decodeComment($value);
}
return $this->decodeNode($value);
}
return $this->decodeArray($value);
}
return $value;
}

private function decodeArray(array $array) : array {
$decodedArray = [];
foreach ($array as $key => $value) {
$decodedArray[$key] = $this->decodeRecursive($value);
}
return $decodedArray;
}

private function decodeNode(array $value) : Node {
$nodeType = $value['nodeType'];
if (!\is_string($nodeType)) {
throw new \RuntimeException('Node type must be a string');
}

$reflectionClass = $this->reflectionClassFromNodeType($nodeType);
/** @var Node $node */
$node = $reflectionClass->newInstanceWithoutConstructor();

if (isset($value['attributes'])) {
if (!\is_array($value['attributes'])) {
throw new \RuntimeException('Attributes must be an array');
}

$node->setAttributes($this->decodeArray($value['attributes']));
}

foreach ($value as $name => $subNode) {
if ($name === 'nodeType' || $name === 'attributes') {
continue;
}

$node->$name = $this->decodeRecursive($subNode);
}

return $node;
}

private function decodeComment(array $value) : Comment {
$className = $value['nodeType'] === 'Comment' ? Comment::class : Comment\Doc::class;
if (!isset($value['text'])) {
throw new \RuntimeException('Comment must have text');
}

return new $className($value['text'], $value['line'] ?? -1, $value['filePos'] ?? -1);
}

private function reflectionClassFromNodeType(string $nodeType) : \ReflectionClass {
if (!isset($this->reflectionClassCache[$nodeType])) {
$className = $this->classNameFromNodeType($nodeType);
$this->reflectionClassCache[$nodeType] = new \ReflectionClass($className);
}
return $this->reflectionClassCache[$nodeType];
}

private function classNameFromNodeType(string $nodeType) : string {
$className = 'PhpParser\\Node\\' . strtr($nodeType, '_', '\\');
if (class_exists($className)) {
return $className;
}

$className .= '_';
if (class_exists($className)) {
return $className;
}

throw new \RuntimeException("Unknown node type \"$nodeType\"");
}
}
44 changes: 44 additions & 0 deletions test/PhpParser/JsonDecoderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace PhpParser;

use PHPUnit\Framework\TestCase;

class JsonDecoderTest extends TestCase {
public function testRoundTrip() {
$code = <<<'PHP'
<?php
// comment
/** doc comment */
function functionName(&$a = 0, $b = 1.0) {
echo 'Foo';
}
PHP;

$parser = new Parser\Php7(new Lexer());
$stmts = $parser->parse($code);
$json = json_encode($stmts);

$jsonDecoder = new JsonDecoder();
$decodedStmts = $jsonDecoder->decode($json);
$this->assertEquals($stmts, $decodedStmts);
}

/** @dataProvider provideTestDecodingError */
public function testDecodingError($json, $expectedMessage) {
$jsonDecoder = new JsonDecoder();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage($expectedMessage);
$jsonDecoder->decode($json);
}

public function provideTestDecodingError() {
return [
['???', 'JSON decoding error: Syntax error'],
['{"nodeType":123}', 'Node type must be a string'],
['{"nodeType":"Name","attributes":123}', 'Attributes must be an array'],
['{"nodeType":"Comment"}', 'Comment must have text'],
['{"nodeType":"xxx"}', 'Unknown node type "xxx"'],
];
}
}

0 comments on commit 9373a8e

Please sign in to comment.