xref: /PHP-Parser/lib/PhpParser/JsonDecoder.php (revision 502b0909)
1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5class JsonDecoder {
6    /** @var \ReflectionClass<Node>[] Node type to reflection class map */
7    private array $reflectionClassCache;
8
9    /** @return mixed */
10    public function decode(string $json) {
11        $value = json_decode($json, true);
12        if (json_last_error()) {
13            throw new \RuntimeException('JSON decoding error: ' . json_last_error_msg());
14        }
15
16        return $this->decodeRecursive($value);
17    }
18
19    /**
20     * @param mixed $value
21     * @return mixed
22     */
23    private function decodeRecursive($value) {
24        if (\is_array($value)) {
25            if (isset($value['nodeType'])) {
26                if ($value['nodeType'] === 'Comment' || $value['nodeType'] === 'Comment_Doc') {
27                    return $this->decodeComment($value);
28                }
29                return $this->decodeNode($value);
30            }
31            return $this->decodeArray($value);
32        }
33        return $value;
34    }
35
36    private function decodeArray(array $array): array {
37        $decodedArray = [];
38        foreach ($array as $key => $value) {
39            $decodedArray[$key] = $this->decodeRecursive($value);
40        }
41        return $decodedArray;
42    }
43
44    private function decodeNode(array $value): Node {
45        $nodeType = $value['nodeType'];
46        if (!\is_string($nodeType)) {
47            throw new \RuntimeException('Node type must be a string');
48        }
49
50        $reflectionClass = $this->reflectionClassFromNodeType($nodeType);
51        $node = $reflectionClass->newInstanceWithoutConstructor();
52
53        if (isset($value['attributes'])) {
54            if (!\is_array($value['attributes'])) {
55                throw new \RuntimeException('Attributes must be an array');
56            }
57
58            $node->setAttributes($this->decodeArray($value['attributes']));
59        }
60
61        foreach ($value as $name => $subNode) {
62            if ($name === 'nodeType' || $name === 'attributes') {
63                continue;
64            }
65
66            $node->$name = $this->decodeRecursive($subNode);
67        }
68
69        return $node;
70    }
71
72    private function decodeComment(array $value): Comment {
73        $className = $value['nodeType'] === 'Comment' ? Comment::class : Comment\Doc::class;
74        if (!isset($value['text'])) {
75            throw new \RuntimeException('Comment must have text');
76        }
77
78        return new $className(
79            $value['text'],
80            $value['line'] ?? -1, $value['filePos'] ?? -1, $value['tokenPos'] ?? -1,
81            $value['endLine'] ?? -1, $value['endFilePos'] ?? -1, $value['endTokenPos'] ?? -1
82        );
83    }
84
85    /** @return \ReflectionClass<Node> */
86    private function reflectionClassFromNodeType(string $nodeType): \ReflectionClass {
87        if (!isset($this->reflectionClassCache[$nodeType])) {
88            $className = $this->classNameFromNodeType($nodeType);
89            $this->reflectionClassCache[$nodeType] = new \ReflectionClass($className);
90        }
91        return $this->reflectionClassCache[$nodeType];
92    }
93
94    /** @return class-string<Node> */
95    private function classNameFromNodeType(string $nodeType): string {
96        $className = 'PhpParser\\Node\\' . strtr($nodeType, '_', '\\');
97        if (class_exists($className)) {
98            return $className;
99        }
100
101        $className .= '_';
102        if (class_exists($className)) {
103            return $className;
104        }
105
106        throw new \RuntimeException("Unknown node type \"$nodeType\"");
107    }
108}
109