@ant-yasa/uast-parser-php 0.2.9 → 0.2.10

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.
Files changed (33) hide show
  1. package/dist/package.json +1 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +26 -0
  4. package/src/parser.ts +1234 -0
  5. package/tests/benchmark/base/advanced.php +20 -0
  6. package/tests/benchmark/base/advanced.php.json +1269 -0
  7. package/tests/benchmark/base/basic.php +3 -0
  8. package/tests/benchmark/base/basic.php.json +292 -0
  9. package/tests/benchmark/base/closure-class.php +24 -0
  10. package/tests/benchmark/base/closure-class.php.json +1623 -0
  11. package/tests/benchmark/base/control-flow.php +37 -0
  12. package/tests/benchmark/base/control-flow.php.json +2010 -0
  13. package/tests/benchmark/base/enum-trait-adapt.php +31 -0
  14. package/tests/benchmark/base/enum-trait-adapt.php.json +965 -0
  15. package/tests/benchmark/base/exprs.php +4 -0
  16. package/tests/benchmark/base/exprs.php.json +811 -0
  17. package/tests/benchmark/base/flow-extras.php +12 -0
  18. package/tests/benchmark/base/flow-extras.php.json +897 -0
  19. package/tests/benchmark/base/function.php +7 -0
  20. package/tests/benchmark/base/function.php.json +536 -0
  21. package/tests/benchmark/base/modern-php.php +13 -0
  22. package/tests/benchmark/base/modern-php.php.json +508 -0
  23. package/tests/benchmark/base/namespace.php +5 -0
  24. package/tests/benchmark/base/namespace.php.json +307 -0
  25. package/tests/benchmark/base/new-static.php +3 -0
  26. package/tests/benchmark/base/new-static.php.json +399 -0
  27. package/tests/benchmark/base/runtime.php +13 -0
  28. package/tests/benchmark/base/runtime.php.json +1095 -0
  29. package/tests/benchmark/base/strings-casts.php +17 -0
  30. package/tests/benchmark/base/strings-casts.php.json +1357 -0
  31. package/tests/benchmark/base/structures.php +26 -0
  32. package/tests/benchmark/base/structures.php.json +1582 -0
  33. package/tests/index.ts +87 -0
package/src/parser.ts ADDED
@@ -0,0 +1,1234 @@
1
+ import * as TreeSitter from 'web-tree-sitter';
2
+ import * as UAST from '@ant-yasa/uast-spec';
3
+ import { version } from '../package.json';
4
+
5
+ type SyntaxNode = TreeSitter.Node;
6
+
7
+ export type ParseResult<Result> = Result;
8
+
9
+ let parserInstance: TreeSitter.Parser | null = null;
10
+
11
+ /** 初始化 tree-sitter parser,加载 PHP WASM */
12
+ export async function init(): Promise<void> {
13
+ await TreeSitter.Parser.init();
14
+ const Lang = await TreeSitter.Language.load(
15
+ require.resolve('tree-sitter-php/tree-sitter-php.wasm')
16
+ );
17
+ parserInstance = new TreeSitter.Parser();
18
+ parserInstance.setLanguage(Lang);
19
+ }
20
+
21
+ function getParser(): TreeSitter.Parser {
22
+ if (!parserInstance) {
23
+ throw new Error('Parser not initialized. Call init() first.');
24
+ }
25
+ return parserInstance;
26
+ }
27
+
28
+ function toSourceLocation(node: SyntaxNode, sourcefile?: string) {
29
+ return {
30
+ start: {
31
+ line: node.startPosition.row + 1,
32
+ column: node.startPosition.column + 1,
33
+ },
34
+ end: {
35
+ line: node.endPosition.row + 1,
36
+ column: node.endPosition.column + 1,
37
+ },
38
+ sourcefile,
39
+ };
40
+ }
41
+
42
+ function appendNodeMeta(uastNode: any, tsNode: SyntaxNode | null | undefined, sourcefile?: string): any {
43
+ if (!uastNode || !UAST.isNode(uastNode)) {
44
+ return uastNode;
45
+ }
46
+
47
+ if (tsNode) {
48
+ const loc = toSourceLocation(tsNode, sourcefile);
49
+ uastNode.loc = loc;
50
+ uastNode._meta.loc = loc;
51
+ }
52
+
53
+ return uastNode;
54
+ }
55
+
56
+ function visitList(nodes: SyntaxNode[], opts: Record<string, any>): Array<any> {
57
+ return nodes
58
+ .map((node) => visit(node, opts))
59
+ .filter((node) => node !== null && node !== undefined);
60
+ }
61
+
62
+ function flattenInstructions(nodes: Array<any>): Array<any> {
63
+ const instructions: Array<any> = [];
64
+ for (const node of nodes) {
65
+ if (!node) continue;
66
+ if (UAST.isSequence(node)) {
67
+ instructions.push(...node.expressions);
68
+ continue;
69
+ }
70
+ instructions.push(node);
71
+ }
72
+ return instructions;
73
+ }
74
+
75
+ function mapBinaryOperator(operator: string | undefined): string {
76
+ if (!operator) {
77
+ return '+';
78
+ }
79
+ const mapping: Record<string, string> = {
80
+ '.': '+',
81
+ '??': '||',
82
+ '<=>': '-',
83
+ 'or': '||',
84
+ 'and': '&&',
85
+ 'xor': '^',
86
+ '<>': '!=',
87
+ };
88
+ // PHP 关键字大小写不敏感,统一转小写
89
+ const lower = operator.toLowerCase();
90
+ return mapping[lower] || lower;
91
+ }
92
+
93
+ function mapAssignOperator(operator: string): string {
94
+ const mapping: Record<string, string> = {
95
+ '.=': '+=',
96
+ '??=': '=',
97
+ '**=': '*=',
98
+ };
99
+ const validOperators = new Set(['=', '^=', '&=', '<<=', '>>=', '>>>=', '+=', '-=', '*=', '/=', '%=', '|=', '**=']);
100
+ if (validOperators.has(operator)) {
101
+ return operator;
102
+ }
103
+ return mapping[operator] || '=';
104
+ }
105
+
106
+ /** 去除字符串两端的引号 */
107
+ function stripQuotes(text: string): string {
108
+ if (text.length >= 2) {
109
+ const first = text[0];
110
+ const last = text[text.length - 1];
111
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
112
+ return text.slice(1, -1);
113
+ }
114
+ }
115
+ return text;
116
+ }
117
+
118
+ /** 从 heredoc/nowdoc 中提取内容(去掉标签行和结束标签行) */
119
+ function stripHeredoc(text: string): string {
120
+ const lines = text.split('\n');
121
+ // 第一行: <<<TAG 或 <<<'TAG'
122
+ // 最后一行: TAG;
123
+ if (lines.length >= 2) {
124
+ return lines.slice(1, -1).join('\n');
125
+ }
126
+ return text;
127
+ }
128
+
129
+ /** 从节点的匿名子节点中提取 operator 文本 */
130
+ function getOperator(node: SyntaxNode): string {
131
+ for (let i = 0; i < node.childCount; i++) {
132
+ const child = node.child(i)!;
133
+ if (!child.isNamed && child.text !== '(' && child.text !== ')' && child.text !== ',' && child.text !== ';') {
134
+ return child.text;
135
+ }
136
+ }
137
+ return '=';
138
+ }
139
+
140
+ /** 处理函数参数(simple_parameter / variadic_parameter / property_promotion_parameter) */
141
+ function createParameter(paramNode: SyntaxNode, opts: Record<string, any>): any {
142
+ const sourcefile = opts?.sourcefile as string | undefined;
143
+ const nameNode = paramNode.childForFieldName('name');
144
+ const id = nameNode ? visit(nameNode, opts) as UAST.Identifier : UAST.identifier('_');
145
+ const defaultValueNode = paramNode.childForFieldName('default_value');
146
+ const init = defaultValueNode ? visit(defaultValueNode, opts) as UAST.Expression : null;
147
+ const varDecl = UAST.variableDeclaration(id, init, false, UAST.dynamicType());
148
+ varDecl._meta.variadic = paramNode.type === 'variadic_parameter';
149
+ // property_promotion_parameter 的 visibility
150
+ if (paramNode.type === 'property_promotion_parameter') {
151
+ for (const child of paramNode.namedChildren) {
152
+ if (child.type === 'visibility_modifier') {
153
+ varDecl._meta.visibility = child.text;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+ return appendNodeMeta(varDecl, paramNode, sourcefile);
159
+ }
160
+
161
+ /** 构建函数定义(function_definition / method_declaration / anonymous_function) */
162
+ function createFunctionLike(node: SyntaxNode, opts: Record<string, any>): any {
163
+ const sourcefile = opts?.sourcefile as string | undefined;
164
+ const nameNode = node.childForFieldName('name');
165
+ const id = nameNode ? visit(nameNode, opts) as UAST.Identifier : null;
166
+ const paramsNode = node.childForFieldName('parameters');
167
+ const parameters = paramsNode
168
+ ? paramsNode.namedChildren.map((p) => createParameter(p, opts))
169
+ : [];
170
+ const bodyNode = node.childForFieldName('body');
171
+ const body = bodyNode
172
+ ? visit(bodyNode, opts) as UAST.Instruction
173
+ : appendNodeMeta(UAST.scopedStatement([]), node, sourcefile);
174
+ const modifiers: string[] = [];
175
+ for (const child of node.namedChildren) {
176
+ if (child.type === 'visibility_modifier' || child.type === 'static_modifier'
177
+ || child.type === 'abstract_modifier' || child.type === 'final_modifier'
178
+ || child.type === 'readonly_modifier') {
179
+ modifiers.push(child.text);
180
+ }
181
+ }
182
+ const fdef = UAST.functionDefinition(id, parameters, UAST.dynamicType(), body, modifiers);
183
+ return appendNodeMeta(fdef, node, sourcefile);
184
+ }
185
+
186
+ /** 构建类/接口/trait 定义 */
187
+ function createClassLike(node: SyntaxNode, opts: Record<string, any>, kind?: string): any {
188
+ const sourcefile = opts?.sourcefile as string | undefined;
189
+ const nameNode = node.childForFieldName('name');
190
+ const id = nameNode ? visit(nameNode, opts) as UAST.Identifier : null;
191
+ const bodyNode = node.childForFieldName('body');
192
+ const body = bodyNode
193
+ ? flattenInstructions(visitList(bodyNode.namedChildren, opts))
194
+ : [];
195
+ // extends
196
+ const supers: Array<UAST.Expression> = [];
197
+ const baseClause = node.childForFieldName('base_clause');
198
+ if (baseClause) {
199
+ for (const child of baseClause.namedChildren) {
200
+ const superExpr = visit(child, opts);
201
+ if (superExpr) supers.push(superExpr);
202
+ }
203
+ }
204
+ const cdef = UAST.classDefinition(id, body, supers);
205
+ // implements
206
+ const implementsClause = node.childForFieldName('class_interface_clause');
207
+ if (implementsClause) {
208
+ cdef._meta.implements = visitList(implementsClause.namedChildren, opts);
209
+ }
210
+ if (kind) {
211
+ cdef._meta.kind = kind;
212
+ }
213
+ // modifiers
214
+ for (const child of node.namedChildren) {
215
+ if (child.type === 'abstract_modifier') cdef._meta.isAbstract = true;
216
+ if (child.type === 'final_modifier') cdef._meta.isFinal = true;
217
+ if (child.type === 'readonly_modifier') cdef._meta.isReadonly = true;
218
+ }
219
+ return appendNodeMeta(cdef, node, sourcefile);
220
+ }
221
+
222
+ function visit(node: SyntaxNode | null | undefined, opts: Record<string, any>): any {
223
+ if (!node) {
224
+ return null;
225
+ }
226
+
227
+ const sourcefile = opts?.sourcefile as string | undefined;
228
+
229
+ switch (node.type) {
230
+ case 'program': {
231
+ const children = node.namedChildren.filter((child) => child.type !== 'php_tag');
232
+ const body = flattenInstructions(visitList(children, opts));
233
+ const compileUnit = UAST.compileUnit(body, 'php', null, sourcefile || '', version);
234
+ return appendNodeMeta(compileUnit, node, sourcefile);
235
+ }
236
+
237
+ case 'expression_statement': {
238
+ const child = node.namedChildren[0];
239
+ if (!child) {
240
+ return appendNodeMeta(UAST.noop(), node, sourcefile);
241
+ }
242
+ const result = visit(child, opts);
243
+ // throw_expression 等节点返回的是语句而非表达式,直接返回
244
+ if (UAST.isNode(result) && result.type.endsWith('Statement')) {
245
+ return appendNodeMeta(result, node, sourcefile);
246
+ }
247
+ const exprStmt = UAST.expressionStatement(result as UAST.Expression);
248
+ return appendNodeMeta(exprStmt, node, sourcefile);
249
+ }
250
+
251
+ case 'variable_name': {
252
+ // $variable -> 'variable'(variable_name 的 namedChild(0) 是 name 节点)
253
+ const nameNode = node.namedChildren[0];
254
+ const name = nameNode ? nameNode.text : node.text.replace(/^\$/, '');
255
+ return appendNodeMeta(UAST.identifier(name), node, sourcefile);
256
+ }
257
+
258
+ case 'dynamic_variable_name': {
259
+ // $$var -> 变量变量,递归访问内部节点
260
+ const inner = node.namedChildren[0];
261
+ if (inner) {
262
+ return visit(inner, opts);
263
+ }
264
+ return appendNodeMeta(UAST.identifier(node.text.replace(/^\$+/, '')), node, sourcefile);
265
+ }
266
+
267
+ case 'by_ref': {
268
+ // &$var -> 引用传递,直接访问内部变量
269
+ const inner = node.namedChildren[0];
270
+ if (inner) {
271
+ const result = visit(inner, opts);
272
+ if (UAST.isNode(result)) {
273
+ result._meta.byref = true;
274
+ }
275
+ return result;
276
+ }
277
+ return appendNodeMeta(UAST.noop(), node, sourcefile);
278
+ }
279
+
280
+ case 'name':
281
+ return appendNodeMeta(UAST.identifier(node.text), node, sourcefile);
282
+
283
+ case 'qualified_name':
284
+ return appendNodeMeta(UAST.identifier(node.text), node, sourcefile);
285
+
286
+ case 'namespace_name':
287
+ return appendNodeMeta(UAST.identifier(node.text), node, sourcefile);
288
+
289
+ case 'relative_scope':
290
+ // self / parent / static
291
+ return appendNodeMeta(UAST.identifier(node.text), node, sourcefile);
292
+
293
+ case 'integer':
294
+ return appendNodeMeta(UAST.literal(Number(node.text), 'number'), node, sourcefile);
295
+
296
+ case 'float':
297
+ return appendNodeMeta(UAST.literal(Number(node.text), 'number'), node, sourcefile);
298
+
299
+ case 'string':
300
+ return appendNodeMeta(UAST.literal(stripQuotes(node.text), 'string'), node, sourcefile);
301
+
302
+ case 'encapsed_string': {
303
+ // 含变量插值的双引号字符串,拆成 binary concat
304
+ const parts = node.namedChildren.map((child) => {
305
+ if (child.type === 'string_content') {
306
+ return appendNodeMeta(UAST.literal(child.text, 'string'), child, sourcefile);
307
+ }
308
+ return visit(child, opts);
309
+ });
310
+ if (parts.length === 0) {
311
+ return appendNodeMeta(UAST.literal('', 'string'), node, sourcefile);
312
+ }
313
+ let expr = parts[0];
314
+ for (let i = 1; i < parts.length; i++) {
315
+ expr = appendNodeMeta(UAST.binaryExpression('+' as any, expr, parts[i]), node, sourcefile);
316
+ }
317
+ if (expr && UAST.isNode(expr)) {
318
+ expr._meta.encapsed = true;
319
+ }
320
+ return appendNodeMeta(expr, node, sourcefile);
321
+ }
322
+
323
+ case 'heredoc': {
324
+ const content = stripHeredoc(node.text);
325
+ const literal = UAST.literal(content, 'string');
326
+ literal._meta.heredoc = true;
327
+ return appendNodeMeta(literal, node, sourcefile);
328
+ }
329
+
330
+ case 'nowdoc': {
331
+ const content = stripHeredoc(node.text);
332
+ const literal = UAST.literal(content, 'string');
333
+ literal._meta.nowdoc = true;
334
+ return appendNodeMeta(literal, node, sourcefile);
335
+ }
336
+
337
+ case 'boolean':
338
+ return appendNodeMeta(
339
+ UAST.literal(node.text.toLowerCase() === 'true', 'boolean'),
340
+ node,
341
+ sourcefile
342
+ );
343
+
344
+ case 'null':
345
+ return appendNodeMeta(UAST.literal(null, 'null'), node, sourcefile);
346
+
347
+ case 'argument': {
348
+ // argument 节点包装了一个表达式 child
349
+ const child = node.namedChildren[0];
350
+ return child ? visit(child, opts) : appendNodeMeta(UAST.noop(), node, sourcefile);
351
+ }
352
+
353
+ case 'parenthesized_expression': {
354
+ const child = node.namedChildren[0];
355
+ return child ? visit(child, opts) : appendNodeMeta(UAST.noop(), node, sourcefile);
356
+ }
357
+
358
+ case 'text_interpolation':
359
+ // HTML 文本片段,跳过
360
+ return null;
361
+
362
+ // ─── 赋值 ───
363
+
364
+ case 'assignment_expression': {
365
+ const left = visit(node.childForFieldName('left'), opts) as UAST.LVal;
366
+ const right = visit(node.childForFieldName('right'), opts) as UAST.Expression;
367
+ const op = mapAssignOperator(getOperator(node));
368
+ const assign = UAST.assignmentExpression(left, right, op as any, false);
369
+ return appendNodeMeta(assign, node, sourcefile);
370
+ }
371
+
372
+ case 'augmented_assignment_expression': {
373
+ const left = visit(node.childForFieldName('left'), opts) as UAST.LVal;
374
+ const right = visit(node.childForFieldName('right'), opts) as UAST.Expression;
375
+ const rawOp = getOperator(node);
376
+ const mappedOp = mapAssignOperator(rawOp);
377
+ const assign = UAST.assignmentExpression(left, right, mappedOp as any, false);
378
+ if (rawOp !== mappedOp) {
379
+ assign._meta.rawOperator = rawOp;
380
+ }
381
+ return appendNodeMeta(assign, node, sourcefile);
382
+ }
383
+
384
+ case 'reference_assignment_expression': {
385
+ const left = visit(node.childForFieldName('left'), opts) as UAST.LVal;
386
+ const right = visit(node.childForFieldName('right'), opts) as UAST.Expression;
387
+ const assign = UAST.assignmentExpression(left, right, '=', true);
388
+ assign._meta.byref = true;
389
+ return appendNodeMeta(assign, node, sourcefile);
390
+ }
391
+
392
+ // ─── 二元 / 条件 / 一元 ───
393
+
394
+ case 'binary_expression': {
395
+ const left = visit(node.childForFieldName('left'), opts) as UAST.Expression;
396
+ const right = visit(node.childForFieldName('right'), opts) as UAST.Expression;
397
+ const op = getOperator(node);
398
+ const binary = UAST.binaryExpression(mapBinaryOperator(op) as any, left, right);
399
+ return appendNodeMeta(binary, node, sourcefile);
400
+ }
401
+
402
+ case 'conditional_expression': {
403
+ const named = node.namedChildren;
404
+ if (named.length >= 3) {
405
+ // 完整三元:condition ? consequent : alternative
406
+ const cond = visit(named[0], opts) as UAST.Expression;
407
+ const consequent = visit(named[1], opts) as UAST.Expression;
408
+ const alternative = visit(named[2], opts) as UAST.Expression;
409
+ return appendNodeMeta(UAST.conditionalExpression(cond, consequent, alternative), node, sourcefile);
410
+ }
411
+ // PHP Elvis ?:(省略 consequent),只有 2 个 namedChildren
412
+ const cond = visit(named[0], opts) as UAST.Expression;
413
+ const alternative = visit(named[1], opts) as UAST.Expression;
414
+ // 复用 condition 作为 consequent
415
+ const condDup = visit(named[0], opts) as UAST.Expression;
416
+ return appendNodeMeta(UAST.conditionalExpression(cond, condDup, alternative), node, sourcefile);
417
+ }
418
+
419
+ case 'unary_op_expression': {
420
+ const op = getOperator(node);
421
+ const operand = visit(node.namedChildren[0], opts) as UAST.Expression;
422
+ const unary = UAST.unaryExpression(op as any, operand, false);
423
+ return appendNodeMeta(unary, node, sourcefile);
424
+ }
425
+
426
+ case 'update_expression': {
427
+ // 判断 prefix/postfix:第一个子节点是匿名(operator)则 prefix,否则 postfix
428
+ const firstChild = node.child(0)!;
429
+ const isPostfix = firstChild.isNamed;
430
+ const op = getOperator(node);
431
+ const operand = visit(node.namedChildren[0], opts) as UAST.Expression;
432
+ const unary = UAST.unaryExpression(op as any, operand, isPostfix);
433
+ return appendNodeMeta(unary, node, sourcefile);
434
+ }
435
+
436
+ // ─── 调用 ───
437
+
438
+ case 'function_call_expression': {
439
+ const callee = visit(node.childForFieldName('function'), opts) as UAST.Expression;
440
+ const argsNode = node.childForFieldName('arguments');
441
+ const args = argsNode ? visitList(argsNode.namedChildren, opts) as Array<UAST.Expression> : [];
442
+ const call = UAST.callExpression(callee, args);
443
+ return appendNodeMeta(call, node, sourcefile);
444
+ }
445
+
446
+ case 'member_call_expression': {
447
+ const object = visit(node.childForFieldName('object'), opts) as UAST.Expression;
448
+ const name = visit(node.childForFieldName('name'), opts) as UAST.Expression;
449
+ const memberCallee = UAST.memberAccess(object, name, false);
450
+ appendNodeMeta(memberCallee, node, sourcefile);
451
+ const argsNode = node.childForFieldName('arguments');
452
+ const args = argsNode ? visitList(argsNode.namedChildren, opts) as Array<UAST.Expression> : [];
453
+ const call = UAST.callExpression(memberCallee, args);
454
+ return appendNodeMeta(call, node, sourcefile);
455
+ }
456
+
457
+ case 'scoped_call_expression': {
458
+ const scope = visit(node.childForFieldName('scope'), opts) as UAST.Expression;
459
+ const name = visit(node.childForFieldName('name'), opts) as UAST.Expression;
460
+ const memberCallee = UAST.memberAccess(scope, name, false);
461
+ memberCallee._meta.isStatic = true;
462
+ appendNodeMeta(memberCallee, node, sourcefile);
463
+ const argsNode = node.childForFieldName('arguments');
464
+ const args = argsNode ? visitList(argsNode.namedChildren, opts) as Array<UAST.Expression> : [];
465
+ const call = UAST.callExpression(memberCallee, args);
466
+ return appendNodeMeta(call, node, sourcefile);
467
+ }
468
+
469
+ // ─── new / cast ───
470
+
471
+ case 'object_creation_expression': {
472
+ const className = visit(node.namedChildren[0], opts) as UAST.Expression;
473
+ const argsNode = node.childForFieldName('arguments');
474
+ const args = argsNode ? visitList(argsNode.namedChildren, opts) as Array<UAST.Expression> : [];
475
+ const expr = UAST.newExpression(className, args);
476
+ return appendNodeMeta(expr, node, sourcefile);
477
+ }
478
+
479
+ case 'cast_expression': {
480
+ const typeNode = node.childForFieldName('type');
481
+ const castType = typeNode ? typeNode.text.replace(/[()]/g, '').trim() : 'mixed';
482
+ const value = visit(node.childForFieldName('value'), opts) as UAST.Expression;
483
+ const call = UAST.callExpression(UAST.identifier(castType), [value]);
484
+ call._meta.isCast = true;
485
+ return appendNodeMeta(call, node, sourcefile);
486
+ }
487
+
488
+ // ─── 成员访问 ───
489
+
490
+ case 'member_access_expression': {
491
+ const object = visit(node.childForFieldName('object'), opts) as UAST.Expression;
492
+ const name = visit(node.childForFieldName('name'), opts) as UAST.Expression;
493
+ const member = UAST.memberAccess(object, name, false);
494
+ return appendNodeMeta(member, node, sourcefile);
495
+ }
496
+
497
+ case 'nullsafe_member_access_expression': {
498
+ const object = visit(node.childForFieldName('object'), opts) as UAST.Expression;
499
+ const name = visit(node.childForFieldName('name'), opts) as UAST.Expression;
500
+ const access = UAST.memberAccess(object, name, false);
501
+ appendNodeMeta(access, node, sourcefile);
502
+ access._meta.nullsafe = true;
503
+ const expr = UAST.conditionalExpression(object, access, UAST.literal(null, 'null'));
504
+ return appendNodeMeta(expr, node, sourcefile);
505
+ }
506
+
507
+ case 'scoped_property_access_expression': {
508
+ const scope = visit(node.childForFieldName('scope'), opts) as UAST.Expression;
509
+ const name = visit(node.childForFieldName('name'), opts) as UAST.Expression;
510
+ const member = UAST.memberAccess(scope, name, false);
511
+ member._meta.isStatic = true;
512
+ return appendNodeMeta(member, node, sourcefile);
513
+ }
514
+
515
+ case 'class_constant_access_expression': {
516
+ // class_constant_access_expression 没有字段名,按位置取 namedChildren
517
+ const scope = visit(node.namedChildren[0], opts) as UAST.Expression;
518
+ const name = visit(node.namedChildren[1], opts) as UAST.Expression;
519
+ const member = UAST.memberAccess(scope, name, false);
520
+ member._meta.isStatic = true;
521
+ return appendNodeMeta(member, node, sourcefile);
522
+ }
523
+
524
+ case 'subscript_expression': {
525
+ const named = node.namedChildren;
526
+ const object = visit(named[0], opts) as UAST.Expression;
527
+ // $arr[] 数组追加时 index 为空,用 noop 占位
528
+ const index = named.length > 1 ? visit(named[1], opts) as UAST.Expression : UAST.noop() as any;
529
+ const member = UAST.memberAccess(object, index, true);
530
+ return appendNodeMeta(member, node, sourcefile);
531
+ }
532
+
533
+ // ─── 数组 / 列表 / match ───
534
+
535
+ case 'array_creation_expression': {
536
+ const properties = node.namedChildren.map((element) => {
537
+ const elementChildren = element.namedChildren;
538
+ if (elementChildren.length >= 2) {
539
+ // key => value
540
+ const key = visit(elementChildren[0], opts) as UAST.Expression;
541
+ const value = visit(elementChildren[1], opts) as UAST.Expression;
542
+ const prop = UAST.objectProperty(key, value);
543
+ return appendNodeMeta(prop, element, sourcefile);
544
+ }
545
+ // value only
546
+ const value = visit(elementChildren[0], opts) as UAST.Expression;
547
+ const prop = UAST.objectProperty(UAST.literal(null, 'null'), value);
548
+ return appendNodeMeta(prop, element, sourcefile);
549
+ });
550
+ const expr = UAST.objectExpression(properties);
551
+ expr._meta.isArray = true;
552
+ return appendNodeMeta(expr, node, sourcefile);
553
+ }
554
+
555
+ case 'list_literal': {
556
+ const elements = node.namedChildren.map((child) => visit(child, opts) as UAST.Expression);
557
+ const tuple = UAST.tupleExpression(elements);
558
+ return appendNodeMeta(tuple, node, sourcefile);
559
+ }
560
+
561
+ case 'match_expression': {
562
+ const condition = visit(node.childForFieldName('condition_list') || node.childForFieldName('condition'), opts) as UAST.Expression;
563
+ const bodyNode = node.childForFieldName('body');
564
+ const arms = bodyNode ? bodyNode.namedChildren.map((arm) => {
565
+ if (arm.type === 'match_default_expression') {
566
+ // default arm
567
+ const body = visit(arm.childForFieldName('return_expression') || arm.namedChildren[0], opts) as UAST.Expression;
568
+ const armObject = UAST.objectExpression([
569
+ appendNodeMeta(
570
+ UAST.objectProperty(UAST.identifier('conds'), UAST.tupleExpression([UAST.literal('default', 'string')])),
571
+ arm, sourcefile
572
+ ),
573
+ appendNodeMeta(UAST.objectProperty(UAST.identifier('body'), body), arm, sourcefile),
574
+ ]);
575
+ armObject._meta.default = true;
576
+ return appendNodeMeta(armObject, arm, sourcefile);
577
+ }
578
+ // match_conditional_expression
579
+ const condListNode = arm.childForFieldName('conditional_expressions');
580
+ const conds = condListNode
581
+ ? visitList(condListNode.namedChildren, opts) as Array<UAST.Expression>
582
+ : visitList(arm.namedChildren.slice(0, -1), opts) as Array<UAST.Expression>;
583
+ const returnExpr = arm.childForFieldName('return_expression') || arm.namedChildren[arm.namedChildren.length - 1];
584
+ const body = visit(returnExpr, opts) as UAST.Expression;
585
+ const armObject = UAST.objectExpression([
586
+ appendNodeMeta(
587
+ UAST.objectProperty(UAST.identifier('conds'), UAST.tupleExpression(conds.length > 0 ? conds : [UAST.literal('default', 'string')])),
588
+ arm, sourcefile
589
+ ),
590
+ appendNodeMeta(UAST.objectProperty(UAST.identifier('body'), body), arm, sourcefile),
591
+ ]);
592
+ return appendNodeMeta(armObject, arm, sourcefile);
593
+ }) : [];
594
+ const matchArgs = [condition, UAST.tupleExpression(arms)];
595
+ const call = UAST.callExpression(UAST.identifier('match'), matchArgs);
596
+ call._meta.synthetic = true;
597
+ return appendNodeMeta(call, node, sourcefile);
598
+ }
599
+
600
+ // ─── import ───
601
+
602
+ case 'include_expression':
603
+ case 'include_once_expression':
604
+ case 'require_expression':
605
+ case 'require_once_expression': {
606
+ const targetNode = visit(node.namedChildren[0], opts);
607
+ let target: UAST.Literal;
608
+ if (UAST.isLiteral(targetNode)) {
609
+ target = targetNode;
610
+ } else {
611
+ // 动态 include,用空字符串占位
612
+ target = UAST.literal('', 'string');
613
+ target._meta.dynamicFrom = targetNode;
614
+ }
615
+ const importExpr = UAST.importExpression(target);
616
+ importExpr._meta.require = node.type.startsWith('require');
617
+ importExpr._meta.once = node.type.endsWith('once_expression');
618
+ return appendNodeMeta(importExpr, node, sourcefile);
619
+ }
620
+
621
+ // ─── 其他表达式 ───
622
+
623
+ case 'echo_statement': {
624
+ const args = visitList(node.namedChildren, opts) as Array<UAST.Expression>;
625
+ const echoCall = UAST.callExpression(UAST.identifier('echo'), args);
626
+ appendNodeMeta(echoCall, node, sourcefile);
627
+ const exprStmt = UAST.expressionStatement(echoCall);
628
+ return appendNodeMeta(exprStmt, node, sourcefile);
629
+ }
630
+
631
+ case 'yield_expression': {
632
+ const valueNode = node.namedChildren[0] || null;
633
+ const expr = UAST.yieldExpression(valueNode ? visit(valueNode, opts) as UAST.Expression : null);
634
+ return appendNodeMeta(expr, node, sourcefile);
635
+ }
636
+
637
+ case 'error_suppression_expression': {
638
+ const expr = visit(node.namedChildren[0], opts);
639
+ if (expr && UAST.isNode(expr)) {
640
+ expr._meta.silent = true;
641
+ }
642
+ return expr;
643
+ }
644
+
645
+ case 'clone_expression': {
646
+ const expr = visit(node.namedChildren[0], opts) as UAST.Expression;
647
+ const call = UAST.callExpression(UAST.identifier('clone'), [expr]);
648
+ return appendNodeMeta(call, node, sourcefile);
649
+ }
650
+
651
+ case 'print_intrinsic': {
652
+ const expr = visit(node.namedChildren[0], opts) as UAST.Expression;
653
+ const call = UAST.callExpression(UAST.identifier('print'), [expr]);
654
+ return appendNodeMeta(call, node, sourcefile);
655
+ }
656
+
657
+ case 'exit_statement': {
658
+ const args = node.namedChildren.length > 0
659
+ ? [visit(node.namedChildren[0], opts) as UAST.Expression]
660
+ : [];
661
+ const call = UAST.callExpression(UAST.identifier('exit'), args);
662
+ return appendNodeMeta(call, node, sourcefile);
663
+ }
664
+
665
+ case 'sequence_expression': {
666
+ const items = visitList(node.namedChildren, opts);
667
+ const seq = UAST.sequence(items);
668
+ return appendNodeMeta(seq, node, sourcefile);
669
+ }
670
+
671
+ // ─── 语句 ───
672
+
673
+ case 'compound_statement': {
674
+ const body = flattenInstructions(visitList(node.namedChildren, opts));
675
+ const scoped = UAST.scopedStatement(body);
676
+ return appendNodeMeta(scoped, node, sourcefile);
677
+ }
678
+
679
+ case 'if_statement': {
680
+ const condNode = node.childForFieldName('condition');
681
+ // parenthesized_expression → 取内层
682
+ const test = condNode && condNode.type === 'parenthesized_expression'
683
+ ? visit(condNode.namedChildren[0], opts) as UAST.Expression
684
+ : visit(condNode, opts) as UAST.Expression;
685
+ const bodyNode = node.childForFieldName('body');
686
+ const consequent = bodyNode
687
+ ? visit(bodyNode, opts) as UAST.Instruction
688
+ : UAST.scopedStatement([]);
689
+ // alternative: else_clause 或 else_if_clause
690
+ const altNode = node.childForFieldName('alternative');
691
+ let alternate: UAST.Instruction | null = null;
692
+ if (altNode) {
693
+ if (altNode.type === 'else_if_clause') {
694
+ // 递归构建嵌套 if
695
+ const elseIfCond = altNode.childForFieldName('condition');
696
+ const elseIfTest = elseIfCond && elseIfCond.type === 'parenthesized_expression'
697
+ ? visit(elseIfCond.namedChildren[0], opts) as UAST.Expression
698
+ : visit(elseIfCond, opts) as UAST.Expression;
699
+ const elseIfBody = altNode.childForFieldName('body');
700
+ const elseIfConsequent = elseIfBody
701
+ ? visit(elseIfBody, opts) as UAST.Instruction
702
+ : UAST.scopedStatement([]);
703
+ const elseIfAlt = altNode.childForFieldName('alternative');
704
+ const nestedAlt = elseIfAlt ? visit(elseIfAlt, opts) as UAST.Instruction : null;
705
+ alternate = UAST.ifStatement(elseIfTest, elseIfConsequent, nestedAlt);
706
+ appendNodeMeta(alternate, altNode, sourcefile);
707
+ } else if (altNode.type === 'else_clause') {
708
+ const elseBody = altNode.namedChildren[0];
709
+ alternate = elseBody ? visit(elseBody, opts) as UAST.Instruction : UAST.scopedStatement([]);
710
+ } else {
711
+ alternate = visit(altNode, opts) as UAST.Instruction;
712
+ }
713
+ }
714
+ const ifStmt = UAST.ifStatement(test, consequent, alternate);
715
+ return appendNodeMeta(ifStmt, node, sourcefile);
716
+ }
717
+
718
+ case 'else_if_clause': {
719
+ const condNode = node.childForFieldName('condition');
720
+ const test = condNode && condNode.type === 'parenthesized_expression'
721
+ ? visit(condNode.namedChildren[0], opts) as UAST.Expression
722
+ : visit(condNode, opts) as UAST.Expression;
723
+ const bodyNode = node.childForFieldName('body');
724
+ const consequent = bodyNode
725
+ ? visit(bodyNode, opts) as UAST.Instruction
726
+ : UAST.scopedStatement([]);
727
+ const altNode = node.childForFieldName('alternative');
728
+ const alternate = altNode ? visit(altNode, opts) as UAST.Instruction : null;
729
+ const ifStmt = UAST.ifStatement(test, consequent, alternate);
730
+ return appendNodeMeta(ifStmt, node, sourcefile);
731
+ }
732
+
733
+ case 'else_clause': {
734
+ const child = node.namedChildren[0];
735
+ return child ? visit(child, opts) : appendNodeMeta(UAST.scopedStatement([]), node, sourcefile);
736
+ }
737
+
738
+ case 'return_statement': {
739
+ const child = node.namedChildren[0] || null;
740
+ const ret = UAST.returnStatement(child ? visit(child, opts) as UAST.Expression : null);
741
+ return appendNodeMeta(ret, node, sourcefile);
742
+ }
743
+
744
+ case 'break_statement': {
745
+ const stmt = UAST.breakStatement();
746
+ const levelNode = node.namedChildren[0];
747
+ if (levelNode && levelNode.type === 'integer') {
748
+ stmt._meta.level = Number(levelNode.text);
749
+ }
750
+ return appendNodeMeta(stmt, node, sourcefile);
751
+ }
752
+
753
+ case 'continue_statement': {
754
+ const stmt = UAST.continueStatement();
755
+ const levelNode = node.namedChildren[0];
756
+ if (levelNode && levelNode.type === 'integer') {
757
+ stmt._meta.level = Number(levelNode.text);
758
+ }
759
+ return appendNodeMeta(stmt, node, sourcefile);
760
+ }
761
+
762
+ case 'throw_expression': {
763
+ const expr = visit(node.namedChildren[0], opts) as UAST.Expression;
764
+ const stmt = UAST.throwStatement(expr);
765
+ return appendNodeMeta(stmt, node, sourcefile);
766
+ }
767
+
768
+ case 'try_statement': {
769
+ const bodyNode = node.childForFieldName('body');
770
+ const body = bodyNode ? visit(bodyNode, opts) as UAST.Statement : UAST.scopedStatement([]) as any;
771
+ const catches: Array<UAST.CatchClause> = [];
772
+ let finalizer: UAST.Instruction | null = null;
773
+ for (const child of node.namedChildren) {
774
+ if (child.type === 'catch_clause') {
775
+ catches.push(visit(child, opts) as UAST.CatchClause);
776
+ } else if (child.type === 'finally_clause') {
777
+ const finallyBody = child.namedChildren[0];
778
+ finalizer = finallyBody ? visit(finallyBody, opts) as UAST.Instruction : UAST.scopedStatement([]);
779
+ }
780
+ }
781
+ const tryStmt = UAST.tryStatement(body, catches.length > 0 ? catches : null, finalizer);
782
+ return appendNodeMeta(tryStmt, node, sourcefile);
783
+ }
784
+
785
+ case 'catch_clause': {
786
+ let paramId: UAST.Identifier = UAST.identifier('_');
787
+ const catchTypes: Array<any> = [];
788
+ let catchBody: UAST.Instruction = UAST.scopedStatement([]);
789
+ for (const child of node.namedChildren) {
790
+ if (child.type === 'type_list') {
791
+ for (const typeChild of child.namedChildren) {
792
+ catchTypes.push(visit(typeChild, opts));
793
+ }
794
+ } else if (child.type === 'named_type' || child.type === 'qualified_name' || child.type === 'name') {
795
+ catchTypes.push(visit(child, opts));
796
+ } else if (child.type === 'variable_name') {
797
+ paramId = visit(child, opts) as UAST.Identifier;
798
+ } else if (child.type === 'compound_statement') {
799
+ catchBody = visit(child, opts) as UAST.Instruction;
800
+ }
801
+ }
802
+ const param = UAST.variableDeclaration(paramId, null, false, UAST.dynamicType());
803
+ param._meta.catchTypes = catchTypes;
804
+ appendNodeMeta(param, node, sourcefile);
805
+ const clause = UAST.catchClause([param], catchBody);
806
+ return appendNodeMeta(clause, node, sourcefile);
807
+ }
808
+
809
+ case 'switch_statement': {
810
+ const condNode = node.childForFieldName('condition');
811
+ const discriminant = condNode && condNode.type === 'parenthesized_expression'
812
+ ? visit(condNode.namedChildren[0], opts) as UAST.Expression
813
+ : visit(condNode, opts) as UAST.Expression;
814
+ const bodyNode = node.childForFieldName('body');
815
+ // 只取 case_statement / default_statement,过滤 comment 等
816
+ const caseNodes = bodyNode
817
+ ? bodyNode.namedChildren.filter((c) => c.type === 'case_statement' || c.type === 'default_statement')
818
+ : [];
819
+ const cases = visitList(caseNodes, opts) as Array<UAST.CaseClause>;
820
+ const switchStmt = UAST.switchStatement(discriminant, cases);
821
+ return appendNodeMeta(switchStmt, node, sourcefile);
822
+ }
823
+
824
+ case 'case_statement': {
825
+ const children = node.namedChildren;
826
+ const test = children.length > 0 ? visit(children[0], opts) as UAST.Expression : null;
827
+ const bodyStatements = children.length > 1
828
+ ? flattenInstructions(visitList(children.slice(1), opts))
829
+ : [];
830
+ const body = UAST.scopedStatement(bodyStatements);
831
+ appendNodeMeta(body, node, sourcefile);
832
+ const clause = UAST.caseClause(test, body);
833
+ return appendNodeMeta(clause, node, sourcefile);
834
+ }
835
+
836
+ case 'default_statement': {
837
+ const bodyStatements = flattenInstructions(visitList(node.namedChildren, opts));
838
+ const body = UAST.scopedStatement(bodyStatements);
839
+ appendNodeMeta(body, node, sourcefile);
840
+ const clause = UAST.caseClause(null, body);
841
+ return appendNodeMeta(clause, node, sourcefile);
842
+ }
843
+
844
+ case 'for_statement': {
845
+ const children = node.namedChildren;
846
+ let init: UAST.Expression | null = null;
847
+ let test: UAST.Expression | null = null;
848
+ let update: UAST.Expression | null = null;
849
+ let forBody: UAST.Instruction = UAST.scopedStatement([]);
850
+ const bodyIdx = children.findIndex((c) => c.type === 'compound_statement' || c.type === 'colon_block');
851
+ if (bodyIdx >= 0) {
852
+ forBody = visit(children[bodyIdx], opts) as UAST.Instruction;
853
+ }
854
+ const exprs = children.filter((c) => c.type !== 'compound_statement' && c.type !== 'colon_block');
855
+ if (exprs.length >= 1) init = visit(exprs[0], opts) as UAST.Expression;
856
+ if (exprs.length >= 2) test = visit(exprs[1], opts) as UAST.Expression;
857
+ if (exprs.length >= 3) update = visit(exprs[2], opts) as UAST.Expression;
858
+ const forStmt = UAST.forStatement(init, test, update, forBody);
859
+ return appendNodeMeta(forStmt, node, sourcefile);
860
+ }
861
+
862
+ case 'foreach_statement': {
863
+ const children = node.namedChildren;
864
+ let source: UAST.Expression = UAST.noop() as any;
865
+ let key: UAST.VariableDeclaration | null = null;
866
+ let value: UAST.VariableDeclaration | null = null;
867
+ let foreachBody: UAST.Instruction = UAST.scopedStatement([]);
868
+ const bodyIdx = children.findIndex((c) => c.type === 'compound_statement' || c.type === 'colon_block');
869
+ if (bodyIdx >= 0) {
870
+ foreachBody = visit(children[bodyIdx], opts) as UAST.Instruction;
871
+ }
872
+ const nonBody = children.filter((c) => c.type !== 'compound_statement' && c.type !== 'colon_block');
873
+ if (nonBody.length >= 1) {
874
+ source = visit(nonBody[0], opts) as UAST.Expression;
875
+ }
876
+ if (nonBody.length >= 2) {
877
+ const second = nonBody[1];
878
+ if (second.type === 'pair') {
879
+ const pairChildren = second.namedChildren;
880
+ if (pairChildren.length >= 2) {
881
+ const keyExpr = visit(pairChildren[0], opts) as UAST.LVal;
882
+ key = UAST.variableDeclaration(keyExpr, null, false, UAST.dynamicType());
883
+ appendNodeMeta(key, pairChildren[0], sourcefile);
884
+ const valExpr = visit(pairChildren[1], opts) as UAST.LVal;
885
+ value = UAST.variableDeclaration(valExpr, null, false, UAST.dynamicType());
886
+ appendNodeMeta(value, pairChildren[1], sourcefile);
887
+ }
888
+ } else {
889
+ const valExpr = visit(second, opts) as UAST.LVal;
890
+ value = UAST.variableDeclaration(valExpr, null, false, UAST.dynamicType());
891
+ appendNodeMeta(value, second, sourcefile);
892
+ }
893
+ }
894
+ const rangeStmt = UAST.rangeStatement(key, value, source, foreachBody);
895
+ return appendNodeMeta(rangeStmt, node, sourcefile);
896
+ }
897
+
898
+ case 'while_statement': {
899
+ const condNode = node.childForFieldName('condition');
900
+ const test = condNode && condNode.type === 'parenthesized_expression'
901
+ ? visit(condNode.namedChildren[0], opts) as UAST.Expression
902
+ : visit(condNode, opts) as UAST.Expression;
903
+ const bodyNode = node.childForFieldName('body');
904
+ const whileBody = bodyNode ? visit(bodyNode, opts) as UAST.Instruction : UAST.scopedStatement([]);
905
+ const stmt = UAST.whileStatement(test, whileBody, false);
906
+ return appendNodeMeta(stmt, node, sourcefile);
907
+ }
908
+
909
+ case 'do_statement': {
910
+ const bodyNode = node.childForFieldName('body');
911
+ const doBody = bodyNode ? visit(bodyNode, opts) as UAST.Instruction : UAST.scopedStatement([]);
912
+ const condNode = node.childForFieldName('condition');
913
+ const test = condNode && condNode.type === 'parenthesized_expression'
914
+ ? visit(condNode.namedChildren[0], opts) as UAST.Expression
915
+ : visit(condNode, opts) as UAST.Expression;
916
+ const stmt = UAST.whileStatement(test, doBody, true);
917
+ return appendNodeMeta(stmt, node, sourcefile);
918
+ }
919
+
920
+ case 'empty_statement':
921
+ return appendNodeMeta(UAST.noop(), node, sourcefile);
922
+
923
+ case 'goto_statement': {
924
+ const noop = UAST.noop();
925
+ noop._meta.goto = node.namedChildren[0] ? node.namedChildren[0].text : null;
926
+ return appendNodeMeta(noop, node, sourcefile);
927
+ }
928
+
929
+ case 'named_label_statement': {
930
+ const noop = UAST.noop();
931
+ noop._meta.label = node.namedChildren[0] ? node.namedChildren[0].text : null;
932
+ return appendNodeMeta(noop, node, sourcefile);
933
+ }
934
+
935
+ case 'unset_statement': {
936
+ const args = visitList(node.namedChildren, opts) as Array<UAST.Expression>;
937
+ const call = UAST.callExpression(UAST.identifier('unset'), args);
938
+ appendNodeMeta(call, node, sourcefile);
939
+ const exprStmt = UAST.expressionStatement(call);
940
+ return appendNodeMeta(exprStmt, node, sourcefile);
941
+ }
942
+
943
+ // ─── 声明 ───
944
+
945
+ case 'function_definition':
946
+ case 'method_declaration':
947
+ return createFunctionLike(node, opts);
948
+
949
+ case 'anonymous_function': {
950
+ const fdef = createFunctionLike(node, opts);
951
+ for (const child of node.namedChildren) {
952
+ if (child.type === 'anonymous_function_use_clause') {
953
+ fdef._meta.uses = visitList(child.namedChildren, opts);
954
+ break;
955
+ }
956
+ }
957
+ return fdef;
958
+ }
959
+
960
+ case 'arrow_function': {
961
+ const paramsNode = node.childForFieldName('parameters');
962
+ const parameters = paramsNode
963
+ ? paramsNode.namedChildren.map((p) => createParameter(p, opts))
964
+ : [];
965
+ const bodyNode = node.childForFieldName('body');
966
+ let body: UAST.Instruction;
967
+ if (bodyNode) {
968
+ const bodyExpr = visit(bodyNode, opts);
969
+ if (bodyExpr && UAST.isNode(bodyExpr) && !UAST.isExpr(bodyExpr)) {
970
+ body = bodyExpr as UAST.Instruction;
971
+ } else {
972
+ const ret = UAST.returnStatement(bodyExpr as UAST.Expression);
973
+ appendNodeMeta(ret, bodyNode, sourcefile);
974
+ body = UAST.scopedStatement([ret]);
975
+ appendNodeMeta(body, bodyNode, sourcefile);
976
+ }
977
+ } else {
978
+ body = UAST.scopedStatement([]);
979
+ }
980
+ const fdef = UAST.functionDefinition(null, parameters, UAST.dynamicType(), body, []);
981
+ return appendNodeMeta(fdef, node, sourcefile);
982
+ }
983
+
984
+ case 'simple_parameter':
985
+ case 'variadic_parameter':
986
+ case 'property_promotion_parameter':
987
+ return createParameter(node, opts);
988
+
989
+ case 'class_declaration':
990
+ return createClassLike(node, opts);
991
+
992
+ case 'interface_declaration':
993
+ return createClassLike(node, opts, 'interface');
994
+
995
+ case 'trait_declaration':
996
+ return createClassLike(node, opts, 'trait');
997
+
998
+ case 'enum_declaration': {
999
+ const nameNode = node.childForFieldName('name');
1000
+ const id = nameNode ? visit(nameNode, opts) as UAST.Identifier : null;
1001
+ const bodyNode = node.childForFieldName('body');
1002
+ const body = bodyNode
1003
+ ? flattenInstructions(visitList(bodyNode.namedChildren, opts))
1004
+ : [];
1005
+ const cdef = UAST.classDefinition(id, body, []);
1006
+ cdef._meta.kind = 'enum';
1007
+ return appendNodeMeta(cdef, node, sourcefile);
1008
+ }
1009
+
1010
+ case 'enum_case': {
1011
+ const nameNode = node.childForFieldName('name');
1012
+ const id = nameNode ? visit(nameNode, opts) as UAST.Identifier : UAST.identifier('_');
1013
+ let init: UAST.Expression | null = null;
1014
+ for (const child of node.namedChildren) {
1015
+ if (child !== nameNode && child.type !== 'name') {
1016
+ init = visit(child, opts) as UAST.Expression;
1017
+ break;
1018
+ }
1019
+ }
1020
+ const decl = UAST.variableDeclaration(id, init, false, UAST.dynamicType());
1021
+ decl._meta.enumCase = true;
1022
+ return appendNodeMeta(decl, node, sourcefile);
1023
+ }
1024
+
1025
+ case 'property_declaration': {
1026
+ const declarations: Array<any> = [];
1027
+ let visibility: string | null = null;
1028
+ let isStatic = false;
1029
+ let isReadonly = false;
1030
+ for (const child of node.namedChildren) {
1031
+ if (child.type === 'visibility_modifier') {
1032
+ visibility = child.text;
1033
+ } else if (child.type === 'static_modifier') {
1034
+ isStatic = true;
1035
+ } else if (child.type === 'readonly_modifier') {
1036
+ isReadonly = true;
1037
+ } else if (child.type === 'property_element') {
1038
+ const propName = child.childForFieldName('name') || child.namedChildren[0];
1039
+ const propValue = child.namedChildren.length > 1 ? child.namedChildren[1] : null;
1040
+ const id = propName ? visit(propName, opts) as UAST.LVal : UAST.identifier('_');
1041
+ const init = propValue ? visit(propValue, opts) as UAST.Expression : null;
1042
+ const decl = UAST.variableDeclaration(id, init, false, UAST.dynamicType());
1043
+ decl._meta.visibility = visibility;
1044
+ decl._meta.isStatic = isStatic;
1045
+ decl._meta.readonly = isReadonly;
1046
+ declarations.push(appendNodeMeta(decl, child, sourcefile));
1047
+ }
1048
+ }
1049
+ if (declarations.length === 1) return declarations[0];
1050
+ if (declarations.length === 0) return appendNodeMeta(UAST.noop(), node, sourcefile);
1051
+ return appendNodeMeta(UAST.sequence(declarations), node, sourcefile);
1052
+ }
1053
+
1054
+ case 'const_declaration': {
1055
+ const declarations: Array<any> = [];
1056
+ for (const child of node.namedChildren) {
1057
+ if (child.type === 'const_element') {
1058
+ const nameChild = child.childForFieldName('name') || child.namedChildren[0];
1059
+ const valueChild = child.childForFieldName('value') || child.namedChildren[1];
1060
+ const id = nameChild ? visit(nameChild, opts) as UAST.LVal : UAST.identifier('_');
1061
+ const init = valueChild ? visit(valueChild, opts) as UAST.Expression : null;
1062
+ const decl = UAST.variableDeclaration(id, init, false, UAST.dynamicType());
1063
+ decl._meta.constant = true;
1064
+ declarations.push(appendNodeMeta(decl, child, sourcefile));
1065
+ }
1066
+ }
1067
+ if (declarations.length === 1) return declarations[0];
1068
+ if (declarations.length === 0) return appendNodeMeta(UAST.noop(), node, sourcefile);
1069
+ return appendNodeMeta(UAST.sequence(declarations), node, sourcefile);
1070
+ }
1071
+
1072
+ case 'use_declaration': {
1073
+ // trait use 语句
1074
+ const traitNames = visitList(node.namedChildren, opts) as Array<UAST.Expression>;
1075
+ const call = UAST.callExpression(UAST.identifier('trait_use'), traitNames);
1076
+ call._meta.synthetic = true;
1077
+ appendNodeMeta(call, node, sourcefile);
1078
+ const exprStmt = UAST.expressionStatement(call);
1079
+ return appendNodeMeta(exprStmt, node, sourcefile);
1080
+ }
1081
+
1082
+ case 'namespace_definition': {
1083
+ const nameNode = node.childForFieldName('name');
1084
+ const bodyNode = node.childForFieldName('body');
1085
+ const instructions: Array<UAST.Instruction> = [];
1086
+ if (nameNode) {
1087
+ const pkg = UAST.packageDeclaration(UAST.identifier(nameNode.text));
1088
+ instructions.push(appendNodeMeta(pkg, nameNode, sourcefile));
1089
+ }
1090
+ if (bodyNode) {
1091
+ instructions.push(...flattenInstructions(visitList(bodyNode.namedChildren, opts)));
1092
+ }
1093
+ if (instructions.length === 1) return instructions[0];
1094
+ if (instructions.length === 0) return appendNodeMeta(UAST.noop(), node, sourcefile);
1095
+ return appendNodeMeta(UAST.sequence(instructions), node, sourcefile);
1096
+ }
1097
+
1098
+ case 'namespace_use_declaration': {
1099
+ const imports: Array<any> = [];
1100
+ for (const child of node.namedChildren) {
1101
+ if (child.type === 'namespace_use_clause') {
1102
+ const nameChild = child.namedChildren[0];
1103
+ const from = UAST.literal(nameChild ? nameChild.text : '', 'string');
1104
+ const aliasChild = child.namedChildren.length > 1 ? child.namedChildren[1] : null;
1105
+ const alias = aliasChild ? visit(aliasChild, opts) as UAST.Identifier : undefined;
1106
+ const importExpr = UAST.importExpression(from, alias);
1107
+ imports.push(appendNodeMeta(importExpr, child, sourcefile));
1108
+ } else if (child.type === 'namespace_use_group') {
1109
+ for (const groupChild of child.namedChildren) {
1110
+ if (groupChild.type === 'namespace_use_group_clause') {
1111
+ const gcName = groupChild.namedChildren[0];
1112
+ const from = UAST.literal(gcName ? gcName.text : '', 'string');
1113
+ const gcAlias = groupChild.namedChildren.length > 1 ? groupChild.namedChildren[1] : null;
1114
+ const alias = gcAlias ? visit(gcAlias, opts) as UAST.Identifier : undefined;
1115
+ const importExpr = UAST.importExpression(from, alias);
1116
+ imports.push(appendNodeMeta(importExpr, groupChild, sourcefile));
1117
+ }
1118
+ }
1119
+ }
1120
+ }
1121
+ if (imports.length === 1) {
1122
+ return UAST.expressionStatement(imports[0]);
1123
+ }
1124
+ if (imports.length === 0) return appendNodeMeta(UAST.noop(), node, sourcefile);
1125
+ return appendNodeMeta(UAST.sequence(imports.map((i: any) => UAST.expressionStatement(i))), node, sourcefile);
1126
+ }
1127
+
1128
+ case 'global_declaration': {
1129
+ const declarations = node.namedChildren.map((child) => {
1130
+ const id = visit(child, opts) as UAST.LVal;
1131
+ const decl = UAST.variableDeclaration(id, null, false, UAST.dynamicType());
1132
+ decl._meta.storage = 'global';
1133
+ return appendNodeMeta(decl, child, sourcefile);
1134
+ });
1135
+ if (declarations.length === 1) return declarations[0];
1136
+ if (declarations.length === 0) return appendNodeMeta(UAST.noop(), node, sourcefile);
1137
+ return appendNodeMeta(UAST.sequence(declarations), node, sourcefile);
1138
+ }
1139
+
1140
+ case 'function_static_declaration': {
1141
+ const declarations: Array<any> = [];
1142
+ for (const child of node.namedChildren) {
1143
+ if (child.type === 'static_variable_declaration') {
1144
+ const varName = child.namedChildren[0];
1145
+ const varInit = child.namedChildren.length > 1 ? child.namedChildren[1] : null;
1146
+ const id = varName ? visit(varName, opts) as UAST.LVal : UAST.identifier('_');
1147
+ const init = varInit ? visit(varInit, opts) as UAST.Expression : null;
1148
+ const decl = UAST.variableDeclaration(id, init, false, UAST.dynamicType());
1149
+ decl._meta.storage = 'static';
1150
+ declarations.push(appendNodeMeta(decl, child, sourcefile));
1151
+ }
1152
+ }
1153
+ if (declarations.length === 1) return declarations[0];
1154
+ if (declarations.length === 0) return appendNodeMeta(UAST.noop(), node, sourcefile);
1155
+ return appendNodeMeta(UAST.sequence(declarations), node, sourcefile);
1156
+ }
1157
+
1158
+ case 'declare_statement': {
1159
+ const directives: Array<{ key: any; value: any }> = [];
1160
+ let declareBody: UAST.Instruction | null = null;
1161
+ for (const child of node.namedChildren) {
1162
+ if (child.type === 'declare_directive') {
1163
+ const key = child.namedChildren[0] ? visit(child.namedChildren[0], opts) : null;
1164
+ const value = child.namedChildren[1] ? visit(child.namedChildren[1], opts) : null;
1165
+ directives.push({ key, value });
1166
+ } else if (child.type === 'compound_statement' || child.type === 'colon_block') {
1167
+ declareBody = visit(child, opts) as UAST.Instruction;
1168
+ }
1169
+ }
1170
+ const result = declareBody || appendNodeMeta(UAST.noop(), node, sourcefile);
1171
+ if (result && UAST.isNode(result)) {
1172
+ result._meta.declare = { directives };
1173
+ }
1174
+ return appendNodeMeta(result, node, sourcefile);
1175
+ }
1176
+
1177
+ // ─── 辅助节点类型 ───
1178
+
1179
+ case 'named_type': {
1180
+ const child = node.namedChildren[0];
1181
+ return child ? visit(child, opts) : appendNodeMeta(UAST.identifier(node.text), node, sourcefile);
1182
+ }
1183
+
1184
+ case 'colon_block': {
1185
+ const body = flattenInstructions(visitList(node.namedChildren, opts));
1186
+ return appendNodeMeta(UAST.scopedStatement(body), node, sourcefile);
1187
+ }
1188
+
1189
+ default: {
1190
+ const noop = UAST.noop();
1191
+ return appendNodeMeta(noop, node, sourcefile);
1192
+ }
1193
+ }
1194
+ }
1195
+
1196
+ function sanitize(node: any) {
1197
+ if (!node) {
1198
+ return;
1199
+ }
1200
+
1201
+ if (Array.isArray(node)) {
1202
+ node.forEach((item: any) => sanitize(item));
1203
+ return;
1204
+ }
1205
+
1206
+ if (!node.type) {
1207
+ return;
1208
+ }
1209
+
1210
+ if (node.loc) {
1211
+ Object.keys(node.loc).forEach((key: string) => {
1212
+ if (!['start', 'end', 'sourcefile'].includes(key)) {
1213
+ delete node.loc[key];
1214
+ }
1215
+ });
1216
+ }
1217
+
1218
+ Object.keys(node).forEach((key: string) => sanitize(node[key]));
1219
+ }
1220
+
1221
+ export function parse(content: string, opts: Record<string, any> = {}): ParseResult<UAST.Node> {
1222
+ const parser = getParser();
1223
+ const tree = parser.parse(content);
1224
+ if (!tree) {
1225
+ throw new Error('Failed to parse PHP code');
1226
+ }
1227
+ try {
1228
+ const node = visit(tree.rootNode, opts);
1229
+ sanitize(node);
1230
+ return node;
1231
+ } finally {
1232
+ tree.delete();
1233
+ }
1234
+ }