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.
@@ -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
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ """PHP runtime layer: binary resolution, bundled archives, script runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "Runner",
7
+ "resolve_php",
8
+ ]
9
+
10
+ from ._binary import resolve_php
11
+ from ._runner import Runner