| 1 |
<?php declare(strict_types=1); |
| 2 |
|
| 3 |
namespace PhpParser; |
| 4 |
|
| 5 |
class 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 |
|