ucn 3.7.24 → 3.7.26
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/README.md +192 -463
- package/cli/index.js +285 -1054
- package/core/cache.js +193 -0
- package/core/callers.js +817 -0
- package/core/deadcode.js +320 -0
- package/core/discovery.js +1 -1
- package/core/execute.js +207 -10
- package/core/expand-cache.js +16 -5
- package/core/imports.js +21 -15
- package/core/output.js +370 -35
- package/core/project.js +365 -2272
- package/core/shared.js +11 -1
- package/core/stacktrace.js +313 -0
- package/core/verify.js +533 -0
- package/languages/go.js +57 -21
- package/languages/html.js +14 -3
- package/languages/java.js +4 -2
- package/languages/javascript.js +36 -9
- package/languages/rust.js +49 -17
- package/mcp/server.js +39 -172
- package/package.json +1 -1
package/core/verify.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/verify.js - Signature verification, refactoring planning, call site analysis
|
|
3
|
+
*
|
|
4
|
+
* Extracted from project.js. All functions take an `index` (ProjectIndex)
|
|
5
|
+
* as the first argument instead of using `this`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
|
|
9
|
+
const { escapeRegExp } = require('./shared');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find a call expression node at the target line matching funcName
|
|
13
|
+
*/
|
|
14
|
+
function findCallNode(node, callTypes, targetRow, funcName) {
|
|
15
|
+
if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) {
|
|
16
|
+
return null; // Skip nodes that don't contain the target line
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (callTypes.has(node.type) && node.startPosition.row <= targetRow && node.endPosition.row >= targetRow) {
|
|
20
|
+
// Java constructor: new ClassName(args) — name is in 'type' field
|
|
21
|
+
if (node.type === 'object_creation_expression') {
|
|
22
|
+
const typeNode = node.childForFieldName('type');
|
|
23
|
+
if (typeNode) {
|
|
24
|
+
// Strip generics and package qualifiers: com.foo.Bar<T> -> Bar
|
|
25
|
+
const typeName = typeNode.text.replace(/<.*>$/, '').split('.').pop();
|
|
26
|
+
if (typeName === funcName) return node;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// Check if this call is for our target function
|
|
30
|
+
const funcNode = node.childForFieldName('function') ||
|
|
31
|
+
node.childForFieldName('name'); // Java method_invocation uses 'name'
|
|
32
|
+
if (funcNode) {
|
|
33
|
+
const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
|
|
34
|
+
? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
35
|
+
: funcNode.text;
|
|
36
|
+
if (funcText === funcName) return node;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Recurse into children
|
|
42
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
43
|
+
const result = findCallNode(node.child(i), callTypes, targetRow, funcName);
|
|
44
|
+
if (result) return result;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear the AST tree cache (call after batch operations)
|
|
51
|
+
* @param {object} index - ProjectIndex instance
|
|
52
|
+
*/
|
|
53
|
+
function clearTreeCache(index) {
|
|
54
|
+
index._treeCache = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Analyze a call site to understand how it's being called (AST-based)
|
|
59
|
+
* @param {object} index - ProjectIndex instance
|
|
60
|
+
* @param {object} call - Usage object with file, line, content
|
|
61
|
+
* @param {string} funcName - Function name to find
|
|
62
|
+
* @returns {object} { args, argCount, hasSpread, hasVariable }
|
|
63
|
+
*/
|
|
64
|
+
function analyzeCallSite(index, call, funcName) {
|
|
65
|
+
try {
|
|
66
|
+
const language = detectLanguage(call.file);
|
|
67
|
+
if (!language) return { args: null, argCount: 0 };
|
|
68
|
+
|
|
69
|
+
// Use tree cache to avoid re-parsing the same file in batch operations
|
|
70
|
+
let tree = index._treeCache?.get(call.file);
|
|
71
|
+
if (!tree) {
|
|
72
|
+
const content = index._readFile(call.file);
|
|
73
|
+
// HTML files need special handling: parse script blocks as JS
|
|
74
|
+
if (language === 'html') {
|
|
75
|
+
const htmlModule = getLanguageModule('html');
|
|
76
|
+
const htmlParser = getParser('html');
|
|
77
|
+
const jsParser = getParser('javascript');
|
|
78
|
+
if (!htmlParser || !jsParser) return { args: null, argCount: 0 };
|
|
79
|
+
const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
|
|
80
|
+
if (blocks.length === 0) return { args: null, argCount: 0 };
|
|
81
|
+
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
82
|
+
tree = safeParse(jsParser, virtualJS);
|
|
83
|
+
} else {
|
|
84
|
+
const parser = getParser(language);
|
|
85
|
+
if (!parser) return { args: null, argCount: 0 };
|
|
86
|
+
tree = safeParse(parser, content);
|
|
87
|
+
}
|
|
88
|
+
if (!tree) return { args: null, argCount: 0 };
|
|
89
|
+
if (!index._treeCache) index._treeCache = new Map();
|
|
90
|
+
index._treeCache.set(call.file, tree);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Call node types vary by language
|
|
94
|
+
const callTypes = new Set(['call_expression', 'call', 'method_invocation', 'object_creation_expression']);
|
|
95
|
+
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
96
|
+
|
|
97
|
+
// Find the call expression at the target line matching funcName
|
|
98
|
+
const callNode = findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
99
|
+
if (!callNode) return { args: null, argCount: 0 };
|
|
100
|
+
|
|
101
|
+
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
102
|
+
const funcNode = callNode.childForFieldName('function') ||
|
|
103
|
+
callNode.childForFieldName('name');
|
|
104
|
+
let isMethodCall = false;
|
|
105
|
+
if (funcNode) {
|
|
106
|
+
// member_expression (JS), attribute (Python), selector_expression (Go), field_expression (Rust)
|
|
107
|
+
if (['member_expression', 'attribute', 'selector_expression', 'field_expression'].includes(funcNode.type)) {
|
|
108
|
+
isMethodCall = true;
|
|
109
|
+
}
|
|
110
|
+
// Java method_invocation with object
|
|
111
|
+
if (callNode.type === 'method_invocation' && callNode.childForFieldName('object')) {
|
|
112
|
+
isMethodCall = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
117
|
+
if (!argsNode) return { args: [], argCount: 0, isMethodCall };
|
|
118
|
+
|
|
119
|
+
const args = [];
|
|
120
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
121
|
+
args.push(argsNode.namedChild(i).text.trim());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
args,
|
|
126
|
+
argCount: args.length,
|
|
127
|
+
hasSpread: args.some(a => a.startsWith('...')),
|
|
128
|
+
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
129
|
+
isMethodCall
|
|
130
|
+
};
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { args: null, argCount: 0 };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Identify common calling patterns
|
|
138
|
+
* @param {Array} callSites - Array of call site objects
|
|
139
|
+
* @param {string} funcName - Function name
|
|
140
|
+
* @returns {object} Pattern counts
|
|
141
|
+
*/
|
|
142
|
+
function identifyCallPatterns(callSites, funcName) {
|
|
143
|
+
const patterns = {
|
|
144
|
+
constantArgs: 0, // Call sites with literal/constant arguments
|
|
145
|
+
variableArgs: 0, // Call sites passing variables
|
|
146
|
+
chainedCalls: 0, // Calls that are part of method chains
|
|
147
|
+
awaitedCalls: 0, // Async calls with await
|
|
148
|
+
spreadCalls: 0 // Calls using spread operator
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const site of callSites) {
|
|
152
|
+
const expr = site.expression;
|
|
153
|
+
|
|
154
|
+
if (site.hasSpread) patterns.spreadCalls++;
|
|
155
|
+
if (/await\s/.test(expr)) patterns.awaitedCalls++;
|
|
156
|
+
if (new RegExp('\\.' + escapeRegExp(funcName) + '\\s*\\(').test(expr)) patterns.chainedCalls++;
|
|
157
|
+
|
|
158
|
+
if (site.args && site.args.length > 0) {
|
|
159
|
+
const hasLiteral = site.args.some(a =>
|
|
160
|
+
/^[\d'"{\[]/.test(a) || a === 'true' || a === 'false' || a === 'null'
|
|
161
|
+
);
|
|
162
|
+
if (hasLiteral) patterns.constantArgs++;
|
|
163
|
+
if (site.hasVariable) patterns.variableArgs++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return patterns;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Verify that all call sites match a function's signature
|
|
172
|
+
* @param {object} index - ProjectIndex instance
|
|
173
|
+
* @param {string} name - Function name
|
|
174
|
+
* @param {object} options - { file }
|
|
175
|
+
* @returns {object} Verification results with mismatches
|
|
176
|
+
*/
|
|
177
|
+
function verify(index, name, options = {}) {
|
|
178
|
+
index._beginOp();
|
|
179
|
+
try {
|
|
180
|
+
const { def } = index.resolveSymbol(name, { file: options.file });
|
|
181
|
+
if (!def) {
|
|
182
|
+
return { found: false, function: name };
|
|
183
|
+
}
|
|
184
|
+
// For Python/Rust methods, exclude self/cls from parameter count
|
|
185
|
+
// (callers don't pass self/cls explicitly: obj.method(a, b) not obj.method(obj, a, b))
|
|
186
|
+
const fileEntry = index.files.get(def.file);
|
|
187
|
+
const lang = fileEntry?.language;
|
|
188
|
+
let params = def.paramsStructured || [];
|
|
189
|
+
if ((lang === 'python' || lang === 'rust') && params.length > 0) {
|
|
190
|
+
const firstName = params[0].name;
|
|
191
|
+
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
192
|
+
params = params.slice(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const hasRest = params.some(p => p.rest);
|
|
196
|
+
// Rest params don't count toward expected/min — they accept 0+ extra args
|
|
197
|
+
const nonRestParams = params.filter(p => !p.rest);
|
|
198
|
+
const expectedParamCount = nonRestParams.length;
|
|
199
|
+
const optionalCount = nonRestParams.filter(p => p.optional || p.default !== undefined).length;
|
|
200
|
+
const minArgs = expectedParamCount - optionalCount;
|
|
201
|
+
|
|
202
|
+
// Get all call sites
|
|
203
|
+
const usages = index.usages(name, { codeOnly: true });
|
|
204
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
205
|
+
|
|
206
|
+
const valid = [];
|
|
207
|
+
const mismatches = [];
|
|
208
|
+
const uncertain = [];
|
|
209
|
+
|
|
210
|
+
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
211
|
+
// This prevents false positives where a standalone function name matches method calls
|
|
212
|
+
const defIsMethod = def.isMethod || def.type === 'method' || def.className;
|
|
213
|
+
|
|
214
|
+
for (const call of calls) {
|
|
215
|
+
const analysis = analyzeCallSite(index, call, name);
|
|
216
|
+
|
|
217
|
+
// Skip method calls when verifying a non-method definition
|
|
218
|
+
if (analysis.isMethodCall && !defIsMethod) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (analysis.args === null) {
|
|
223
|
+
// Couldn't parse arguments
|
|
224
|
+
uncertain.push({
|
|
225
|
+
file: call.relativePath,
|
|
226
|
+
line: call.line,
|
|
227
|
+
expression: call.content.trim(),
|
|
228
|
+
reason: 'Could not parse call arguments'
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (analysis.hasSpread) {
|
|
234
|
+
// Spread args - can't verify count
|
|
235
|
+
uncertain.push({
|
|
236
|
+
file: call.relativePath,
|
|
237
|
+
line: call.line,
|
|
238
|
+
expression: call.content.trim(),
|
|
239
|
+
reason: 'Uses spread operator'
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const argCount = analysis.argCount;
|
|
245
|
+
|
|
246
|
+
// Check if arg count is valid
|
|
247
|
+
if (hasRest) {
|
|
248
|
+
// With rest param, need at least minArgs
|
|
249
|
+
if (argCount >= minArgs) {
|
|
250
|
+
valid.push({ file: call.relativePath, line: call.line });
|
|
251
|
+
} else {
|
|
252
|
+
mismatches.push({
|
|
253
|
+
file: call.relativePath,
|
|
254
|
+
line: call.line,
|
|
255
|
+
expression: call.content.trim(),
|
|
256
|
+
expected: `at least ${minArgs} arg(s)`,
|
|
257
|
+
actual: argCount,
|
|
258
|
+
args: analysis.args
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
// Without rest, need between minArgs and expectedParamCount
|
|
263
|
+
if (argCount >= minArgs && argCount <= expectedParamCount) {
|
|
264
|
+
valid.push({ file: call.relativePath, line: call.line });
|
|
265
|
+
} else {
|
|
266
|
+
mismatches.push({
|
|
267
|
+
file: call.relativePath,
|
|
268
|
+
line: call.line,
|
|
269
|
+
expression: call.content.trim(),
|
|
270
|
+
expected: minArgs === expectedParamCount
|
|
271
|
+
? `${expectedParamCount} arg(s)`
|
|
272
|
+
: `${minArgs}-${expectedParamCount} arg(s)`,
|
|
273
|
+
actual: argCount,
|
|
274
|
+
args: analysis.args
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
clearTreeCache(index);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
found: true,
|
|
283
|
+
function: name,
|
|
284
|
+
file: def.relativePath,
|
|
285
|
+
startLine: def.startLine,
|
|
286
|
+
signature: index.formatSignature(def),
|
|
287
|
+
params: params.map(p => ({
|
|
288
|
+
name: p.name,
|
|
289
|
+
optional: p.optional || p.default !== undefined,
|
|
290
|
+
hasDefault: p.default !== undefined
|
|
291
|
+
})),
|
|
292
|
+
expectedArgs: { min: minArgs, max: hasRest ? '∞' : expectedParamCount },
|
|
293
|
+
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
294
|
+
valid: valid.length,
|
|
295
|
+
mismatches: mismatches.length,
|
|
296
|
+
uncertain: uncertain.length,
|
|
297
|
+
mismatchDetails: mismatches,
|
|
298
|
+
uncertainDetails: uncertain
|
|
299
|
+
};
|
|
300
|
+
} finally { index._endOp(); }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Plan a refactoring operation
|
|
305
|
+
* @param {object} index - ProjectIndex instance
|
|
306
|
+
* @param {string} name - Function name
|
|
307
|
+
* @param {object} options - { addParam, removeParam, renameTo, defaultValue }
|
|
308
|
+
* @returns {object} Plan with before/after signatures and affected call sites
|
|
309
|
+
*/
|
|
310
|
+
function plan(index, name, options = {}) {
|
|
311
|
+
index._beginOp();
|
|
312
|
+
try {
|
|
313
|
+
const definitions = index.symbols.get(name);
|
|
314
|
+
if (!definitions || definitions.length === 0) {
|
|
315
|
+
return { found: false, function: name };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const resolved = index.resolveSymbol(name, { file: options.file });
|
|
319
|
+
const def = resolved.def || definitions[0];
|
|
320
|
+
const impact = index.impact(name, { file: options.file });
|
|
321
|
+
const currentParams = def.paramsStructured || [];
|
|
322
|
+
const currentSignature = index.formatSignature(def);
|
|
323
|
+
|
|
324
|
+
let newParams = [...currentParams];
|
|
325
|
+
let newSignature = currentSignature;
|
|
326
|
+
let operation = null;
|
|
327
|
+
let changes = [];
|
|
328
|
+
|
|
329
|
+
if (options.addParam) {
|
|
330
|
+
operation = 'add-param';
|
|
331
|
+
const newParam = {
|
|
332
|
+
name: options.addParam,
|
|
333
|
+
...(options.defaultValue && { default: options.defaultValue })
|
|
334
|
+
};
|
|
335
|
+
newParams.push(newParam);
|
|
336
|
+
|
|
337
|
+
// Generate new signature
|
|
338
|
+
const paramsList = newParams.map(p => {
|
|
339
|
+
let str = p.name;
|
|
340
|
+
if (p.type) str += `: ${p.type}`;
|
|
341
|
+
if (p.default) str += ` = ${p.default}`;
|
|
342
|
+
return str;
|
|
343
|
+
}).join(', ');
|
|
344
|
+
newSignature = `${name}(${paramsList})`;
|
|
345
|
+
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
346
|
+
|
|
347
|
+
// Describe changes needed at each call site
|
|
348
|
+
for (const fileGroup of impact.byFile) {
|
|
349
|
+
for (const site of fileGroup.sites) {
|
|
350
|
+
const suggestion = options.defaultValue
|
|
351
|
+
? `No change needed (has default value)`
|
|
352
|
+
: `Add argument: ${options.addParam}`;
|
|
353
|
+
changes.push({
|
|
354
|
+
file: site.file,
|
|
355
|
+
line: site.line,
|
|
356
|
+
expression: site.expression,
|
|
357
|
+
suggestion,
|
|
358
|
+
args: site.args
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (options.removeParam) {
|
|
365
|
+
operation = 'remove-param';
|
|
366
|
+
const paramIndex = currentParams.findIndex(p => p.name === options.removeParam);
|
|
367
|
+
if (paramIndex === -1) {
|
|
368
|
+
return {
|
|
369
|
+
found: true,
|
|
370
|
+
error: `Parameter "${options.removeParam}" not found in ${name}`,
|
|
371
|
+
currentParams: currentParams.map(p => p.name)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
newParams = currentParams.filter(p => p.name !== options.removeParam);
|
|
376
|
+
|
|
377
|
+
// Generate new signature
|
|
378
|
+
const paramsList = newParams.map(p => {
|
|
379
|
+
let str = p.name;
|
|
380
|
+
if (p.type) str += `: ${p.type}`;
|
|
381
|
+
if (p.default) str += ` = ${p.default}`;
|
|
382
|
+
return str;
|
|
383
|
+
}).join(', ');
|
|
384
|
+
newSignature = `${name}(${paramsList})`;
|
|
385
|
+
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
386
|
+
|
|
387
|
+
// For Python/Rust methods, self/cls/&self/&mut self is in paramsStructured
|
|
388
|
+
// but callers don't pass it. Adjust paramIndex to caller-side position.
|
|
389
|
+
const fileEntry = index.files.get(def.file);
|
|
390
|
+
const lang = fileEntry?.language;
|
|
391
|
+
let selfOffset = 0;
|
|
392
|
+
if ((lang === 'python' || lang === 'rust') && currentParams.length > 0) {
|
|
393
|
+
const firstName = currentParams[0].name;
|
|
394
|
+
if (firstName === 'self' || firstName === 'cls' || firstName === '&self' || firstName === '&mut self') {
|
|
395
|
+
selfOffset = 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const callerArgIndex = paramIndex - selfOffset;
|
|
399
|
+
|
|
400
|
+
// Describe changes at each call site
|
|
401
|
+
for (const fileGroup of impact.byFile) {
|
|
402
|
+
for (const site of fileGroup.sites) {
|
|
403
|
+
if (site.args && site.argCount > callerArgIndex) {
|
|
404
|
+
changes.push({
|
|
405
|
+
file: site.file,
|
|
406
|
+
line: site.line,
|
|
407
|
+
expression: site.expression,
|
|
408
|
+
suggestion: `Remove argument ${callerArgIndex + 1}: ${site.args[callerArgIndex] || '?'}`,
|
|
409
|
+
args: site.args
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (options.renameTo) {
|
|
417
|
+
operation = 'rename';
|
|
418
|
+
newSignature = currentSignature.replace(new RegExp('\\b' + escapeRegExp(name) + '\\b'), options.renameTo);
|
|
419
|
+
|
|
420
|
+
// All call sites need renaming
|
|
421
|
+
for (const fileGroup of impact.byFile) {
|
|
422
|
+
for (const site of fileGroup.sites) {
|
|
423
|
+
const newExpression = site.expression.replace(
|
|
424
|
+
new RegExp('\\b' + escapeRegExp(name) + '\\b'),
|
|
425
|
+
options.renameTo
|
|
426
|
+
);
|
|
427
|
+
changes.push({
|
|
428
|
+
file: site.file,
|
|
429
|
+
line: site.line,
|
|
430
|
+
expression: site.expression,
|
|
431
|
+
suggestion: `Rename to: ${newExpression}`,
|
|
432
|
+
newExpression
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
found: true,
|
|
440
|
+
function: name,
|
|
441
|
+
file: def.relativePath,
|
|
442
|
+
startLine: def.startLine,
|
|
443
|
+
operation,
|
|
444
|
+
before: {
|
|
445
|
+
signature: currentSignature,
|
|
446
|
+
params: currentParams.map(p => p.name)
|
|
447
|
+
},
|
|
448
|
+
after: {
|
|
449
|
+
signature: newSignature,
|
|
450
|
+
params: newParams.map(p => p.name)
|
|
451
|
+
},
|
|
452
|
+
totalChanges: changes.length,
|
|
453
|
+
filesAffected: new Set(changes.map(c => c.file)).size,
|
|
454
|
+
changes
|
|
455
|
+
};
|
|
456
|
+
} finally { index._endOp(); }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Analyze a call site using AST for example scoring.
|
|
461
|
+
* @param {object} index - ProjectIndex instance
|
|
462
|
+
* @param {string} filePath - File path
|
|
463
|
+
* @param {number} lineNum - Line number
|
|
464
|
+
* @param {string} funcName - Function name
|
|
465
|
+
* @returns {object} Analysis results
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
function analyzeCallSiteAST(index, filePath, lineNum, funcName) {
|
|
469
|
+
const result = {
|
|
470
|
+
isAwait: false, isDestructured: false, isTypedAssignment: false,
|
|
471
|
+
isInReturn: false, isInCatch: false, isInConditional: false,
|
|
472
|
+
hasComment: false, isStandalone: false
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const language = detectLanguage(filePath);
|
|
477
|
+
if (!language) return result;
|
|
478
|
+
|
|
479
|
+
const parser = getParser(language);
|
|
480
|
+
const content = index._readFile(filePath);
|
|
481
|
+
const tree = safeParse(parser, content);
|
|
482
|
+
if (!tree) return result;
|
|
483
|
+
|
|
484
|
+
const row = lineNum - 1;
|
|
485
|
+
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
486
|
+
if (!node) return result;
|
|
487
|
+
|
|
488
|
+
let current = node;
|
|
489
|
+
let foundCall = false;
|
|
490
|
+
|
|
491
|
+
while (current) {
|
|
492
|
+
const type = current.type;
|
|
493
|
+
|
|
494
|
+
if (!foundCall && (type === 'call_expression' || type === 'call')) {
|
|
495
|
+
const calleeNode = current.childForFieldName('function') || current.namedChild(0);
|
|
496
|
+
if (calleeNode && calleeNode.text === funcName) {
|
|
497
|
+
foundCall = true;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (foundCall) {
|
|
502
|
+
if (type === 'await_expression') result.isAwait = true;
|
|
503
|
+
if (type === 'variable_declarator' || type === 'assignment_expression') {
|
|
504
|
+
const parent = current.parent;
|
|
505
|
+
if (parent && (parent.type === 'lexical_declaration' || parent.type === 'variable_declaration')) {
|
|
506
|
+
result.isTypedAssignment = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (type === 'array_pattern' || type === 'object_pattern') result.isDestructured = true;
|
|
510
|
+
if (type === 'return_statement') result.isInReturn = true;
|
|
511
|
+
if (type === 'catch_clause' || type === 'except_clause') result.isInCatch = true;
|
|
512
|
+
if (type === 'if_statement' || type === 'conditional_expression' || type === 'ternary_expression') result.isInConditional = true;
|
|
513
|
+
if (type === 'expression_statement') result.isStandalone = true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
current = current.parent;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const contentLines = content.split('\n');
|
|
520
|
+
if (lineNum > 1) {
|
|
521
|
+
const prevLine = contentLines[lineNum - 2].trim();
|
|
522
|
+
if (prevLine.startsWith('//') || prevLine.startsWith('#') || prevLine.endsWith('*/')) {
|
|
523
|
+
result.hasComment = true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (e) {
|
|
527
|
+
// Return default result on error
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
module.exports = { verify, plan, analyzeCallSite, analyzeCallSiteAST, findCallNode, clearTreeCache, identifyCallPatterns };
|
package/languages/go.js
CHANGED
|
@@ -45,8 +45,13 @@ function extractGoParams(paramsNode) {
|
|
|
45
45
|
function extractReceiver(receiverNode) {
|
|
46
46
|
if (!receiverNode) return null;
|
|
47
47
|
const text = receiverNode.text;
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// Match named receiver: (r *Router) or (r Router[T])
|
|
49
|
+
const namedMatch = text.match(/\(\s*\w+\s+(\*?\w+(?:\[[\w,\s]+\])?)\s*\)/);
|
|
50
|
+
if (namedMatch) return namedMatch[1];
|
|
51
|
+
// Match unnamed receiver: (Router) or (*Router) or (Router[T])
|
|
52
|
+
const unnamedMatch = text.match(/\(\s*(\*?\w+(?:\[[\w,\s]+\])?)\s*\)/);
|
|
53
|
+
if (unnamedMatch) return unnamedMatch[1];
|
|
54
|
+
return text.replace(/^\(|\)$/g, '').trim();
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -215,7 +220,17 @@ function findClasses(code, parser) {
|
|
|
215
220
|
*/
|
|
216
221
|
function extractStructFields(structNode, code) {
|
|
217
222
|
const fields = [];
|
|
218
|
-
|
|
223
|
+
// struct_type contains a field_declaration_list child (not a 'body' field)
|
|
224
|
+
let fieldListNode = structNode.childForFieldName('body');
|
|
225
|
+
if (!fieldListNode) {
|
|
226
|
+
for (let i = 0; i < structNode.namedChildCount; i++) {
|
|
227
|
+
if (structNode.namedChild(i).type === 'field_declaration_list') {
|
|
228
|
+
fieldListNode = structNode.namedChild(i);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!fieldListNode) fieldListNode = structNode;
|
|
219
234
|
|
|
220
235
|
for (let i = 0; i < fieldListNode.namedChildCount; i++) {
|
|
221
236
|
const field = fieldListNode.namedChild(i);
|
|
@@ -444,9 +459,9 @@ function findCallsInCode(code, parser) {
|
|
|
444
459
|
});
|
|
445
460
|
}
|
|
446
461
|
|
|
447
|
-
// Track local closures: atoi := func(...) { ... }
|
|
462
|
+
// Track local closures: atoi := func(...) { ... } or var handler = func(...) { ... }
|
|
448
463
|
if (node.type === 'short_var_declaration' || node.type === 'var_declaration') {
|
|
449
|
-
// Check if
|
|
464
|
+
// Check if a subtree contains a func_literal
|
|
450
465
|
const hasFunc = (n) => {
|
|
451
466
|
if (!n) return false;
|
|
452
467
|
if (n.type === 'func_literal') return true;
|
|
@@ -455,20 +470,41 @@ function findCallsInCode(code, parser) {
|
|
|
455
470
|
}
|
|
456
471
|
return false;
|
|
457
472
|
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (!closureScopes.has(scopeKey)) closureScopes.set(scopeKey, new Set());
|
|
469
|
-
for (const n of names) closureScopes.get(scopeKey).add(n);
|
|
473
|
+
let names = [];
|
|
474
|
+
if (node.type === 'short_var_declaration') {
|
|
475
|
+
// short_var_declaration checks the whole RHS
|
|
476
|
+
if (hasFunc(node)) {
|
|
477
|
+
const left = node.childForFieldName('left');
|
|
478
|
+
if (left) {
|
|
479
|
+
names = left.type === 'expression_list'
|
|
480
|
+
? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
|
|
481
|
+
.filter(n => n.type === 'identifier').map(n => n.text)
|
|
482
|
+
: left.type === 'identifier' ? [left.text] : [];
|
|
470
483
|
}
|
|
471
484
|
}
|
|
485
|
+
} else {
|
|
486
|
+
// var_declaration: check per-spec so only names with func_literal values are tracked
|
|
487
|
+
// Handle both: var x = func(){} (var_declaration > var_spec)
|
|
488
|
+
// and: var (\n x = func(){} \n) (var_declaration > var_spec_list > var_spec)
|
|
489
|
+
const collectClosureNames = (parent) => {
|
|
490
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
491
|
+
const child = parent.namedChild(i);
|
|
492
|
+
if (child.type === 'var_spec' && hasFunc(child)) {
|
|
493
|
+
const nameNode = child.childForFieldName('name');
|
|
494
|
+
if (nameNode && nameNode.type === 'identifier') {
|
|
495
|
+
names.push(nameNode.text);
|
|
496
|
+
}
|
|
497
|
+
} else if (child.type === 'var_spec_list') {
|
|
498
|
+
collectClosureNames(child);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
collectClosureNames(node);
|
|
503
|
+
}
|
|
504
|
+
if (names.length > 0 && functionStack.length > 0) {
|
|
505
|
+
const scopeKey = functionStack[functionStack.length - 1].startLine;
|
|
506
|
+
if (!closureScopes.has(scopeKey)) closureScopes.set(scopeKey, new Set());
|
|
507
|
+
for (const n of names) closureScopes.get(scopeKey).add(n);
|
|
472
508
|
}
|
|
473
509
|
}
|
|
474
510
|
|
|
@@ -546,8 +582,8 @@ function findImportsInCode(code, parser) {
|
|
|
546
582
|
|
|
547
583
|
for (let i = 0; i < spec.namedChildCount; i++) {
|
|
548
584
|
const child = spec.namedChild(i);
|
|
549
|
-
if (child.type === 'interpreted_string_literal') {
|
|
550
|
-
// Remove quotes
|
|
585
|
+
if (child.type === 'interpreted_string_literal' || child.type === 'raw_string_literal') {
|
|
586
|
+
// Remove quotes (double quotes or backticks)
|
|
551
587
|
modulePath = child.text.slice(1, -1);
|
|
552
588
|
} else if (child.type === 'package_identifier') {
|
|
553
589
|
alias = child.text;
|
|
@@ -647,7 +683,7 @@ function findExportsInCode(code, parser) {
|
|
|
647
683
|
exports.push({
|
|
648
684
|
name: nameNode.text,
|
|
649
685
|
type: 'type',
|
|
650
|
-
line:
|
|
686
|
+
line: spec.startPosition.row + 1
|
|
651
687
|
});
|
|
652
688
|
}
|
|
653
689
|
}
|
|
@@ -665,7 +701,7 @@ function findExportsInCode(code, parser) {
|
|
|
665
701
|
exports.push({
|
|
666
702
|
name: nameNode.text,
|
|
667
703
|
type: node.type === 'const_declaration' ? 'const' : 'var',
|
|
668
|
-
line:
|
|
704
|
+
line: spec.startPosition.row + 1
|
|
669
705
|
});
|
|
670
706
|
}
|
|
671
707
|
}
|