mcp-code-context 2.2.0 → 2.3.1
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.
- package/.io/RULES.md +12 -0
- package/CHANGELOG.md +88 -1
- package/README.md +27 -0
- package/RELEASE_NOTES_v2.3.0.md +228 -0
- package/RELEASE_NOTES_v2.3.1.md +110 -0
- package/dist/src/ast/writers/safeRename.d.ts +14 -0
- package/dist/src/ast/writers/safeRename.js +258 -0
- package/dist/src/ast/writers/safeRename.js.map +1 -0
- package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
- package/dist/src/ast/writers/symbolWriter.js +9 -14
- package/dist/src/ast/writers/symbolWriter.js.map +1 -1
- package/dist/src/cache/astCache.d.ts +33 -0
- package/dist/src/cache/astCache.js +113 -0
- package/dist/src/cache/astCache.js.map +1 -0
- package/dist/src/handlers/readHandlers.d.ts +70 -0
- package/dist/src/handlers/readHandlers.js +431 -0
- package/dist/src/handlers/readHandlers.js.map +1 -0
- package/dist/src/handlers/utilHandlers.d.ts +31 -0
- package/dist/src/handlers/utilHandlers.js +69 -0
- package/dist/src/handlers/utilHandlers.js.map +1 -0
- package/dist/src/handlers/writeHandlers.d.ts +57 -0
- package/dist/src/handlers/writeHandlers.js +475 -0
- package/dist/src/handlers/writeHandlers.js.map +1 -0
- package/dist/src/index.d.ts +6 -143
- package/dist/src/index.js +77 -1091
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/arrayUtils.d.ts +12 -0
- package/dist/src/utils/arrayUtils.js +27 -0
- package/dist/src/utils/arrayUtils.js.map +1 -0
- package/dist/src/utils/constants.d.ts +46 -0
- package/dist/src/utils/constants.js +112 -0
- package/dist/src/utils/constants.js.map +1 -0
- package/dist/src/utils/ignoreManager.d.ts +8 -0
- package/dist/src/utils/ignoreManager.js +49 -0
- package/dist/src/utils/ignoreManager.js.map +1 -1
- package/dist/src/utils/normalization.d.ts +49 -0
- package/dist/src/utils/normalization.js +173 -0
- package/dist/src/utils/normalization.js.map +1 -0
- package/dist/src/utils/projectRoot.d.ts +12 -5
- package/dist/src/utils/projectRoot.js +22 -20
- package/dist/src/utils/projectRoot.js.map +1 -1
- package/dist/src/utils/secureValidation.d.ts +15 -0
- package/dist/src/utils/secureValidation.js +41 -0
- package/dist/src/utils/secureValidation.js.map +1 -0
- package/dist/src/utils/transactionManager.d.ts +45 -0
- package/dist/src/utils/transactionManager.js +102 -0
- package/dist/src/utils/transactionManager.js.map +1 -0
- package/dist/src/utils/validation.d.ts +87 -0
- package/dist/src/utils/validation.js +231 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/docs/ARCHITECTURE.md +300 -0
- package/docs/architecture/ADR-001-modular-handlers.md +119 -0
- package/package.json +5 -5
- package/AGENT_INSTRUCTIONS.md +0 -135
- package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
- package/MEJORAS_PROPUESTAS.md +0 -324
- package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
- package/dist/src/tests/test-confirmation-flow.js +0 -43
- package/dist/src/tests/test-confirmation-flow.js.map +0 -1
- package/dist/tests/debug-replace.d.ts +0 -1
- package/dist/tests/debug-replace.js +0 -17
- package/dist/tests/debug-replace.js.map +0 -1
- package/dist/tests/test-backup-system.d.ts +0 -7
- package/dist/tests/test-backup-system.js +0 -152
- package/dist/tests/test-backup-system.js.map +0 -1
- package/dist/tests/test-confirmation-flow.d.ts +0 -1
- package/dist/tests/test-confirmation-flow.js +0 -43
- package/dist/tests/test-confirmation-flow.js.map +0 -1
- package/dist/tests/test-dart.d.ts +0 -1
- package/dist/tests/test-dart.js +0 -242
- package/dist/tests/test-dart.js.map +0 -1
- package/dist/tests/test-new-tools.d.ts +0 -4
- package/dist/tests/test-new-tools.js +0 -147
- package/dist/tests/test-new-tools.js.map +0 -1
- package/dist/tests/test-php-ast.d.ts +0 -1
- package/dist/tests/test-php-ast.js +0 -152
- package/dist/tests/test-php-ast.js.map +0 -1
- package/dist/tests/test-php.d.ts +0 -1
- package/dist/tests/test-php.js +0 -234
- package/dist/tests/test-php.js.map +0 -1
- package/dist/tests/test-smoke.d.ts +0 -5
- package/dist/tests/test-smoke.js +0 -83
- package/dist/tests/test-smoke.js.map +0 -1
- package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
- package/dist/tests/writers/test-crlf-handling.js +0 -73
- package/dist/tests/writers/test-crlf-handling.js.map +0 -1
- package/dist/tests/writers/test-write-smoke.d.ts +0 -9
- package/dist/tests/writers/test-write-smoke.js +0 -346
- package/dist/tests/writers/test-write-smoke.js.map +0 -1
- package/src/ast/dartCompressor.ts +0 -1013
- package/src/ast/phpCompressor.ts +0 -814
- package/src/ast/semanticCompressor.ts +0 -808
- package/src/ast/writers/dartWriter.ts +0 -648
- package/src/ast/writers/phpWriter.ts +0 -508
- package/src/ast/writers/pyWriter.ts +0 -446
- package/src/ast/writers/symbolWriter.ts +0 -277
- package/src/ast/writers/tsWriter.ts +0 -418
- package/src/index.ts +0 -1784
- package/src/tools/readFileLines.ts +0 -104
- package/src/tools/searchCodePattern.ts +0 -129
- package/src/utils/backupManager.ts +0 -141
- package/src/utils/confirmationCache.ts +0 -61
- package/src/utils/diffEngine.ts +0 -223
- package/src/utils/fuzzyMatch.ts +0 -86
- package/src/utils/ignoreManager.ts +0 -144
- package/src/utils/projectRoot.ts +0 -44
- package/tests/test-backup-system.ts +0 -174
- package/tests/test-confirmation-flow.ts +0 -49
- package/tests/test-dart.d.ts +0 -1
- package/tests/test-dart.js +0 -209
- package/tests/test-dart.js.map +0 -1
- package/tests/test-dart.ts +0 -265
- package/tests/test-new-tools.ts +0 -165
- package/tests/test-php-ast.d.ts +0 -1
- package/tests/test-php-ast.js +0 -152
- package/tests/test-php-ast.js.map +0 -1
- package/tests/test-php-ast.ts +0 -155
- package/tests/test-php.d.ts +0 -1
- package/tests/test-php.js +0 -234
- package/tests/test-php.js.map +0 -1
- package/tests/test-php.ts +0 -261
- package/tests/test-smoke.d.ts +0 -5
- package/tests/test-smoke.js +0 -83
- package/tests/test-smoke.js.map +0 -1
- package/tests/test-smoke.ts +0 -93
- package/tests/writers/test-crlf-handling.ts +0 -100
- package/tests/writers/test-write-smoke.ts +0 -568
- package/tsconfig.json +0 -19
package/src/ast/phpCompressor.ts
DELETED
|
@@ -1,814 +0,0 @@
|
|
|
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
|
-
}
|