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
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* phpWriter.ts — PHP Surgical Writer
|
|
3
|
-
*
|
|
4
|
-
* Uses php-parser to parse the AST for symbol location data,
|
|
5
|
-
* then performs line-splice operations on the raw source text.
|
|
6
|
-
* php-parser doesn't have a write API like ts-morph, so we locate
|
|
7
|
-
* symbols via AST → get line numbers → splice in the replacement.
|
|
8
|
-
*
|
|
9
|
-
* Supports: .php, .phtml
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
|
-
|
|
14
|
-
// ─── Types ──────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export interface WriteResult {
|
|
17
|
-
success: boolean;
|
|
18
|
-
newContent: string;
|
|
19
|
-
symbolsAffected: string[];
|
|
20
|
-
error?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type InsertPosition = "before" | "after" | "inside_start" | "inside_end";
|
|
24
|
-
|
|
25
|
-
interface PhpNode {
|
|
26
|
-
kind: string;
|
|
27
|
-
name?: string | { name: string; kind: string };
|
|
28
|
-
children?: PhpNode[];
|
|
29
|
-
body?: PhpNode[] | PhpNode;
|
|
30
|
-
items?: PhpNode[];
|
|
31
|
-
loc?: {
|
|
32
|
-
start: { line: number; column: number; offset: number };
|
|
33
|
-
end: { line: number; column: number; offset: number };
|
|
34
|
-
};
|
|
35
|
-
leadingComments?: Array<{ kind: string; value: string }>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ─── Parser ─────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
let _parser: any = null;
|
|
41
|
-
|
|
42
|
-
function getParser(): any {
|
|
43
|
-
if (!_parser) {
|
|
44
|
-
const require = createRequire(import.meta.url);
|
|
45
|
-
const phpParser = require("php-parser");
|
|
46
|
-
_parser = new phpParser.Engine({
|
|
47
|
-
parser: {
|
|
48
|
-
extractDoc: true,
|
|
49
|
-
php7: true,
|
|
50
|
-
locations: true,
|
|
51
|
-
suppressErrors: true,
|
|
52
|
-
},
|
|
53
|
-
ast: { withPositions: true },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return _parser;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function parsePhp(content: string): PhpNode | null {
|
|
60
|
-
try {
|
|
61
|
-
return getParser().parseCode(content, "file.php");
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── Replace Symbol ─────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Replace the full text of a named symbol in a PHP file.
|
|
71
|
-
* When className is provided, scopes the search to within that class only.
|
|
72
|
-
*/
|
|
73
|
-
export function replacePHPSymbol(
|
|
74
|
-
content: string,
|
|
75
|
-
symbolName: string,
|
|
76
|
-
newContent: string,
|
|
77
|
-
className?: string,
|
|
78
|
-
): WriteResult {
|
|
79
|
-
const ast = parsePhp(content);
|
|
80
|
-
if (!ast) {
|
|
81
|
-
return {
|
|
82
|
-
success: false,
|
|
83
|
-
newContent: content,
|
|
84
|
-
symbolsAffected: [],
|
|
85
|
-
error: "Failed to parse PHP file",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const node = findSymbolNode(ast, symbolName, className);
|
|
90
|
-
if (!node || !node.loc) {
|
|
91
|
-
return {
|
|
92
|
-
success: false,
|
|
93
|
-
newContent: content,
|
|
94
|
-
symbolsAffected: [],
|
|
95
|
-
error: className
|
|
96
|
-
? `Symbol "${symbolName}" not found in class "${className}" in PHP file`
|
|
97
|
-
: `Symbol "${symbolName}" not found in PHP file`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const lines = content.split("\n");
|
|
102
|
-
|
|
103
|
-
// Find the actual start including doc comments
|
|
104
|
-
const startLine = findActualStart(lines, node);
|
|
105
|
-
const endLine = node.loc.end.line; // 1-indexed, inclusive
|
|
106
|
-
|
|
107
|
-
// Detect the indentation of the original symbol
|
|
108
|
-
const originalIndent = getIndent(lines[startLine - 1] || "");
|
|
109
|
-
|
|
110
|
-
// Re-indent the new content to match
|
|
111
|
-
const reindented = reindentCode(newContent.trim(), originalIndent);
|
|
112
|
-
|
|
113
|
-
// Splice in the replacement
|
|
114
|
-
const before = lines.slice(0, startLine - 1);
|
|
115
|
-
const after = lines.slice(endLine);
|
|
116
|
-
const result = [...before, reindented, ...after].join("\n");
|
|
117
|
-
|
|
118
|
-
// Validate: try to parse the result
|
|
119
|
-
const validationAst = parsePhp(result);
|
|
120
|
-
if (!validationAst) {
|
|
121
|
-
return {
|
|
122
|
-
success: false,
|
|
123
|
-
newContent: content,
|
|
124
|
-
symbolsAffected: [],
|
|
125
|
-
error: `Replacement produced invalid PHP syntax. Aborting.`,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
success: true,
|
|
131
|
-
newContent: result,
|
|
132
|
-
symbolsAffected: [symbolName],
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ─── Insert Code ────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Insert new code at a precise location relative to an anchor symbol.
|
|
140
|
-
* When className is provided, scopes the anchor search to within that class only.
|
|
141
|
-
*/
|
|
142
|
-
export function insertPHPCode(
|
|
143
|
-
content: string,
|
|
144
|
-
code: string,
|
|
145
|
-
anchorSymbol: string | null,
|
|
146
|
-
position: InsertPosition,
|
|
147
|
-
className?: string,
|
|
148
|
-
): WriteResult {
|
|
149
|
-
const lines = content.split("\n");
|
|
150
|
-
|
|
151
|
-
if (!anchorSymbol) {
|
|
152
|
-
// No anchor — insert before the final closing tag or at end
|
|
153
|
-
const lastClosingBrace = findLastClosingBrace(lines);
|
|
154
|
-
if (lastClosingBrace !== -1) {
|
|
155
|
-
// Insert before the last } (likely end of namespace or class)
|
|
156
|
-
lines.splice(lastClosingBrace, 0, "", code.trim(), "");
|
|
157
|
-
} else {
|
|
158
|
-
lines.push("", code.trim());
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
success: true,
|
|
162
|
-
newContent: lines.join("\n"),
|
|
163
|
-
symbolsAffected: [],
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const ast = parsePhp(content);
|
|
168
|
-
if (!ast) {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
newContent: content,
|
|
172
|
-
symbolsAffected: [],
|
|
173
|
-
error: "Failed to parse PHP file",
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const node = findSymbolNode(ast, anchorSymbol, className);
|
|
178
|
-
if (!node || !node.loc) {
|
|
179
|
-
return {
|
|
180
|
-
success: false,
|
|
181
|
-
newContent: content,
|
|
182
|
-
symbolsAffected: [],
|
|
183
|
-
error: className
|
|
184
|
-
? `Anchor symbol "${anchorSymbol}" not found in class "${className}"`
|
|
185
|
-
: `Anchor symbol "${anchorSymbol}" not found`,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const startLine = findActualStart(lines, node);
|
|
190
|
-
const endLine = node.loc.end.line;
|
|
191
|
-
|
|
192
|
-
const indent = getIndent(lines[startLine - 1] || "");
|
|
193
|
-
const reindented = reindentCode(code.trim(), indent);
|
|
194
|
-
|
|
195
|
-
switch (position) {
|
|
196
|
-
case "before":
|
|
197
|
-
lines.splice(startLine - 1, 0, reindented, "");
|
|
198
|
-
break;
|
|
199
|
-
case "after":
|
|
200
|
-
lines.splice(endLine, 0, "", reindented);
|
|
201
|
-
break;
|
|
202
|
-
case "inside_start": {
|
|
203
|
-
// Find the opening brace of the class/interface
|
|
204
|
-
const braceIdx = findOpeningBrace(lines, startLine - 1);
|
|
205
|
-
if (braceIdx === -1) {
|
|
206
|
-
return {
|
|
207
|
-
success: false,
|
|
208
|
-
newContent: content,
|
|
209
|
-
symbolsAffected: [],
|
|
210
|
-
error: `Cannot find opening brace for "${anchorSymbol}"`,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
const innerIndent = indent + " ";
|
|
214
|
-
const innerCode = reindentCode(code.trim(), innerIndent);
|
|
215
|
-
lines.splice(braceIdx + 1, 0, innerCode, "");
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
case "inside_end": {
|
|
219
|
-
// Insert before the closing brace
|
|
220
|
-
const innerIndent = indent + " ";
|
|
221
|
-
const innerCode = reindentCode(code.trim(), innerIndent);
|
|
222
|
-
lines.splice(endLine - 1, 0, "", innerCode);
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
success: true,
|
|
229
|
-
newContent: lines.join("\n"),
|
|
230
|
-
symbolsAffected: [anchorSymbol],
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ─── Rename Symbol ──────────────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Rename all occurrences of a symbol within a PHP file.
|
|
238
|
-
* Performs whole-word replacement respecting PHP identifier boundaries.
|
|
239
|
-
*/
|
|
240
|
-
export function renamePHPSymbol(
|
|
241
|
-
content: string,
|
|
242
|
-
oldName: string,
|
|
243
|
-
newName: string,
|
|
244
|
-
): WriteResult {
|
|
245
|
-
const ast = parsePhp(content);
|
|
246
|
-
if (!ast) {
|
|
247
|
-
return {
|
|
248
|
-
success: false,
|
|
249
|
-
newContent: content,
|
|
250
|
-
symbolsAffected: [],
|
|
251
|
-
error: "Failed to parse PHP file",
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Verify the symbol exists
|
|
256
|
-
const node = findSymbolNode(ast, oldName);
|
|
257
|
-
if (!node) {
|
|
258
|
-
return {
|
|
259
|
-
success: false,
|
|
260
|
-
newContent: content,
|
|
261
|
-
symbolsAffected: [],
|
|
262
|
-
error: `Symbol "${oldName}" not found for renaming`,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Perform whole-word replacement
|
|
267
|
-
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
268
|
-
const regex = new RegExp(`\\b${escaped}\\b`, "g");
|
|
269
|
-
const result = content.replace(regex, newName);
|
|
270
|
-
|
|
271
|
-
// Validate
|
|
272
|
-
const validationAst = parsePhp(result);
|
|
273
|
-
if (!validationAst) {
|
|
274
|
-
return {
|
|
275
|
-
success: false,
|
|
276
|
-
newContent: content,
|
|
277
|
-
symbolsAffected: [],
|
|
278
|
-
error: `Rename produced invalid PHP syntax`,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
success: true,
|
|
284
|
-
newContent: result,
|
|
285
|
-
symbolsAffected: [oldName],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ─── Remove Symbol ──────────────────────────────────────────────────
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Remove a named symbol from a PHP file.
|
|
293
|
-
* When className is provided, scopes the search to within that class only.
|
|
294
|
-
*/
|
|
295
|
-
export function removePHPSymbol(
|
|
296
|
-
content: string,
|
|
297
|
-
symbolName: string,
|
|
298
|
-
className?: string,
|
|
299
|
-
): WriteResult {
|
|
300
|
-
const ast = parsePhp(content);
|
|
301
|
-
if (!ast) {
|
|
302
|
-
return {
|
|
303
|
-
success: false,
|
|
304
|
-
newContent: content,
|
|
305
|
-
symbolsAffected: [],
|
|
306
|
-
error: "Failed to parse PHP file",
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const node = findSymbolNode(ast, symbolName, className);
|
|
311
|
-
if (!node || !node.loc) {
|
|
312
|
-
return {
|
|
313
|
-
success: false,
|
|
314
|
-
newContent: content,
|
|
315
|
-
symbolsAffected: [],
|
|
316
|
-
error: className
|
|
317
|
-
? `Symbol "${symbolName}" not found in class "${className}" for removal`
|
|
318
|
-
: `Symbol "${symbolName}" not found for removal`,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const lines = content.split("\n");
|
|
323
|
-
const startLine = findActualStart(lines, node);
|
|
324
|
-
const endLine = node.loc.end.line;
|
|
325
|
-
|
|
326
|
-
// Remove the lines
|
|
327
|
-
lines.splice(startLine - 1, endLine - startLine + 1);
|
|
328
|
-
|
|
329
|
-
// Clean up consecutive blank lines
|
|
330
|
-
let result = lines.join("\n");
|
|
331
|
-
result = result.replace(/\n{3,}/g, "\n\n");
|
|
332
|
-
|
|
333
|
-
return {
|
|
334
|
-
success: true,
|
|
335
|
-
newContent: result,
|
|
336
|
-
symbolsAffected: [symbolName],
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ─── Internal Helpers ───────────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
function getNodeName(node: PhpNode | undefined | null): string {
|
|
343
|
-
if (!node || !node.name) return "";
|
|
344
|
-
if (typeof node.name === "string") return node.name;
|
|
345
|
-
if (typeof node.name === "object" && "name" in node.name)
|
|
346
|
-
return node.name.name;
|
|
347
|
-
return "";
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Find a class/interface/trait/enum node by name in the AST tree.
|
|
352
|
-
* Used to scope symbol searches to a specific container.
|
|
353
|
-
*/
|
|
354
|
-
function findClassNodePhp(node: PhpNode, className: string): PhpNode | null {
|
|
355
|
-
if (!node) return null;
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
["class", "interface", "trait", "enum"].includes(node.kind) &&
|
|
359
|
-
getNodeName(node) === className
|
|
360
|
-
) {
|
|
361
|
-
return node;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const allChildren = [
|
|
365
|
-
...(node.children || []),
|
|
366
|
-
...(Array.isArray(node.body) ? node.body : []),
|
|
367
|
-
];
|
|
368
|
-
|
|
369
|
-
for (const child of allChildren) {
|
|
370
|
-
const found = findClassNodePhp(child, className);
|
|
371
|
-
if (found) return found;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function findSymbolNode(
|
|
378
|
-
node: PhpNode,
|
|
379
|
-
symbolName: string,
|
|
380
|
-
className?: string,
|
|
381
|
-
): PhpNode | null {
|
|
382
|
-
if (!node) return null;
|
|
383
|
-
|
|
384
|
-
// If className is provided, first find the class, then search within it
|
|
385
|
-
if (className) {
|
|
386
|
-
const classNode = findClassNodePhp(node, className);
|
|
387
|
-
if (!classNode) return null;
|
|
388
|
-
|
|
389
|
-
// Search only within this class's body
|
|
390
|
-
const body = Array.isArray(classNode.body) ? classNode.body : [];
|
|
391
|
-
for (const member of body) {
|
|
392
|
-
if (member.kind === "method" && getNodeName(member) === symbolName) {
|
|
393
|
-
return member;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const nodeName = getNodeName(node);
|
|
400
|
-
|
|
401
|
-
// Direct match on class, interface, trait, enum, function
|
|
402
|
-
if (
|
|
403
|
-
["class", "interface", "trait", "enum", "function"].includes(node.kind) &&
|
|
404
|
-
nodeName === symbolName
|
|
405
|
-
) {
|
|
406
|
-
return node;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Search methods inside classes/interfaces/traits/enums
|
|
410
|
-
if (["class", "interface", "trait", "enum"].includes(node.kind)) {
|
|
411
|
-
const body = Array.isArray(node.body) ? node.body : [];
|
|
412
|
-
for (const member of body) {
|
|
413
|
-
if (member.kind === "method" && getNodeName(member) === symbolName) {
|
|
414
|
-
return member;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Recursive search through children
|
|
420
|
-
const allChildren = [
|
|
421
|
-
...(node.children || []),
|
|
422
|
-
...(Array.isArray(node.body) ? node.body : []),
|
|
423
|
-
];
|
|
424
|
-
|
|
425
|
-
for (const child of allChildren) {
|
|
426
|
-
const found = findSymbolNode(child, symbolName);
|
|
427
|
-
if (found) return found;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Walk backward from a symbol's start to capture leading doc comments.
|
|
435
|
-
* Returns the 1-indexed line number of the actual start.
|
|
436
|
-
*/
|
|
437
|
-
function findActualStart(lines: string[], node: PhpNode): number {
|
|
438
|
-
if (!node.loc) return 1;
|
|
439
|
-
|
|
440
|
-
let startLine = node.loc.start.line; // 1-indexed
|
|
441
|
-
|
|
442
|
-
if (node.leadingComments && node.leadingComments.length > 0) {
|
|
443
|
-
for (let i = startLine - 2; i >= 0; i--) {
|
|
444
|
-
const trimmed = lines[i].trim();
|
|
445
|
-
if (
|
|
446
|
-
trimmed.startsWith("/**") ||
|
|
447
|
-
trimmed.startsWith("*") ||
|
|
448
|
-
trimmed.startsWith("*/") ||
|
|
449
|
-
trimmed.startsWith("//") ||
|
|
450
|
-
trimmed.startsWith("#[")
|
|
451
|
-
) {
|
|
452
|
-
startLine = i + 1; // Convert back to 1-indexed
|
|
453
|
-
} else if (trimmed === "") {
|
|
454
|
-
continue;
|
|
455
|
-
} else {
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return startLine;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function getIndent(line: string): string {
|
|
465
|
-
const match = line.match(/^(\s*)/);
|
|
466
|
-
return match ? match[1] : "";
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Re-indent a block of code to match a target indentation level.
|
|
471
|
-
* Detects the minimum indentation of the input and replaces it with target.
|
|
472
|
-
*/
|
|
473
|
-
function reindentCode(code: string, targetIndent: string): string {
|
|
474
|
-
const lines = code.split("\n");
|
|
475
|
-
if (lines.length === 0) return code;
|
|
476
|
-
|
|
477
|
-
// Find minimum non-empty indentation
|
|
478
|
-
let minIndent = Infinity;
|
|
479
|
-
for (const line of lines) {
|
|
480
|
-
if (line.trim() === "") continue;
|
|
481
|
-
const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
482
|
-
if (indent < minIndent) minIndent = indent;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (minIndent === Infinity) minIndent = 0;
|
|
486
|
-
|
|
487
|
-
// Re-indent each line
|
|
488
|
-
return lines
|
|
489
|
-
.map((line) => {
|
|
490
|
-
if (line.trim() === "") return "";
|
|
491
|
-
return targetIndent + line.slice(minIndent);
|
|
492
|
-
})
|
|
493
|
-
.join("\n");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function findOpeningBrace(lines: string[], startIdx: number): number {
|
|
497
|
-
for (let i = startIdx; i < lines.length; i++) {
|
|
498
|
-
if (lines[i].includes("{")) return i;
|
|
499
|
-
}
|
|
500
|
-
return -1;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function findLastClosingBrace(lines: string[]): number {
|
|
504
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
505
|
-
if (lines[i].trim() === "}") return i;
|
|
506
|
-
}
|
|
507
|
-
return -1;
|
|
508
|
-
}
|