From 9373a8e9f551516bc8e42aedeacd1b4f635d27fc Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Fri, 18 Aug 2017 23:56:12 +0200 Subject: [PATCH] Implement JsonDecoder Converts JSON representation back into node tree. --- ...3_Other_node_tree_representations.markdown | 18 +++- lib/PhpParser/JsonDecoder.php | 98 +++++++++++++++++++ test/PhpParser/JsonDecoderTest.php | 44 +++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 lib/PhpParser/JsonDecoder.php create mode 100644 test/PhpParser/JsonDecoderTest.php diff --git a/doc/3_Other_node_tree_representations.markdown b/doc/3_Other_node_tree_representations.markdown index f59988a..effe1e3 100644 --- a/doc/3_Other_node_tree_representations.markdown +++ b/doc/3_Other_node_tree_representations.markdown @@ -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. \ No newline at end of file +The JSON representation may be converted back into a node tree using the `JsonDecoder`: + +```php +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. \ No newline at end of file diff --git a/lib/PhpParser/JsonDecoder.php b/lib/PhpParser/JsonDecoder.php new file mode 100644 index 0000000..832ac25 --- /dev/null +++ b/lib/PhpParser/JsonDecoder.php @@ -0,0 +1,98 @@ +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\""); + } +} \ No newline at end of file diff --git a/test/PhpParser/JsonDecoderTest.php b/test/PhpParser/JsonDecoderTest.php new file mode 100644 index 0000000..7e37ab9 --- /dev/null +++ b/test/PhpParser/JsonDecoderTest.php @@ -0,0 +1,44 @@ +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"'], + ]; + } +} \ No newline at end of file