php-parser-py 1.2.4__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- php_parser_py/__init__.py +33 -0
- php_parser_py/_exceptions.py +58 -0
- php_parser_py/_graph/__init__.py +19 -0
- php_parser_py/_graph/_ast.py +227 -0
- php_parser_py/_graph/_edge.py +102 -0
- php_parser_py/_graph/_modifier.py +143 -0
- php_parser_py/_graph/_node.py +256 -0
- php_parser_py/_graph/_node_id.py +56 -0
- php_parser_py/_mapping/__init__.py +14 -0
- php_parser_py/_mapping/_build.py +227 -0
- php_parser_py/_mapping/_contract.py +71 -0
- php_parser_py/_mapping/_serialize.py +220 -0
- php_parser_py/_parser.py +311 -0
- php_parser_py/_printer.py +147 -0
- php_parser_py/_resources/ExtendedPrinter.php +212 -0
- php_parser_py/_resources/parse.php +112 -0
- php_parser_py/_resources/php +0 -0
- php_parser_py/_resources/php-parser-5.7.0.tar.gz +0 -0
- php_parser_py/_resources/php.exe +0 -0
- php_parser_py/_resources/print.php +150 -0
- php_parser_py/_resources/transport.php +195 -0
- php_parser_py/_runtime/__init__.py +11 -0
- php_parser_py/_runtime/_binary.py +113 -0
- php_parser_py/_runtime/_bundle.py +164 -0
- php_parser_py/_runtime/_runner.py +258 -0
- php_parser_py/_transport.py +104 -0
- php_parser_py/_types.py +87 -0
- php_parser_py/py.typed +0 -0
- php_parser_py-1.2.4.dist-info/METADATA +100 -0
- php_parser_py-1.2.4.dist-info/RECORD +32 -0
- php_parser_py-1.2.4.dist-info/WHEEL +5 -0
- php_parser_py-1.2.4.dist-info/licenses/LICENSE +339 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Blv;
|
|
6
|
+
|
|
7
|
+
use PhpParser\Node;
|
|
8
|
+
use PhpParser\Node\Expr\Assign;
|
|
9
|
+
use PhpParser\Node\Expr\CallLike;
|
|
10
|
+
use PhpParser\Node\Expr\Instanceof_;
|
|
11
|
+
use PhpParser\Node\Expr\Yield_;
|
|
12
|
+
use PhpParser\Node\Scalar\String_;
|
|
13
|
+
use PhpParser\Node\Stmt\Declare_;
|
|
14
|
+
use PhpParser\Node\Stmt\Nop;
|
|
15
|
+
use PhpParser\PrettyPrinter\Standard;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Attribute marking a node whose printed form must be wrapped in parens.
|
|
19
|
+
*/
|
|
20
|
+
const FORMAT_WRAP_PARENS = '__format_wrap_parens';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Attribute marking a string node whose value is emitted raw (unescaped).
|
|
24
|
+
*/
|
|
25
|
+
const FORMAT_REGEX = '__format_regex';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Standard printer with selective printer-bug fixes.
|
|
29
|
+
*
|
|
30
|
+
* Each override defers to its parent method and transforms the result, or
|
|
31
|
+
* rebuilds the same output from parent-shaped parts. Overrides affect only
|
|
32
|
+
* reprinted (modified or inserted) nodes, so an unmodified round-trip is
|
|
33
|
+
* inert.
|
|
34
|
+
*/
|
|
35
|
+
class ExtendedPrinter extends Standard
|
|
36
|
+
{
|
|
37
|
+
/**
|
|
38
|
+
* Reindex statements from 0 and guarantee a trailing PHP_EOL.
|
|
39
|
+
*/
|
|
40
|
+
public function prettyPrintFile(array $stmts): string
|
|
41
|
+
{
|
|
42
|
+
$stmts = array_values($stmts);
|
|
43
|
+
$code = parent::prettyPrintFile($stmts);
|
|
44
|
+
if (!str_ends_with($code, PHP_EOL)) {
|
|
45
|
+
$code .= PHP_EOL;
|
|
46
|
+
}
|
|
47
|
+
return $code;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Print declare(...) with a whitespace-free header.
|
|
52
|
+
*
|
|
53
|
+
* The parent emits "declare (" and ", " item separators. A regex over
|
|
54
|
+
* its output would also mangle a block body or a string item value, so
|
|
55
|
+
* the statement is rebuilt with parent-shaped parts instead.
|
|
56
|
+
*/
|
|
57
|
+
protected function pStmt_Declare(Declare_ $node): string
|
|
58
|
+
{
|
|
59
|
+
$items = array_map(fn (Node $item): string => $this->p($item), $node->declares);
|
|
60
|
+
$header = 'declare(' . implode(',', $items) . ')';
|
|
61
|
+
if ($node->stmts === null) {
|
|
62
|
+
return $header . ';';
|
|
63
|
+
}
|
|
64
|
+
return $header . ' {' . $this->pStmts($node->stmts) . $this->nl . '}';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Strip leading whitespace on blank lines produced before a Nop node.
|
|
69
|
+
*/
|
|
70
|
+
protected function pArray(
|
|
71
|
+
array $nodes,
|
|
72
|
+
array $origNodes,
|
|
73
|
+
int &$pos,
|
|
74
|
+
int $indentAdjustment,
|
|
75
|
+
string $parentNodeClass,
|
|
76
|
+
string $subNodeName,
|
|
77
|
+
?int $fixup
|
|
78
|
+
): ?string {
|
|
79
|
+
$code = parent::pArray(
|
|
80
|
+
$nodes,
|
|
81
|
+
$origNodes,
|
|
82
|
+
$pos,
|
|
83
|
+
$indentAdjustment,
|
|
84
|
+
$parentNodeClass,
|
|
85
|
+
$subNodeName,
|
|
86
|
+
$fixup
|
|
87
|
+
);
|
|
88
|
+
if ($code === null) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
$hasNop = false;
|
|
92
|
+
foreach ($nodes as $node) {
|
|
93
|
+
if ($node instanceof Nop) {
|
|
94
|
+
$hasNop = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!$hasNop) {
|
|
99
|
+
return $code;
|
|
100
|
+
}
|
|
101
|
+
return preg_replace('/^[ \t]+$/m', '', $code);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mark an assignment left operand for parenthesization.
|
|
106
|
+
*
|
|
107
|
+
* When the left operand is itself an assignment that came from the
|
|
108
|
+
* original source while the right operand was created after parsing,
|
|
109
|
+
* the reprinted form needs explicit parentheses to preserve precedence.
|
|
110
|
+
*/
|
|
111
|
+
private function wrapAssign(Node $left, Node $right): void
|
|
112
|
+
{
|
|
113
|
+
if ($left instanceof Assign
|
|
114
|
+
&& $left->getStartTokenPos() > 0
|
|
115
|
+
&& $right->getStartTokenPos() < 0
|
|
116
|
+
) {
|
|
117
|
+
$left->setAttribute(FORMAT_WRAP_PARENS, true);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Flag a chained-assignment left operand before infix printing.
|
|
123
|
+
*/
|
|
124
|
+
protected function pInfixOp(
|
|
125
|
+
string $class,
|
|
126
|
+
Node $leftNode,
|
|
127
|
+
string $operatorString,
|
|
128
|
+
Node $rightNode,
|
|
129
|
+
int $precedence,
|
|
130
|
+
int $lhsPrecedence
|
|
131
|
+
): string {
|
|
132
|
+
$this->wrapAssign($leftNode, $rightNode);
|
|
133
|
+
return parent::pInfixOp(
|
|
134
|
+
$class,
|
|
135
|
+
$leftNode,
|
|
136
|
+
$operatorString,
|
|
137
|
+
$rightNode,
|
|
138
|
+
$precedence,
|
|
139
|
+
$lhsPrecedence
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Flag a chained-assignment left operand before instanceof printing.
|
|
145
|
+
*/
|
|
146
|
+
protected function pExpr_Instanceof(
|
|
147
|
+
Instanceof_ $node,
|
|
148
|
+
int $precedence,
|
|
149
|
+
int $lhsPrecedence
|
|
150
|
+
): string {
|
|
151
|
+
$this->wrapAssign($node->expr, $node->class);
|
|
152
|
+
return parent::pExpr_Instanceof($node, $precedence, $lhsPrecedence);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parenthesize a yield used as an assigned value.
|
|
157
|
+
*/
|
|
158
|
+
protected function pExpr_Assign(
|
|
159
|
+
Assign $node,
|
|
160
|
+
int $precedence,
|
|
161
|
+
int $lhsPrecedence
|
|
162
|
+
): string {
|
|
163
|
+
if ($node->expr instanceof Yield_) {
|
|
164
|
+
$node->expr->setAttribute(FORMAT_WRAP_PARENS, true);
|
|
165
|
+
}
|
|
166
|
+
return parent::pExpr_Assign($node, $precedence, $lhsPrecedence);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Wrap a node's printed form in parentheses when flagged.
|
|
171
|
+
*/
|
|
172
|
+
protected function p(
|
|
173
|
+
Node $node,
|
|
174
|
+
int $precedence = self::MAX_PRECEDENCE,
|
|
175
|
+
int $lhsPrecedence = self::MAX_PRECEDENCE,
|
|
176
|
+
bool $parentFormatPreserved = false
|
|
177
|
+
): string {
|
|
178
|
+
$result = parent::p($node, $precedence, $lhsPrecedence, $parentFormatPreserved);
|
|
179
|
+
if ($node instanceof CallLike && $node->isFirstClassCallable()) {
|
|
180
|
+
$result = $this->dropPlaceholderComma($result);
|
|
181
|
+
}
|
|
182
|
+
if ($node->getAttribute(FORMAT_WRAP_PARENS)) {
|
|
183
|
+
return '(' . $result . ')';
|
|
184
|
+
}
|
|
185
|
+
return $result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Remove a stray comma left after a first-class-callable placeholder.
|
|
190
|
+
*
|
|
191
|
+
* Format-preserving printing copies the original inner formatting around
|
|
192
|
+
* the argument list. When the call gains a "..." placeholder, the copied
|
|
193
|
+
* trailing comma becomes invalid syntax; drop it while keeping spacing.
|
|
194
|
+
* The printed call ends with its own argument list, so the anchor keeps
|
|
195
|
+
* the pattern away from an identical byte run inside a string literal.
|
|
196
|
+
*/
|
|
197
|
+
private function dropPlaceholderComma(string $code): string
|
|
198
|
+
{
|
|
199
|
+
return preg_replace('/(\.\.\.\s*),(\s*\))\z/', '$1$2', $code);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Emit a regex string verbatim when the node is flagged.
|
|
204
|
+
*/
|
|
205
|
+
protected function pScalar_String(String_ $node): string
|
|
206
|
+
{
|
|
207
|
+
if ($node->getAttribute(FORMAT_REGEX)) {
|
|
208
|
+
return $node->value;
|
|
209
|
+
}
|
|
210
|
+
return parent::pScalar_String($node);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse PHP source into a JSON AST.
|
|
7
|
+
*
|
|
8
|
+
* Reads one JSON object from stdin: {"phar_path": string, "code": string}.
|
|
9
|
+
* The code field is base64: JSON cannot carry the raw non-UTF-8 bytes a
|
|
10
|
+
* PHP source file may legally contain. Emits {"errors": [...]} with exit 0
|
|
11
|
+
* on syntax errors, the serialized statements on success, or
|
|
12
|
+
* {"error": ...} with exit 1 on any other failure.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
require __DIR__ . '/transport.php';
|
|
16
|
+
|
|
17
|
+
error_reporting(E_ALL & ~E_DEPRECATED);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Report a json_encode failure as an error envelope instead of silently
|
|
21
|
+
* printing nothing (json_encode returns false). Never returns.
|
|
22
|
+
*/
|
|
23
|
+
function emit_encode_failure(): void
|
|
24
|
+
{
|
|
25
|
+
// Scrub invalid bytes so the envelope itself always encodes.
|
|
26
|
+
echo json_encode(
|
|
27
|
+
['error' => 'AST JSON encoding failed: ' . json_last_error_msg()],
|
|
28
|
+
JSON_INVALID_UTF8_SUBSTITUTE
|
|
29
|
+
);
|
|
30
|
+
exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Emit a diagnostic envelope as JSON. Error messages may quote raw source
|
|
35
|
+
* bytes; scrubbing them keeps the diagnostic deliverable.
|
|
36
|
+
*/
|
|
37
|
+
function emit_json($value): void
|
|
38
|
+
{
|
|
39
|
+
$json = json_encode($value, 0, PPPY_JSON_DEPTH);
|
|
40
|
+
if ($json === false) {
|
|
41
|
+
$json = json_encode($value, JSON_INVALID_UTF8_SUBSTITUTE, PPPY_JSON_DEPTH);
|
|
42
|
+
}
|
|
43
|
+
if ($json === false) {
|
|
44
|
+
emit_encode_failure();
|
|
45
|
+
}
|
|
46
|
+
echo $json;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Emit the statements as JSON. String values holding invalid UTF-8 (or
|
|
51
|
+
* colliding with a marker prefix) and non-finite floats are wrapped in
|
|
52
|
+
* channel-safe transport markers, applied lazily so the common case
|
|
53
|
+
* skips the walk. JSON_PRESERVE_ZERO_FRACTION keeps whole-number floats
|
|
54
|
+
* float-typed: without it 2.0 emits as "2" and Python decodes an int.
|
|
55
|
+
*/
|
|
56
|
+
function emit_stmts(?array $stmts): void
|
|
57
|
+
{
|
|
58
|
+
$json = json_encode($stmts, JSON_PRESERVE_ZERO_FRACTION, PPPY_JSON_DEPTH);
|
|
59
|
+
$recoverable = [JSON_ERROR_UTF8, JSON_ERROR_INF_OR_NAN];
|
|
60
|
+
$needs_escape = ($json === false && in_array(json_last_error(), $recoverable, true))
|
|
61
|
+
|| ($json !== false && strpos($json, '\\u0000pppy') !== false);
|
|
62
|
+
if ($needs_escape) {
|
|
63
|
+
$json = json_encode(
|
|
64
|
+
pppy_encode_tree($stmts),
|
|
65
|
+
JSON_PRESERVE_ZERO_FRACTION,
|
|
66
|
+
PPPY_JSON_DEPTH
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if ($json === false) {
|
|
70
|
+
emit_encode_failure();
|
|
71
|
+
}
|
|
72
|
+
echo $json;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
$raw = file_get_contents('php://stdin');
|
|
76
|
+
$payload = json_decode($raw, true);
|
|
77
|
+
if ($payload === null) {
|
|
78
|
+
fwrite(STDERR, 'parse.php: payload decode failed: ' . json_last_error_msg()
|
|
79
|
+
. ' (bytes=' . strlen($raw) . ")\n");
|
|
80
|
+
exit(2);
|
|
81
|
+
}
|
|
82
|
+
require_once 'phar://' . $payload['phar_path'] . '/vendor/autoload.php';
|
|
83
|
+
|
|
84
|
+
use PhpParser\ErrorHandler\Collecting;
|
|
85
|
+
use PhpParser\ParserFactory;
|
|
86
|
+
|
|
87
|
+
$code = base64_decode($payload['code'], true);
|
|
88
|
+
if ($code === false) {
|
|
89
|
+
fwrite(STDERR, "parse.php: code field is not valid base64\n");
|
|
90
|
+
exit(2);
|
|
91
|
+
}
|
|
92
|
+
$errorHandler = new Collecting();
|
|
93
|
+
$parser = (new ParserFactory())->createForNewestSupportedVersion();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
$stmts = $parser->parse($code, $errorHandler);
|
|
97
|
+
if ($errorHandler->hasErrors()) {
|
|
98
|
+
$errors = array_map(fn($e) => [
|
|
99
|
+
'message' => $e->getMessage(),
|
|
100
|
+
'line' => $e->getStartLine(),
|
|
101
|
+
], $errorHandler->getErrors());
|
|
102
|
+
emit_json(['errors' => $errors]);
|
|
103
|
+
exit(0);
|
|
104
|
+
}
|
|
105
|
+
// PHP-Parser nodes implement JsonSerializable, so encode them directly.
|
|
106
|
+
emit_stmts($stmts);
|
|
107
|
+
} catch (Throwable $e) {
|
|
108
|
+
// Throwable, not Exception: engine-level Error must also become a
|
|
109
|
+
// reportable envelope instead of an uncaught fatal.
|
|
110
|
+
echo json_encode(['error' => $e->getMessage()], JSON_INVALID_UTF8_SUBSTITUTE);
|
|
111
|
+
exit(1);
|
|
112
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render a JSON AST back into PHP source.
|
|
7
|
+
*
|
|
8
|
+
* Reads one JSON object from stdin:
|
|
9
|
+
* {
|
|
10
|
+
* "phar_path": string, // php-parser.phar for the autoloader
|
|
11
|
+
* "extended_path": string, // Blv\ExtendedPrinter source file
|
|
12
|
+
* "printer_class": string, // FQCN to instantiate (read as data)
|
|
13
|
+
* "mode": string, // "full_rewrite" | "format_preserving"
|
|
14
|
+
* "stmts": string, // JSON of the (modified) statements
|
|
15
|
+
* "orig_source": string // original source (format_preserving only)
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* The printer class name is read from the payload and instantiated via a
|
|
19
|
+
* variable class expression; it is never spliced into source. Emits the
|
|
20
|
+
* generated PHP on success, or {"error": ...} with exit 1 on failure.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
require __DIR__ . '/transport.php';
|
|
24
|
+
|
|
25
|
+
error_reporting(E_ALL & ~E_DEPRECATED);
|
|
26
|
+
|
|
27
|
+
$raw = file_get_contents('php://stdin');
|
|
28
|
+
$payload = json_decode($raw, true);
|
|
29
|
+
if ($payload === null) {
|
|
30
|
+
fwrite(STDERR, 'print.php: payload decode failed: ' . json_last_error_msg()
|
|
31
|
+
. ' (bytes=' . strlen($raw) . ")\n");
|
|
32
|
+
exit(2);
|
|
33
|
+
}
|
|
34
|
+
require_once 'phar://' . $payload['phar_path'] . '/vendor/autoload.php';
|
|
35
|
+
require_once $payload['extended_path'];
|
|
36
|
+
|
|
37
|
+
use PhpParser\JsonDecoder;
|
|
38
|
+
use PhpParser\ParserFactory;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
$stmts = pppy_decode_stmts($payload['stmts']);
|
|
42
|
+
// Restore raw bytes and non-finite floats the Python side marker-wrapped.
|
|
43
|
+
$stmts = pppy_decode_tree($stmts);
|
|
44
|
+
|
|
45
|
+
// Normalize the FQCN and instantiate by name (class name travels as data).
|
|
46
|
+
$printerClass = '\\' . ltrim($payload['printer_class'], '\\');
|
|
47
|
+
$printer = new $printerClass();
|
|
48
|
+
|
|
49
|
+
// Output is base64-encoded so non-UTF-8 source bytes survive the text
|
|
50
|
+
// stdout channel back to the caller (mirrors the base64 orig_source input).
|
|
51
|
+
if ($payload['mode'] === 'format_preserving') {
|
|
52
|
+
$origSource = base64_decode($payload['orig_source']);
|
|
53
|
+
echo base64_encode(render_format_preserving($printer, $stmts, $origSource));
|
|
54
|
+
} else {
|
|
55
|
+
echo base64_encode($printer->prettyPrintFile($stmts));
|
|
56
|
+
}
|
|
57
|
+
} catch (Throwable $e) {
|
|
58
|
+
// Throwable, not Exception: engine-level Error (e.g. an unknown printer
|
|
59
|
+
// class) must also become a reportable envelope, not an uncaught fatal.
|
|
60
|
+
// Scrub invalid bytes so the envelope itself always encodes.
|
|
61
|
+
echo json_encode(['error' => $e->getMessage()], JSON_INVALID_UTF8_SUBSTITUTE);
|
|
62
|
+
exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decode the statements JSON and rebuild PhpParser nodes.
|
|
67
|
+
*
|
|
68
|
+
* parse.php encodes at PPPY_JSON_DEPTH, so the decode here must accept the
|
|
69
|
+
* same nesting; JsonDecoder::decode hard-codes json_decode's default
|
|
70
|
+
* 512-level cap, hence the JSON is decoded here and the private node
|
|
71
|
+
* reconstruction step is reached through reflection. A float marker in a
|
|
72
|
+
* node field position would hit a typed float property inside JsonDecoder
|
|
73
|
+
* (strict_types TypeError), so hoist markers into an untyped attribute
|
|
74
|
+
* first. Python's json.dumps escapes the marker NUL bytes, hence the
|
|
75
|
+
* escaped-needle check.
|
|
76
|
+
*/
|
|
77
|
+
function pppy_decode_stmts(string $stmtsJson): array
|
|
78
|
+
{
|
|
79
|
+
$data = json_decode($stmtsJson, true, PPPY_JSON_DEPTH);
|
|
80
|
+
if (json_last_error()) {
|
|
81
|
+
throw new RuntimeException(
|
|
82
|
+
'stmts decoding failed: ' . json_last_error_msg()
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (strpos($stmtsJson, '\\u0000pppyf') !== false) {
|
|
86
|
+
pppy_hoist_floats($data);
|
|
87
|
+
}
|
|
88
|
+
$reconstruct = new ReflectionMethod(JsonDecoder::class, 'decodeRecursive');
|
|
89
|
+
return $reconstruct->invoke(new JsonDecoder(), $data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Re-parse the original source, link unchanged decoded nodes to their original
|
|
94
|
+
* objects by token position, then reprint preserving original formatting.
|
|
95
|
+
*/
|
|
96
|
+
function render_format_preserving($printer, array $stmts, string $origSource): string
|
|
97
|
+
{
|
|
98
|
+
$parser = (new ParserFactory())->createForNewestSupportedVersion();
|
|
99
|
+
$origStmts = $parser->parse($origSource);
|
|
100
|
+
$origTokens = $parser->getTokens();
|
|
101
|
+
|
|
102
|
+
$index = [];
|
|
103
|
+
$collect = function ($nodes) use (&$collect, &$index) {
|
|
104
|
+
foreach ($nodes as $node) {
|
|
105
|
+
if (!$node instanceof PhpParser\Node) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
$start = $node->getStartTokenPos();
|
|
109
|
+
$end = $node->getEndTokenPos();
|
|
110
|
+
if ($start >= 0 && $end >= 0) {
|
|
111
|
+
$key = $start . ':' . $end . ':' . get_class($node);
|
|
112
|
+
$index[$key] = $node;
|
|
113
|
+
}
|
|
114
|
+
foreach ($node->getSubNodeNames() as $name) {
|
|
115
|
+
$child = $node->$name;
|
|
116
|
+
if ($child instanceof PhpParser\Node) {
|
|
117
|
+
$collect([$child]);
|
|
118
|
+
} elseif (is_array($child)) {
|
|
119
|
+
$collect($child);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
$collect($origStmts);
|
|
125
|
+
|
|
126
|
+
$link = function ($nodes) use (&$link, &$index) {
|
|
127
|
+
foreach ($nodes as $node) {
|
|
128
|
+
if (!$node instanceof PhpParser\Node) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
$start = $node->getStartTokenPos();
|
|
132
|
+
$end = $node->getEndTokenPos();
|
|
133
|
+
$key = $start . ':' . $end . ':' . get_class($node);
|
|
134
|
+
if ($start >= 0 && $end >= 0 && isset($index[$key])) {
|
|
135
|
+
$node->setAttribute('origNode', $index[$key]);
|
|
136
|
+
}
|
|
137
|
+
foreach ($node->getSubNodeNames() as $name) {
|
|
138
|
+
$child = $node->$name;
|
|
139
|
+
if ($child instanceof PhpParser\Node) {
|
|
140
|
+
$link([$child]);
|
|
141
|
+
} elseif (is_array($child)) {
|
|
142
|
+
$link($child);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
$link($stmts);
|
|
148
|
+
|
|
149
|
+
return $printer->printFormatPreserving($stmts, $origStmts, $origTokens);
|
|
150
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Byte-safe value codec for the Python JSON channel.
|
|
7
|
+
*
|
|
8
|
+
* JSON is UTF-8 only and rejects non-finite floats, but PHP source and
|
|
9
|
+
* string values are raw bytes and float values may be INF or NAN. A
|
|
10
|
+
* value that cannot travel as strict JSON is wrapped in a NUL-prefixed
|
|
11
|
+
* marker string; the Python side (_transport.py) applies the same
|
|
12
|
+
* codec. parse.php encodes before emitting; print.php decodes after
|
|
13
|
+
* JsonDecoder.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// json_encode/json_decode default to a 512-level nesting cap, rejecting
|
|
17
|
+
// deeply nested but valid ASTs; the engine's own parse recursion gives out
|
|
18
|
+
// long before this ceiling. One depth for both channel directions, so
|
|
19
|
+
// print.php accepts every AST parse.php emitted.
|
|
20
|
+
const PPPY_JSON_DEPTH = 1000000;
|
|
21
|
+
|
|
22
|
+
const PPPY_STR_MARKER = "\x00pppy\x00";
|
|
23
|
+
const PPPY_FLOAT_MARKER = "\x00pppyf\x00";
|
|
24
|
+
const PPPY_MARKER_PREFIX = "\x00pppy"; // common prefix of both markers
|
|
25
|
+
// Node attribute carrying hoisted non-finite float fields (see
|
|
26
|
+
// pppy_hoist_floats): {field name => 'inf'|'-inf'|'nan'}.
|
|
27
|
+
const PPPY_NONFINITE_ATTR = 'pppy_nonfinite';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a channel-unsafe value in a transport marker: a string that is
|
|
31
|
+
* not valid UTF-8 (or collides with a marker prefix), or a non-finite
|
|
32
|
+
* float.
|
|
33
|
+
*/
|
|
34
|
+
function pppy_encode_value($value)
|
|
35
|
+
{
|
|
36
|
+
if (is_string($value)) {
|
|
37
|
+
// json_encode doubles as the exact "will the channel accept it" check.
|
|
38
|
+
if (!str_starts_with($value, PPPY_MARKER_PREFIX)
|
|
39
|
+
&& json_encode($value) !== false
|
|
40
|
+
) {
|
|
41
|
+
return $value;
|
|
42
|
+
}
|
|
43
|
+
return PPPY_STR_MARKER . base64_encode($value);
|
|
44
|
+
}
|
|
45
|
+
if (is_float($value) && !is_finite($value)) {
|
|
46
|
+
return PPPY_FLOAT_MARKER . pppy_float_name($value);
|
|
47
|
+
}
|
|
48
|
+
return $value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Restore the original value of a marker-wrapped string. */
|
|
52
|
+
function pppy_decode_value($value)
|
|
53
|
+
{
|
|
54
|
+
if (!is_string($value) || !str_starts_with($value, PPPY_MARKER_PREFIX)) {
|
|
55
|
+
return $value;
|
|
56
|
+
}
|
|
57
|
+
if (str_starts_with($value, PPPY_STR_MARKER)) {
|
|
58
|
+
return base64_decode(substr($value, strlen(PPPY_STR_MARKER)));
|
|
59
|
+
}
|
|
60
|
+
if (str_starts_with($value, PPPY_FLOAT_MARKER)) {
|
|
61
|
+
return pppy_float_from_name(substr($value, strlen(PPPY_FLOAT_MARKER)));
|
|
62
|
+
}
|
|
63
|
+
return $value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Spell a non-finite float the way Python's repr() does. */
|
|
67
|
+
function pppy_float_name(float $value): string
|
|
68
|
+
{
|
|
69
|
+
if (is_nan($value)) {
|
|
70
|
+
return 'nan';
|
|
71
|
+
}
|
|
72
|
+
return $value === INF ? 'inf' : '-inf';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parse a Python-repr non-finite float name back to a PHP float. */
|
|
76
|
+
function pppy_float_from_name(string $name): float
|
|
77
|
+
{
|
|
78
|
+
switch ($name) {
|
|
79
|
+
case 'inf':
|
|
80
|
+
return INF;
|
|
81
|
+
case '-inf':
|
|
82
|
+
return -INF;
|
|
83
|
+
case 'nan':
|
|
84
|
+
return NAN;
|
|
85
|
+
}
|
|
86
|
+
throw new RuntimeException("Unknown transport float name: $name");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert nodes to their JSON-plain form with channel-unsafe leaves
|
|
91
|
+
* wrapped. Mirrors json_encode's JsonSerializable handling; typed node
|
|
92
|
+
* properties reject marker strings, the plain array form does not.
|
|
93
|
+
*/
|
|
94
|
+
function pppy_encode_tree($value)
|
|
95
|
+
{
|
|
96
|
+
if ($value instanceof JsonSerializable) {
|
|
97
|
+
return pppy_encode_tree($value->jsonSerialize());
|
|
98
|
+
}
|
|
99
|
+
if (is_array($value)) {
|
|
100
|
+
foreach ($value as $key => $item) {
|
|
101
|
+
$value[$key] = pppy_encode_tree($item);
|
|
102
|
+
}
|
|
103
|
+
return $value;
|
|
104
|
+
}
|
|
105
|
+
return pppy_encode_value($value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Move float markers out of node field positions into an untyped node
|
|
110
|
+
* attribute, leaving 0.0 behind. JsonDecoder assigns fields into typed
|
|
111
|
+
* node properties under strict_types, so a marker string in a
|
|
112
|
+
* float-typed field would throw; attributes travel as plain arrays.
|
|
113
|
+
*/
|
|
114
|
+
function pppy_hoist_floats(array &$value): void
|
|
115
|
+
{
|
|
116
|
+
foreach ($value as &$item) {
|
|
117
|
+
if (is_array($item)) {
|
|
118
|
+
pppy_hoist_floats($item);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
unset($item);
|
|
122
|
+
if (!isset($value['nodeType'])) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
foreach ($value as $field => $item) {
|
|
126
|
+
if ($field === 'nodeType' || $field === 'attributes') {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (is_string($item) && str_starts_with($item, PPPY_FLOAT_MARKER)) {
|
|
130
|
+
$value['attributes'][PPPY_NONFINITE_ATTR][$field] =
|
|
131
|
+
substr($item, strlen(PPPY_FLOAT_MARKER));
|
|
132
|
+
$value[$field] = 0.0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Restore marker-wrapped strings and hoisted non-finite floats across
|
|
139
|
+
* a decoded node tree: arrays, node subnodes, node attributes, and
|
|
140
|
+
* comment text.
|
|
141
|
+
*/
|
|
142
|
+
function pppy_decode_tree($value)
|
|
143
|
+
{
|
|
144
|
+
if (is_string($value)) {
|
|
145
|
+
return pppy_decode_value($value);
|
|
146
|
+
}
|
|
147
|
+
if (is_array($value)) {
|
|
148
|
+
foreach ($value as $key => $item) {
|
|
149
|
+
$value[$key] = pppy_decode_tree($item);
|
|
150
|
+
}
|
|
151
|
+
return $value;
|
|
152
|
+
}
|
|
153
|
+
if ($value instanceof PhpParser\Comment) {
|
|
154
|
+
return pppy_decode_comment($value);
|
|
155
|
+
}
|
|
156
|
+
if ($value instanceof PhpParser\Node) {
|
|
157
|
+
pppy_decode_node($value);
|
|
158
|
+
}
|
|
159
|
+
return $value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Decode one node in place: subnodes, attributes, hoisted floats. */
|
|
163
|
+
function pppy_decode_node(PhpParser\Node $node): void
|
|
164
|
+
{
|
|
165
|
+
foreach ($node->getSubNodeNames() as $name) {
|
|
166
|
+
$node->$name = pppy_decode_tree($node->$name);
|
|
167
|
+
}
|
|
168
|
+
$attrs = pppy_decode_tree($node->getAttributes());
|
|
169
|
+
if (isset($attrs[PPPY_NONFINITE_ATTR])) {
|
|
170
|
+
foreach ($attrs[PPPY_NONFINITE_ATTR] as $field => $name) {
|
|
171
|
+
$node->$field = pppy_float_from_name($name);
|
|
172
|
+
}
|
|
173
|
+
unset($attrs[PPPY_NONFINITE_ATTR]);
|
|
174
|
+
}
|
|
175
|
+
$node->setAttributes($attrs);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Rebuild a comment with decoded text, keeping its positions. */
|
|
179
|
+
function pppy_decode_comment(PhpParser\Comment $comment)
|
|
180
|
+
{
|
|
181
|
+
$text = pppy_decode_value($comment->getText());
|
|
182
|
+
if ($text === $comment->getText()) {
|
|
183
|
+
return $comment;
|
|
184
|
+
}
|
|
185
|
+
$class = get_class($comment);
|
|
186
|
+
return new $class(
|
|
187
|
+
$text,
|
|
188
|
+
$comment->getStartLine(),
|
|
189
|
+
$comment->getStartFilePos(),
|
|
190
|
+
$comment->getStartTokenPos(),
|
|
191
|
+
$comment->getEndLine(),
|
|
192
|
+
$comment->getEndFilePos(),
|
|
193
|
+
$comment->getEndTokenPos()
|
|
194
|
+
);
|
|
195
|
+
}
|