mcp-code-context 2.1.3

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 (112) hide show
  1. package/AGENT_INSTRUCTIONS.md +135 -0
  2. package/CHANGELOG.md +84 -0
  3. package/DEPLOYMENT_INSTRUCTIONS.md +110 -0
  4. package/LICENSE +21 -0
  5. package/README.md +247 -0
  6. package/RELEASE_NOTES_v2.1.3.md +78 -0
  7. package/TROUBLESHOOTING.md +38 -0
  8. package/dist/src/ast/dartCompressor.d.ts +27 -0
  9. package/dist/src/ast/dartCompressor.js +859 -0
  10. package/dist/src/ast/dartCompressor.js.map +1 -0
  11. package/dist/src/ast/phpCompressor.d.ts +24 -0
  12. package/dist/src/ast/phpCompressor.js +634 -0
  13. package/dist/src/ast/phpCompressor.js.map +1 -0
  14. package/dist/src/ast/semanticCompressor.d.ts +22 -0
  15. package/dist/src/ast/semanticCompressor.js +677 -0
  16. package/dist/src/ast/semanticCompressor.js.map +1 -0
  17. package/dist/src/ast/writers/dartWriter.d.ts +37 -0
  18. package/dist/src/ast/writers/dartWriter.js +544 -0
  19. package/dist/src/ast/writers/dartWriter.js.map +1 -0
  20. package/dist/src/ast/writers/phpWriter.d.ts +37 -0
  21. package/dist/src/ast/writers/phpWriter.js +411 -0
  22. package/dist/src/ast/writers/phpWriter.js.map +1 -0
  23. package/dist/src/ast/writers/pyWriter.d.ts +37 -0
  24. package/dist/src/ast/writers/pyWriter.js +355 -0
  25. package/dist/src/ast/writers/pyWriter.js.map +1 -0
  26. package/dist/src/ast/writers/symbolWriter.d.ts +62 -0
  27. package/dist/src/ast/writers/symbolWriter.js +179 -0
  28. package/dist/src/ast/writers/symbolWriter.js.map +1 -0
  29. package/dist/src/ast/writers/tsWriter.d.ts +38 -0
  30. package/dist/src/ast/writers/tsWriter.js +344 -0
  31. package/dist/src/ast/writers/tsWriter.js.map +1 -0
  32. package/dist/src/index.d.ts +117 -0
  33. package/dist/src/index.js +1289 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/tests/test-confirmation-flow.d.ts +1 -0
  36. package/dist/src/tests/test-confirmation-flow.js +43 -0
  37. package/dist/src/tests/test-confirmation-flow.js.map +1 -0
  38. package/dist/src/utils/backupManager.d.ts +24 -0
  39. package/dist/src/utils/backupManager.js +59 -0
  40. package/dist/src/utils/backupManager.js.map +1 -0
  41. package/dist/src/utils/confirmationCache.d.ts +16 -0
  42. package/dist/src/utils/confirmationCache.js +40 -0
  43. package/dist/src/utils/confirmationCache.js.map +1 -0
  44. package/dist/src/utils/diffEngine.d.ts +21 -0
  45. package/dist/src/utils/diffEngine.js +163 -0
  46. package/dist/src/utils/diffEngine.js.map +1 -0
  47. package/dist/src/utils/fuzzyMatch.d.ts +8 -0
  48. package/dist/src/utils/fuzzyMatch.js +69 -0
  49. package/dist/src/utils/fuzzyMatch.js.map +1 -0
  50. package/dist/src/utils/ignoreManager.d.ts +22 -0
  51. package/dist/src/utils/ignoreManager.js +135 -0
  52. package/dist/src/utils/ignoreManager.js.map +1 -0
  53. package/dist/tests/debug-replace.d.ts +1 -0
  54. package/dist/tests/debug-replace.js +17 -0
  55. package/dist/tests/debug-replace.js.map +1 -0
  56. package/dist/tests/test-confirmation-flow.d.ts +1 -0
  57. package/dist/tests/test-confirmation-flow.js +43 -0
  58. package/dist/tests/test-confirmation-flow.js.map +1 -0
  59. package/dist/tests/test-dart.d.ts +1 -0
  60. package/dist/tests/test-dart.js +242 -0
  61. package/dist/tests/test-dart.js.map +1 -0
  62. package/dist/tests/test-php-ast.d.ts +1 -0
  63. package/dist/tests/test-php-ast.js +152 -0
  64. package/dist/tests/test-php-ast.js.map +1 -0
  65. package/dist/tests/test-php.d.ts +1 -0
  66. package/dist/tests/test-php.js +234 -0
  67. package/dist/tests/test-php.js.map +1 -0
  68. package/dist/tests/test-smoke.d.ts +5 -0
  69. package/dist/tests/test-smoke.js +83 -0
  70. package/dist/tests/test-smoke.js.map +1 -0
  71. package/dist/tests/writers/test-crlf-handling.d.ts +4 -0
  72. package/dist/tests/writers/test-crlf-handling.js +73 -0
  73. package/dist/tests/writers/test-crlf-handling.js.map +1 -0
  74. package/dist/tests/writers/test-write-smoke.d.ts +9 -0
  75. package/dist/tests/writers/test-write-smoke.js +346 -0
  76. package/dist/tests/writers/test-write-smoke.js.map +1 -0
  77. package/llms.txt +164 -0
  78. package/package.json +57 -0
  79. package/src/ast/dartCompressor.ts +1013 -0
  80. package/src/ast/phpCompressor.ts +814 -0
  81. package/src/ast/semanticCompressor.ts +808 -0
  82. package/src/ast/writers/dartWriter.ts +648 -0
  83. package/src/ast/writers/phpWriter.ts +508 -0
  84. package/src/ast/writers/pyWriter.ts +446 -0
  85. package/src/ast/writers/symbolWriter.ts +277 -0
  86. package/src/ast/writers/tsWriter.ts +418 -0
  87. package/src/index.ts +1553 -0
  88. package/src/utils/backupManager.ts +66 -0
  89. package/src/utils/confirmationCache.ts +61 -0
  90. package/src/utils/diffEngine.ts +223 -0
  91. package/src/utils/fuzzyMatch.ts +86 -0
  92. package/src/utils/ignoreManager.ts +144 -0
  93. package/tests/test-confirmation-flow.ts +49 -0
  94. package/tests/test-dart.d.ts +1 -0
  95. package/tests/test-dart.js +209 -0
  96. package/tests/test-dart.js.map +1 -0
  97. package/tests/test-dart.ts +265 -0
  98. package/tests/test-php-ast.d.ts +1 -0
  99. package/tests/test-php-ast.js +152 -0
  100. package/tests/test-php-ast.js.map +1 -0
  101. package/tests/test-php-ast.ts +155 -0
  102. package/tests/test-php.d.ts +1 -0
  103. package/tests/test-php.js +234 -0
  104. package/tests/test-php.js.map +1 -0
  105. package/tests/test-php.ts +261 -0
  106. package/tests/test-smoke.d.ts +5 -0
  107. package/tests/test-smoke.js +83 -0
  108. package/tests/test-smoke.js.map +1 -0
  109. package/tests/test-smoke.ts +93 -0
  110. package/tests/writers/test-crlf-handling.ts +100 -0
  111. package/tests/writers/test-write-smoke.ts +568 -0
  112. package/tsconfig.json +19 -0
@@ -0,0 +1,814 @@
1
+ /**
2
+ * phpCompressor — AST-based PHP semantic compressor.
3
+ *
4
+ * Uses the `php-parser` npm package (Glayzzle) to parse PHP source code
5
+ * into a real Abstract Syntax Tree, then extracts only structural
6
+ * signatures: namespaces, use statements, classes, interfaces, traits,
7
+ * enums, functions, methods, properties, and constants — stripping all
8
+ * implementation bodies.
9
+ *
10
+ * This gives PHP the same first-class treatment that TypeScript gets
11
+ * via ts-morph: true AST parsing, not regex hacks.
12
+ */
13
+
14
+ import { createRequire } from "node:module";
15
+
16
+ // php-parser is a CJS package — use createRequire for ESM compatibility
17
+ const require = createRequire(import.meta.url);
18
+ const Engine = require("php-parser");
19
+
20
+ // ─── Types for php-parser AST nodes ────────────────────────────────
21
+
22
+ interface PhpNode {
23
+ kind: string;
24
+ name?: string | { name: string; kind: string };
25
+ children?: PhpNode[];
26
+ body?: PhpNode[] | PhpNode;
27
+ items?: PhpNode[];
28
+ cases?: PhpNode[];
29
+ traits?: PhpNode[];
30
+ adaptations?: PhpNode[];
31
+ arguments?: PhpNode[];
32
+
33
+ // Class / Interface / Trait / Enum
34
+ extends?: PhpNode | PhpNode[];
35
+ implements?: PhpNode[];
36
+ isAbstract?: boolean;
37
+ isFinal?: boolean;
38
+ isReadonly?: boolean;
39
+
40
+ // Methods / Functions
41
+ visibility?: string;
42
+ isStatic?: boolean;
43
+ nullable?: boolean;
44
+ type?: PhpNode | string | null;
45
+ value?: PhpNode | string | null;
46
+ byref?: boolean;
47
+ variadic?: boolean;
48
+
49
+ // Properties
50
+ properties?: PhpNode[];
51
+
52
+ // Use statements
53
+ aliases?: PhpNode[];
54
+
55
+ // Constants
56
+ constants?: PhpNode[];
57
+
58
+ // Enum backing type
59
+ valueType?: PhpNode | string | null;
60
+
61
+ // Union / Intersection types
62
+ types?: PhpNode[];
63
+
64
+ // Comments / DocBlocks
65
+ leadingComments?: Array<{ kind: string; value: string }>;
66
+
67
+ // Location (for symbol extraction)
68
+ loc?: {
69
+ start: { line: number; column: number; offset: number };
70
+ end: { line: number; column: number; offset: number };
71
+ };
72
+ }
73
+
74
+ // ─── Singleton Parser ──────────────────────────────────────────────
75
+
76
+ let _parser: any = null;
77
+
78
+ function getParser(): any {
79
+ if (!_parser) {
80
+ _parser = new Engine({
81
+ parser: {
82
+ extractDoc: true,
83
+ php7: true,
84
+ php8: true,
85
+ suppressErrors: true,
86
+ },
87
+ ast: {
88
+ withPositions: true,
89
+ withSource: false,
90
+ },
91
+ });
92
+ }
93
+ return _parser;
94
+ }
95
+
96
+ // ─── Public API ────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Compress a PHP file into its semantic skeleton.
100
+ * Returns only structural signatures — no function/method bodies.
101
+ */
102
+ export function compressPHP(content: string): string {
103
+ const parser = getParser();
104
+
105
+ let ast: PhpNode;
106
+ try {
107
+ ast = parser.parseCode(content, "file.php");
108
+ } catch {
109
+ // If parsing fails, return a truncated preview
110
+ const lines = content.split("\n");
111
+ if (lines.length > 80) {
112
+ return [
113
+ "// ⚠️ PHP AST parsing failed — showing first 60 lines",
114
+ "",
115
+ ...lines.slice(0, 60),
116
+ "",
117
+ `// ... (${lines.length - 60} more lines truncated)`,
118
+ ].join("\n");
119
+ }
120
+ return content;
121
+ }
122
+
123
+ const result: string[] = [];
124
+ processNode(ast, result, 0);
125
+ return result.join("\n");
126
+ }
127
+
128
+ /**
129
+ * Extract the full source code of a named symbol from a PHP file.
130
+ * Supports classes, interfaces, traits, enums, functions, and methods.
131
+ * When className is provided, scopes method search to within that class only.
132
+ * Returns null if the symbol is not found.
133
+ */
134
+ export function extractPHPSymbol(
135
+ content: string,
136
+ symbolName: string,
137
+ className?: string,
138
+ ): string | null {
139
+ const parser = getParser();
140
+
141
+ let ast: PhpNode;
142
+ try {
143
+ ast = parser.parseCode(content, "file.php");
144
+ } catch {
145
+ return null;
146
+ }
147
+
148
+ const lines = content.split("\n");
149
+ const node = findSymbolNode(ast, symbolName, className);
150
+
151
+ if (!node || !node.loc) {
152
+ return null;
153
+ }
154
+
155
+ // Extract lines from source using the node's location
156
+ const startLine = node.loc.start.line - 1; // 0-indexed
157
+ const endLine = node.loc.end.line; // exclusive
158
+
159
+ // For class-level symbols, also capture leading docblock
160
+ let actualStart = startLine;
161
+ if (node.leadingComments && node.leadingComments.length > 0) {
162
+ // Walk backward from startLine to find the docblock
163
+ for (let i = startLine - 1; i >= 0; i--) {
164
+ const trimmed = lines[i].trim();
165
+ if (
166
+ trimmed.startsWith("/**") ||
167
+ trimmed.startsWith("*") ||
168
+ trimmed.startsWith("*/") ||
169
+ trimmed.startsWith("//") ||
170
+ trimmed.startsWith("#[")
171
+ ) {
172
+ actualStart = i;
173
+ } else if (trimmed === "") {
174
+ continue;
175
+ } else {
176
+ break;
177
+ }
178
+ }
179
+ }
180
+
181
+ const extracted = lines.slice(actualStart, endLine);
182
+
183
+ // Trim trailing empty lines
184
+ while (extracted.length > 0 && extracted[extracted.length - 1].trim() === "") {
185
+ extracted.pop();
186
+ }
187
+
188
+ return extracted.join("\n");
189
+ }
190
+
191
+ // ─── AST Traversal & Compression ───────────────────────────────────
192
+
193
+ function processNode(node: PhpNode, result: string[], depth: number): void {
194
+ if (!node) return;
195
+
196
+ switch (node.kind) {
197
+ case "program":
198
+ processChildren(node, result, depth);
199
+ break;
200
+
201
+ case "namespace":
202
+ processNamespace(node, result, depth);
203
+ break;
204
+
205
+ case "usegroup":
206
+ processUseGroup(node, result, depth);
207
+ break;
208
+
209
+ case "class":
210
+ processClass(node, result, depth);
211
+ break;
212
+
213
+ case "interface":
214
+ processInterface(node, result, depth);
215
+ break;
216
+
217
+ case "trait":
218
+ processTrait(node, result, depth);
219
+ break;
220
+
221
+ case "enum":
222
+ processEnum(node, result, depth);
223
+ break;
224
+
225
+ case "function":
226
+ processFunction(node, result, depth);
227
+ break;
228
+
229
+ case "expressionstatement":
230
+ // Top-level expressions (e.g., define('CONST', value))
231
+ processExpression(node, result, depth);
232
+ break;
233
+
234
+ case "if":
235
+ // Top-level guards like: if (!defined('ABSPATH')) exit;
236
+ // Skip these — they're boilerplate
237
+ break;
238
+
239
+ default:
240
+ // Other top-level nodes we don't care about for compression
241
+ break;
242
+ }
243
+ }
244
+
245
+ function processChildren(node: PhpNode, result: string[], depth: number): void {
246
+ const children = node.children || node.body;
247
+ if (Array.isArray(children)) {
248
+ for (const child of children) {
249
+ processNode(child, result, depth);
250
+ }
251
+ }
252
+ }
253
+
254
+ // ─── Namespace ─────────────────────────────────────────────────────
255
+
256
+ function processNamespace(node: PhpNode, result: string[], depth: number): void {
257
+ const name = getNodeName(node);
258
+ if (name) {
259
+ result.push(`namespace ${name};`);
260
+ result.push("");
261
+ }
262
+
263
+ // Process use statements first, then other declarations
264
+ const children = node.children || [];
265
+ const useGroups: PhpNode[] = [];
266
+ const declarations: PhpNode[] = [];
267
+
268
+ for (const child of children) {
269
+ if (child.kind === "usegroup") {
270
+ useGroups.push(child);
271
+ } else {
272
+ declarations.push(child);
273
+ }
274
+ }
275
+
276
+ // Emit use statements
277
+ if (useGroups.length > 0) {
278
+ for (const ug of useGroups) {
279
+ processUseGroup(ug, result, depth);
280
+ }
281
+ result.push("");
282
+ }
283
+
284
+ // Emit declarations
285
+ for (const decl of declarations) {
286
+ processNode(decl, result, depth);
287
+ }
288
+ }
289
+
290
+ // ─── Use Statements ────────────────────────────────────────────────
291
+
292
+ function processUseGroup(node: PhpNode, result: string[], _depth: number): void {
293
+ const items = node.items || [];
294
+ for (const item of items) {
295
+ const name = getNodeName(item);
296
+ if (name) {
297
+ const alias =
298
+ item.aliases && item.aliases.length > 0
299
+ ? ` as ${getNodeName(item.aliases[0])}`
300
+ : "";
301
+ result.push(`use ${name}${alias};`);
302
+ }
303
+ }
304
+ }
305
+
306
+ // ─── Class ─────────────────────────────────────────────────────────
307
+
308
+ function processClass(node: PhpNode, result: string[], depth: number): void {
309
+ const indent = makeIndent(depth);
310
+
311
+ // Emit docblock
312
+ emitDocBlock(node, result, indent);
313
+
314
+ // Build class header
315
+ const parts: string[] = [];
316
+ if (node.isAbstract) parts.push("abstract");
317
+ if (node.isFinal) parts.push("final");
318
+ if (node.isReadonly) parts.push("readonly");
319
+ parts.push("class");
320
+ parts.push(getNodeName(node));
321
+
322
+ if (node.extends) {
323
+ const extName = getNodeName(node.extends as PhpNode);
324
+ if (extName) parts.push(`extends ${extName}`);
325
+ }
326
+
327
+ if (node.implements && node.implements.length > 0) {
328
+ const impls = node.implements.map((i) => getNodeName(i)).filter(Boolean);
329
+ if (impls.length > 0) parts.push(`implements ${impls.join(", ")}`);
330
+ }
331
+
332
+ result.push(`${indent}${parts.join(" ")} {`);
333
+
334
+ // Process class body
335
+ const body = Array.isArray(node.body) ? node.body : [];
336
+ processClassBody(body, result, depth + 1);
337
+
338
+ result.push(`${indent}}`);
339
+ result.push("");
340
+ }
341
+
342
+ // ─── Interface ─────────────────────────────────────────────────────
343
+
344
+ function processInterface(node: PhpNode, result: string[], depth: number): void {
345
+ const indent = makeIndent(depth);
346
+
347
+ emitDocBlock(node, result, indent);
348
+
349
+ const parts: string[] = ["interface", getNodeName(node)];
350
+
351
+ if (node.extends && Array.isArray(node.extends) && node.extends.length > 0) {
352
+ const exts = node.extends.map((e: PhpNode) => getNodeName(e)).filter(Boolean);
353
+ if (exts.length > 0) parts.push(`extends ${exts.join(", ")}`);
354
+ }
355
+
356
+ result.push(`${indent}${parts.join(" ")} {`);
357
+
358
+ const body = Array.isArray(node.body) ? node.body : [];
359
+ processClassBody(body, result, depth + 1);
360
+
361
+ result.push(`${indent}}`);
362
+ result.push("");
363
+ }
364
+
365
+ // ─── Trait ─────────────────────────────────────────────────────────
366
+
367
+ function processTrait(node: PhpNode, result: string[], depth: number): void {
368
+ const indent = makeIndent(depth);
369
+
370
+ emitDocBlock(node, result, indent);
371
+
372
+ result.push(`${indent}trait ${getNodeName(node)} {`);
373
+
374
+ const body = Array.isArray(node.body) ? node.body : [];
375
+ processClassBody(body, result, depth + 1);
376
+
377
+ result.push(`${indent}}`);
378
+ result.push("");
379
+ }
380
+
381
+ // ─── Enum ──────────────────────────────────────────────────────────
382
+
383
+ function processEnum(node: PhpNode, result: string[], depth: number): void {
384
+ const indent = makeIndent(depth);
385
+ const innerIndent = makeIndent(depth + 1);
386
+
387
+ emitDocBlock(node, result, indent);
388
+
389
+ let header = `enum ${getNodeName(node)}`;
390
+
391
+ // Backing type (e.g., enum Color: string)
392
+ if (node.valueType) {
393
+ const backingType = typeof node.valueType === "string"
394
+ ? node.valueType
395
+ : getNodeName(node.valueType as PhpNode);
396
+ if (backingType) header += `: ${backingType}`;
397
+ }
398
+
399
+ if (node.implements && node.implements.length > 0) {
400
+ const impls = node.implements.map((i) => getNodeName(i)).filter(Boolean);
401
+ if (impls.length > 0) header += ` implements ${impls.join(", ")}`;
402
+ }
403
+
404
+ result.push(`${indent}${header} {`);
405
+
406
+ // Emit cases
407
+ const body = Array.isArray(node.body) ? node.body : [];
408
+ for (const member of body) {
409
+ if (member.kind === "enumcase") {
410
+ const caseName = getNodeName(member);
411
+ if (member.value) {
412
+ const val = extractLiteralValue(member.value as PhpNode);
413
+ result.push(`${innerIndent}case ${caseName} = ${val};`);
414
+ } else {
415
+ result.push(`${innerIndent}case ${caseName};`);
416
+ }
417
+ } else if (member.kind === "method") {
418
+ processMethod(member, result, depth + 1);
419
+ } else if (member.kind === "classconstant") {
420
+ processClassConstant(member, result, depth + 1);
421
+ } else if (member.kind === "traituse") {
422
+ processTraitUse(member, result, depth + 1);
423
+ }
424
+ }
425
+
426
+ result.push(`${indent}}`);
427
+ result.push("");
428
+ }
429
+
430
+ // ─── Class Body (shared by class, interface, trait) ────────────────
431
+
432
+ function processClassBody(
433
+ body: PhpNode[],
434
+ result: string[],
435
+ depth: number,
436
+ ): void {
437
+ // Group members by type for clean output
438
+ const traitUses: PhpNode[] = [];
439
+ const constants: PhpNode[] = [];
440
+ const properties: PhpNode[] = [];
441
+ const methods: PhpNode[] = [];
442
+
443
+ for (const member of body) {
444
+ switch (member.kind) {
445
+ case "traituse":
446
+ traitUses.push(member);
447
+ break;
448
+ case "classconstant":
449
+ constants.push(member);
450
+ break;
451
+ case "propertystatement":
452
+ properties.push(member);
453
+ break;
454
+ case "method":
455
+ methods.push(member);
456
+ break;
457
+ default:
458
+ // Other node types we might encounter
459
+ break;
460
+ }
461
+ }
462
+
463
+ // Emit trait uses first
464
+ for (const tu of traitUses) {
465
+ processTraitUse(tu, result, depth);
466
+ }
467
+
468
+ // Then constants
469
+ for (const c of constants) {
470
+ processClassConstant(c, result, depth);
471
+ }
472
+
473
+ // Then properties
474
+ for (const p of properties) {
475
+ processProperty(p, result, depth);
476
+ }
477
+
478
+ // Then methods (signature only)
479
+ for (const m of methods) {
480
+ processMethod(m, result, depth);
481
+ }
482
+ }
483
+
484
+ // ─── Trait Use ──────────────────────────────────────────────────────
485
+
486
+ function processTraitUse(node: PhpNode, result: string[], depth: number): void {
487
+ const indent = makeIndent(depth);
488
+ const traits = node.traits || [];
489
+ const names = traits.map((t) => getNodeName(t)).filter(Boolean);
490
+ if (names.length > 0) {
491
+ result.push(`${indent}use ${names.join(", ")};`);
492
+ }
493
+ }
494
+
495
+ // ─── Class Constants ───────────────────────────────────────────────
496
+
497
+ function processClassConstant(node: PhpNode, result: string[], depth: number): void {
498
+ const indent = makeIndent(depth);
499
+ const vis = node.visibility ? `${node.visibility} ` : "";
500
+
501
+ const constants = node.constants || [];
502
+ for (const c of constants) {
503
+ const name = getNodeName(c);
504
+ const val = c.value ? extractLiteralValue(c.value as PhpNode) : "/* ... */";
505
+ emitDocBlock(node, result, indent);
506
+ result.push(`${indent}${vis}const ${name} = ${val};`);
507
+ }
508
+ }
509
+
510
+ // ─── Properties ────────────────────────────────────────────────────
511
+
512
+ function processProperty(node: PhpNode, result: string[], depth: number): void {
513
+ const indent = makeIndent(depth);
514
+ const vis = node.visibility ? `${node.visibility} ` : "";
515
+ const isStatic = node.isStatic ? "static " : "";
516
+ const isReadonly = node.isReadonly ? "readonly " : "";
517
+
518
+ emitDocBlock(node, result, indent);
519
+
520
+ const properties = node.properties || [];
521
+ for (const prop of properties) {
522
+ const name = getNodeName(prop);
523
+ const typeName = node.type ? buildTypeString(node.type) + " " : "";
524
+ const nullable = node.nullable ? "?" : "";
525
+ const defaultVal = prop.value ? ` = ${extractLiteralValue(prop.value as PhpNode)}` : "";
526
+ result.push(
527
+ `${indent}${vis}${isStatic}${isReadonly}${nullable}${typeName}$${name}${defaultVal};`,
528
+ );
529
+ }
530
+ }
531
+
532
+ // ─── Methods ───────────────────────────────────────────────────────
533
+
534
+ function processMethod(node: PhpNode, result: string[], depth: number): void {
535
+ const indent = makeIndent(depth);
536
+
537
+ emitDocBlock(node, result, indent);
538
+
539
+ const parts: string[] = [];
540
+ if (node.isAbstract) parts.push("abstract");
541
+ if (node.visibility) parts.push(node.visibility);
542
+ if (node.isStatic) parts.push("static");
543
+ parts.push("function");
544
+
545
+ const name = getNodeName(node);
546
+ const params = buildParameterList(node);
547
+ const returnType = buildReturnType(node);
548
+
549
+ if (node.isAbstract) {
550
+ result.push(`${indent}${parts.join(" ")} ${name}(${params})${returnType};`);
551
+ } else {
552
+ result.push(`${indent}${parts.join(" ")} ${name}(${params})${returnType} { /* ... */ }`);
553
+ }
554
+ }
555
+
556
+ // ─── Top-level Functions ───────────────────────────────────────────
557
+
558
+ function processFunction(node: PhpNode, result: string[], depth: number): void {
559
+ const indent = makeIndent(depth);
560
+
561
+ emitDocBlock(node, result, indent);
562
+
563
+ const name = getNodeName(node);
564
+ const params = buildParameterList(node);
565
+ const returnType = buildReturnType(node);
566
+
567
+ result.push(`${indent}function ${name}(${params})${returnType} { /* ... */ }`);
568
+ result.push("");
569
+ }
570
+
571
+ // ─── Expressions (define, etc.) ────────────────────────────────────
572
+
573
+ function processExpression(node: PhpNode, result: string[], _depth: number): void {
574
+ // Look for define() calls
575
+ const expr = (node as any).expression;
576
+ if (!expr) return;
577
+
578
+ if (expr.kind === "call") {
579
+ const callName = getNodeName(expr);
580
+ if (callName === "define" && expr.arguments && expr.arguments.length >= 2) {
581
+ const constName = extractLiteralValue(expr.arguments[0]);
582
+ const constVal = extractLiteralValue(expr.arguments[1]);
583
+ result.push(`define(${constName}, ${constVal});`);
584
+ }
585
+ }
586
+ }
587
+
588
+ // ─── Parameter List Builder ────────────────────────────────────────
589
+
590
+ function buildParameterList(node: PhpNode): string {
591
+ const args = node.arguments || [];
592
+ return args
593
+ .filter((a) => a.kind === "parameter")
594
+ .map((param) => {
595
+ const parts: string[] = [];
596
+
597
+ // Visibility for promoted properties (PHP 8.0+)
598
+ if (param.visibility) parts.push(param.visibility);
599
+ if (param.isReadonly) parts.push("readonly");
600
+
601
+ // Type
602
+ if (param.type) {
603
+ const nullable = param.nullable ? "?" : "";
604
+ parts.push(nullable + buildTypeString(param.type));
605
+ }
606
+
607
+ // Variadic
608
+ const variadic = param.variadic ? "..." : "";
609
+ const byref = param.byref ? "&" : "";
610
+
611
+ parts.push(`${byref}${variadic}$${getNodeName(param)}`);
612
+
613
+ // Default value
614
+ if (param.value) {
615
+ const val = extractLiteralValue(param.value as PhpNode);
616
+ parts.push(`= ${val}`);
617
+ }
618
+
619
+ return parts.join(" ");
620
+ })
621
+ .join(", ");
622
+ }
623
+
624
+ // ─── Return Type Builder ───────────────────────────────────────────
625
+
626
+ function buildReturnType(node: PhpNode): string {
627
+ if (!node.type) return "";
628
+ const nullable = node.nullable ? "?" : "";
629
+ return `: ${nullable}${buildTypeString(node.type)}`;
630
+ }
631
+
632
+ // ─── Type String Builder ───────────────────────────────────────────
633
+
634
+ function buildTypeString(type: PhpNode | string | null): string {
635
+ if (!type) return "";
636
+ if (typeof type === "string") return type;
637
+
638
+ switch (type.kind) {
639
+ case "typereference":
640
+ case "name":
641
+ case "selfreference":
642
+ case "staticreference":
643
+ case "parentreference":
644
+ return getNodeName(type) || type.kind;
645
+
646
+ case "uniontype":
647
+ return (type.types || [])
648
+ .map((t) => buildTypeString(t))
649
+ .join("|");
650
+
651
+ case "intersectiontype":
652
+ return (type.types || [])
653
+ .map((t) => buildTypeString(t))
654
+ .join("&");
655
+
656
+ case "nullabletype":
657
+ return `?${buildTypeString((type as any).type)}`;
658
+
659
+ default:
660
+ return getNodeName(type) || "";
661
+ }
662
+ }
663
+
664
+ // ─── Symbol Finder (for extractPHPSymbol) ──────────────────────────
665
+
666
+ // ─── Symbol Finder (for extractPHPSymbol) ──────────────────────────
667
+
668
+ /**
669
+ * Find a class/interface/trait/enum node by name in the AST tree.
670
+ * Used to scope symbol searches to a specific container.
671
+ */
672
+ function findClassNode(node: PhpNode, className: string): PhpNode | null {
673
+ if (!node) return null;
674
+
675
+ if (
676
+ ["class", "interface", "trait", "enum"].includes(node.kind) &&
677
+ getNodeName(node) === className
678
+ ) {
679
+ return node;
680
+ }
681
+
682
+ const allChildren = [
683
+ ...(node.children || []),
684
+ ...(Array.isArray(node.body) ? node.body : []),
685
+ ];
686
+
687
+ for (const child of allChildren) {
688
+ const found = findClassNode(child, className);
689
+ if (found) return found;
690
+ }
691
+
692
+ return null;
693
+ }
694
+
695
+ function findSymbolNode(
696
+ node: PhpNode,
697
+ symbolName: string,
698
+ className?: string,
699
+ ): PhpNode | null {
700
+ if (!node) return null;
701
+
702
+ // If className is provided, first find the class, then search within it
703
+ if (className) {
704
+ const classNode = findClassNode(node, className);
705
+ if (!classNode) return null;
706
+
707
+ // Search only within this class's body
708
+ const body = Array.isArray(classNode.body) ? classNode.body : [];
709
+ for (const member of body) {
710
+ if (member.kind === "method" && getNodeName(member) === symbolName) {
711
+ return member;
712
+ }
713
+ }
714
+ return null;
715
+ }
716
+
717
+ const nodeName = getNodeName(node);
718
+
719
+ // Direct match on class, interface, trait, enum, function
720
+ if (
721
+ ["class", "interface", "trait", "enum", "function"].includes(node.kind) &&
722
+ nodeName === symbolName
723
+ ) {
724
+ return node;
725
+ }
726
+
727
+ // Search methods inside classes/interfaces/traits/enums
728
+ if (["class", "interface", "trait", "enum"].includes(node.kind)) {
729
+ const body = Array.isArray(node.body) ? node.body : [];
730
+ for (const member of body) {
731
+ if (member.kind === "method" && getNodeName(member) === symbolName) {
732
+ return member;
733
+ }
734
+ }
735
+ }
736
+
737
+ // Recursive search through children
738
+ const allChildren = [
739
+ ...(node.children || []),
740
+ ...(Array.isArray(node.body) ? node.body : []),
741
+ ];
742
+
743
+ for (const child of allChildren) {
744
+ const found = findSymbolNode(child, symbolName);
745
+ if (found) return found;
746
+ }
747
+
748
+ return null;
749
+ }
750
+
751
+ // ─── Utility Functions ─────────────────────────────────────────────
752
+
753
+ function getNodeName(node: PhpNode | undefined | null): string {
754
+ if (!node) return "";
755
+ if (typeof node.name === "string") return node.name;
756
+ if (node.name && typeof node.name === "object" && "name" in node.name) {
757
+ return node.name.name;
758
+ }
759
+ // For typereference, name might be at the node level directly
760
+ if (node.kind === "typereference" || node.kind === "name") {
761
+ return (node as any).name || (node as any).resolution || "";
762
+ }
763
+ if (node.kind === "selfreference") return "self";
764
+ if (node.kind === "staticreference") return "static";
765
+ if (node.kind === "parentreference") return "parent";
766
+ return "";
767
+ }
768
+
769
+ function extractLiteralValue(node: PhpNode): string {
770
+ if (!node) return "/* ... */";
771
+
772
+ switch (node.kind) {
773
+ case "string":
774
+ return `'${(node as any).value || ""}'`;
775
+ case "number":
776
+ return String((node as any).value || 0);
777
+ case "boolean":
778
+ return (node as any).value ? "true" : "false";
779
+ case "nullkeyword":
780
+ return "null";
781
+ case "nowdoc":
782
+ case "encapsed":
783
+ return "'...'";
784
+ case "array":
785
+ return "[/* ... */]";
786
+ case "staticlookup":
787
+ case "propertylookup":
788
+ return "/* ... */";
789
+ default:
790
+ return "/* ... */";
791
+ }
792
+ }
793
+
794
+ function emitDocBlock(
795
+ node: PhpNode,
796
+ result: string[],
797
+ indent: string,
798
+ ): void {
799
+ if (!node.leadingComments || node.leadingComments.length === 0) return;
800
+
801
+ for (const comment of node.leadingComments) {
802
+ if (comment.kind === "commentblock" && comment.value.startsWith("/**")) {
803
+ // This is a PHPDoc block — emit it
804
+ const docLines = comment.value.split("\n");
805
+ for (const line of docLines) {
806
+ result.push(`${indent}${line.trim()}`);
807
+ }
808
+ }
809
+ }
810
+ }
811
+
812
+ function makeIndent(depth: number): string {
813
+ return " ".repeat(depth);
814
+ }