ucn 3.8.23 → 3.8.25
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/.claude/skills/ucn/SKILL.md +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +936 -32
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph.js +24 -2
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -10
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +9 -1
package/core/verify.js
CHANGED
|
@@ -10,6 +10,134 @@ const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits } =
|
|
|
10
10
|
const { escapeRegExp } = require('./shared');
|
|
11
11
|
const { extractImports } = require('./imports');
|
|
12
12
|
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// CALL-SITE CLASSIFICATION (Feature A)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// AST node-type sets per language for walk-up classification of call sites.
|
|
17
|
+
// Detection is structural — we walk parents from the call node and stop at
|
|
18
|
+
// function boundaries to keep the classification scoped to the enclosing fn.
|
|
19
|
+
|
|
20
|
+
// Loop nodes — call sites inside these are "hot path" (likely repeated).
|
|
21
|
+
const LOOP_NODE_TYPES = {
|
|
22
|
+
javascript: new Set(['for_statement', 'while_statement', 'do_statement', 'for_in_statement', 'for_of_statement']),
|
|
23
|
+
typescript: new Set(['for_statement', 'while_statement', 'do_statement', 'for_in_statement', 'for_of_statement']),
|
|
24
|
+
tsx: new Set(['for_statement', 'while_statement', 'do_statement', 'for_in_statement', 'for_of_statement']),
|
|
25
|
+
html: new Set(['for_statement', 'while_statement', 'do_statement', 'for_in_statement', 'for_of_statement']),
|
|
26
|
+
python: new Set(['for_statement', 'while_statement']),
|
|
27
|
+
go: new Set(['for_statement']),
|
|
28
|
+
rust: new Set(['for_expression', 'while_expression', 'loop_expression']),
|
|
29
|
+
java: new Set(['for_statement', 'while_statement', 'do_statement', 'enhanced_for_statement']),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Try nodes — call sites inside these are "guarded" (errors are caught).
|
|
33
|
+
// Go uses defer/recover (skipped). Rust uses Result-based error handling (skipped).
|
|
34
|
+
const TRY_NODE_TYPES = {
|
|
35
|
+
javascript: new Set(['try_statement']),
|
|
36
|
+
typescript: new Set(['try_statement']),
|
|
37
|
+
tsx: new Set(['try_statement']),
|
|
38
|
+
html: new Set(['try_statement']),
|
|
39
|
+
python: new Set(['try_statement']),
|
|
40
|
+
go: new Set(),
|
|
41
|
+
rust: new Set(),
|
|
42
|
+
java: new Set(['try_statement', 'try_with_resources_statement']),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Function boundary nodes — walk-up stops at these (we don't classify across
|
|
46
|
+
// inner function definitions). These also identify "callback wrappers" when
|
|
47
|
+
// they're the value of an argument to another call_expression.
|
|
48
|
+
const FN_NODE_TYPES = {
|
|
49
|
+
javascript: new Set(['function_declaration', 'function_expression', 'arrow_function', 'method_definition', 'generator_function', 'generator_function_declaration']),
|
|
50
|
+
typescript: new Set(['function_declaration', 'function_expression', 'arrow_function', 'method_definition', 'generator_function', 'generator_function_declaration', 'function_signature']),
|
|
51
|
+
tsx: new Set(['function_declaration', 'function_expression', 'arrow_function', 'method_definition', 'generator_function', 'generator_function_declaration', 'function_signature']),
|
|
52
|
+
html: new Set(['function_declaration', 'function_expression', 'arrow_function', 'method_definition', 'generator_function', 'generator_function_declaration']),
|
|
53
|
+
python: new Set(['function_definition', 'async_function_definition', 'lambda']),
|
|
54
|
+
go: new Set(['function_declaration', 'method_declaration', 'func_literal']),
|
|
55
|
+
rust: new Set(['function_item', 'closure_expression']),
|
|
56
|
+
java: new Set(['method_declaration', 'constructor_declaration', 'lambda_expression']),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Await-expression node types per language with async/await support.
|
|
60
|
+
// JS/TS: await is a unary expression `await call()`.
|
|
61
|
+
// Python: await is `await call()`.
|
|
62
|
+
// Go/Java/Rust currently have no await keyword tracked here.
|
|
63
|
+
const AWAIT_NODE_TYPES = {
|
|
64
|
+
javascript: new Set(['await_expression']),
|
|
65
|
+
typescript: new Set(['await_expression']),
|
|
66
|
+
tsx: new Set(['await_expression']),
|
|
67
|
+
html: new Set(['await_expression']),
|
|
68
|
+
python: new Set(['await']),
|
|
69
|
+
go: new Set(),
|
|
70
|
+
rust: new Set(),
|
|
71
|
+
java: new Set(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Argument-list node types — used to detect callback context. When walking up,
|
|
75
|
+
// if a function/lambda we cross has a parent of these types (which is itself
|
|
76
|
+
// inside a call_expression), the inner call is in a callback.
|
|
77
|
+
const ARGUMENTS_NODE_TYPES = new Set(['arguments', 'argument_list']);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Classify a call site by walking up its ancestors.
|
|
81
|
+
*
|
|
82
|
+
* Returns flags describing the structural context: `inLoop`, `inTry`,
|
|
83
|
+
* `inCallback`, `awaited`. Walks from the call node up to the enclosing
|
|
84
|
+
* function boundary (so an outer try wrapping an inner function does NOT
|
|
85
|
+
* leak `inTry: true` into a call inside the inner function).
|
|
86
|
+
*
|
|
87
|
+
* `inCallback` is set when, while walking up to the boundary, we cross an
|
|
88
|
+
* inner function/lambda that is itself an argument of another call.
|
|
89
|
+
*
|
|
90
|
+
* `awaited` is set when the call expression's immediate parent is an
|
|
91
|
+
* await-style node. Non-async languages always return `awaited: false`.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} callNode - tree-sitter node for the call
|
|
94
|
+
* @param {string} language - canonical language name
|
|
95
|
+
* @returns {{inLoop:boolean, inTry:boolean, inCallback:boolean, awaited:boolean}}
|
|
96
|
+
*/
|
|
97
|
+
function classifyCallContext(callNode, language) {
|
|
98
|
+
const result = { inLoop: false, inTry: false, inCallback: false, awaited: false };
|
|
99
|
+
if (!callNode) return result;
|
|
100
|
+
|
|
101
|
+
const loopTypes = LOOP_NODE_TYPES[language] || new Set();
|
|
102
|
+
const tryTypes = TRY_NODE_TYPES[language] || new Set();
|
|
103
|
+
const fnTypes = FN_NODE_TYPES[language] || new Set();
|
|
104
|
+
const awaitTypes = AWAIT_NODE_TYPES[language] || new Set();
|
|
105
|
+
|
|
106
|
+
// awaited: parent of the call must be an await-style node.
|
|
107
|
+
// Some grammars (Python) wrap the call in `await { call }`; others
|
|
108
|
+
// (JS/TS) use `await_expression > call_expression`. Both are detected by
|
|
109
|
+
// checking the immediate parent.
|
|
110
|
+
if (callNode.parent && awaitTypes.has(callNode.parent.type)) {
|
|
111
|
+
result.awaited = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Walk up to classify loop/try/callback. Stop when we cross a function
|
|
115
|
+
// boundary — an inner closure isolates the inner call from outer context.
|
|
116
|
+
let current = callNode.parent;
|
|
117
|
+
while (current) {
|
|
118
|
+
const t = current.type;
|
|
119
|
+
if (loopTypes.has(t)) result.inLoop = true;
|
|
120
|
+
if (tryTypes.has(t)) result.inTry = true;
|
|
121
|
+
// Function boundary — stop, but first check if THIS function is an
|
|
122
|
+
// argument to another call (callback context). The ancestor chain is:
|
|
123
|
+
// outer_call > arguments > arrow_function/lambda > … > inner call
|
|
124
|
+
if (fnTypes.has(t)) {
|
|
125
|
+
const parent = current.parent;
|
|
126
|
+
if (parent && ARGUMENTS_NODE_TYPES.has(parent.type)) {
|
|
127
|
+
const grand = parent.parent;
|
|
128
|
+
if (grand && (grand.type === 'call_expression' || grand.type === 'call' ||
|
|
129
|
+
grand.type === 'method_invocation' || grand.type === 'object_creation_expression' ||
|
|
130
|
+
grand.type === 'macro_invocation')) {
|
|
131
|
+
result.inCallback = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
current = current.parent;
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
13
141
|
/**
|
|
14
142
|
* Find a call expression node at the target line matching funcName
|
|
15
143
|
*/
|
|
@@ -62,6 +190,466 @@ function clearTreeCache(index) {
|
|
|
62
190
|
index._treeCache = null;
|
|
63
191
|
}
|
|
64
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Render a single parameter with TS-correct optional marker placement.
|
|
195
|
+
* BUG-BV fix: `?` follows the NAME, not the TYPE (e.g. `opt?: number`,
|
|
196
|
+
* not the invalid `opt: number?`). Used by verify/plan signature output.
|
|
197
|
+
* @param {object} p - Param object {name, type?, optional?, default?, rest?}
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
function formatTypedParam(p) {
|
|
201
|
+
if (!p || !p.name) return '';
|
|
202
|
+
// Rest-param prefix:
|
|
203
|
+
// Python `**kwargs` / `*args` keep their `*` prefix (name already starts with `*`).
|
|
204
|
+
// JS/TS rest like `...rest` keeps `...` (avoid double-prefix if name already has `...`).
|
|
205
|
+
// Bare names with rest=true get `...` prefix (JS rest with stripped pattern name).
|
|
206
|
+
let s;
|
|
207
|
+
if (p.rest) {
|
|
208
|
+
const n = String(p.name);
|
|
209
|
+
if (n.startsWith('*') || n.startsWith('...')) s = n;
|
|
210
|
+
else s = `...${n}`;
|
|
211
|
+
} else {
|
|
212
|
+
s = p.name;
|
|
213
|
+
}
|
|
214
|
+
// Optional marker — placed AFTER name, BEFORE type (TS syntax: `opt?: number`)
|
|
215
|
+
if (p.optional && !p.rest && p.default == null) s += '?';
|
|
216
|
+
if (p.type) s += `: ${p.type}`;
|
|
217
|
+
if (p.default != null) s += ` = ${p.default}`;
|
|
218
|
+
return s;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Render a param name for the plan `before.params` / `after.params` arrays.
|
|
223
|
+
* These arrays are name-keyed (callers do `.includes('retries')` exact match),
|
|
224
|
+
* so we keep TS optional `?` and type annotation for BUG-BV/#181 contracts,
|
|
225
|
+
* but omit the ` = default` suffix and rest `*`/`...` prefix that callers don't
|
|
226
|
+
* test against. Mirrors the pre-rewrite shape of plan output.
|
|
227
|
+
* @param {object} p
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
function formatPlanParamName(p) {
|
|
231
|
+
if (!p || !p.name) return '';
|
|
232
|
+
let s = p.name;
|
|
233
|
+
if (p.optional && !p.default) s += '?';
|
|
234
|
+
if (p.type) s += `: ${p.type}`;
|
|
235
|
+
return s;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Compute the modifier-prefix tokens for a function/method definition.
|
|
240
|
+
* Returns an array of tokens (e.g. ['static', 'async']) drawn from:
|
|
241
|
+
* - def.modifiers (Java, Python async, Rust pub/async, ...)
|
|
242
|
+
* - def.isAsync / def.async (JS/TS class methods)
|
|
243
|
+
* - def.memberType (JS/TS: 'static', 'static get', 'static override', ...)
|
|
244
|
+
*
|
|
245
|
+
* BUG-5: rename and add-param signature reconstruction must preserve modifier
|
|
246
|
+
* prefixes (async/static/public/...) — JS class methods don't populate
|
|
247
|
+
* def.modifiers, so we synthesise tokens from isAsync + memberType.
|
|
248
|
+
* @param {object} def
|
|
249
|
+
* @returns {string[]} ordered modifier tokens (no trailing space)
|
|
250
|
+
*/
|
|
251
|
+
function computeModifierTokens(def) {
|
|
252
|
+
if (!def) return [];
|
|
253
|
+
const tokens = [];
|
|
254
|
+
// Pull declared modifiers first (Java public/static/final, Python ['async'], Rust pub/async).
|
|
255
|
+
if (Array.isArray(def.modifiers) && def.modifiers.length) {
|
|
256
|
+
for (const m of def.modifiers) {
|
|
257
|
+
if (typeof m === 'string' && m.length && !tokens.includes(m)) tokens.push(m);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// JS/TS class methods: memberType encodes static/get/set/override/private.
|
|
261
|
+
// Examples: 'static', 'static get', 'static override', 'static override get',
|
|
262
|
+
// 'override', 'override get', 'get', 'set', 'private', 'method',
|
|
263
|
+
// 'abstract', 'constructor'. Only structural prefixes are added.
|
|
264
|
+
const memberType = def.memberType;
|
|
265
|
+
if (typeof memberType === 'string' && memberType.length) {
|
|
266
|
+
const STRUCTURAL_PREFIXES = new Set(['static', 'override', 'abstract', 'public', 'private', 'protected', 'readonly', 'get', 'set']);
|
|
267
|
+
for (const tok of memberType.split(/\s+/)) {
|
|
268
|
+
if (STRUCTURAL_PREFIXES.has(tok) && !tokens.includes(tok)) tokens.push(tok);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Async (JS/TS isAsync, fallback for languages that set def.async).
|
|
272
|
+
const asyncFlag = def.isAsync || def.async || (Array.isArray(def.modifiers) && def.modifiers.includes('async'));
|
|
273
|
+
if (asyncFlag && !tokens.includes('async')) tokens.push('async');
|
|
274
|
+
return tokens;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build a function signature string from a definition, using
|
|
279
|
+
* TS-correct param formatting (BUG-BV). Local to verify.js to avoid
|
|
280
|
+
* the shared formatter's incorrect `?` placement.
|
|
281
|
+
* @param {object} def - Symbol definition
|
|
282
|
+
* @param {object} [overrides] - Optional { paramsStructured, returnType, name } overrides
|
|
283
|
+
* @returns {string}
|
|
284
|
+
*/
|
|
285
|
+
function formatTypedSignature(def, overrides = {}) {
|
|
286
|
+
const parts = [];
|
|
287
|
+
const modTokens = computeModifierTokens(def);
|
|
288
|
+
if (modTokens.length) {
|
|
289
|
+
parts.push(modTokens.join(' '));
|
|
290
|
+
}
|
|
291
|
+
const name = overrides.name || def.name;
|
|
292
|
+
parts.push(name);
|
|
293
|
+
const ps = overrides.paramsStructured != null ? overrides.paramsStructured : def.paramsStructured;
|
|
294
|
+
if (Array.isArray(ps)) {
|
|
295
|
+
const paramTypes = def.paramTypes || {};
|
|
296
|
+
const parts2 = ps.map(p => {
|
|
297
|
+
// Apply paramTypes mapping when paramsStructured doesn't carry types
|
|
298
|
+
const merged = { ...p };
|
|
299
|
+
if (!merged.type && paramTypes[p.name]) merged.type = paramTypes[p.name];
|
|
300
|
+
return formatTypedParam(merged);
|
|
301
|
+
});
|
|
302
|
+
parts.push(`(${parts2.filter(Boolean).join(', ')})`);
|
|
303
|
+
} else if (def.params !== undefined) {
|
|
304
|
+
parts.push(`(${def.params})`);
|
|
305
|
+
}
|
|
306
|
+
const rt = overrides.returnType != null ? overrides.returnType : def.returnType;
|
|
307
|
+
if (rt) parts.push(`: ${rt}`);
|
|
308
|
+
return parts.join(' ');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* BUG-BY: For an arrow function declared as `const x: (a: number) => number = (a) => ...`
|
|
313
|
+
* the inline arrow params/return type are missing types — they live on the
|
|
314
|
+
* variable_declarator's type_annotation. Walk up to the declarator and
|
|
315
|
+
* extract `function_type` parts (params + return type) when present.
|
|
316
|
+
*
|
|
317
|
+
* Returns null if no enrichment is available; otherwise an object with
|
|
318
|
+
* { paramsStructured, returnType } suitable for use as overrides.
|
|
319
|
+
*
|
|
320
|
+
* Only applies to TS-family files (typescript/tsx). JS doesn't have function_type
|
|
321
|
+
* annotations at the variable declarator level.
|
|
322
|
+
*
|
|
323
|
+
* @param {object} index - ProjectIndex instance
|
|
324
|
+
* @param {object} def - Symbol definition (must have file + startLine)
|
|
325
|
+
* @returns {{ paramsStructured: Array, returnType: string|null }|null}
|
|
326
|
+
*/
|
|
327
|
+
function extractArrowTypesFromVarDecl(index, def) {
|
|
328
|
+
if (!def || !def.file || !def.startLine) return null;
|
|
329
|
+
const lang = detectLanguage(def.file);
|
|
330
|
+
if (lang !== 'typescript' && lang !== 'tsx') return null;
|
|
331
|
+
// Already have types — nothing to enrich.
|
|
332
|
+
const ps = def.paramsStructured;
|
|
333
|
+
const allHaveTypes = Array.isArray(ps) && ps.length > 0 && ps.every(p => p && p.type);
|
|
334
|
+
if (allHaveTypes && def.returnType) return null;
|
|
335
|
+
let parser;
|
|
336
|
+
try {
|
|
337
|
+
parser = getParser(lang);
|
|
338
|
+
} catch (e) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if (!parser) return null;
|
|
342
|
+
let content;
|
|
343
|
+
try {
|
|
344
|
+
content = index._readFile(def.file);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const tree = safeParse(parser, content);
|
|
349
|
+
if (!tree) return null;
|
|
350
|
+
|
|
351
|
+
// Find the variable_declarator that wraps the arrow function at def.startLine
|
|
352
|
+
const targetRow = def.startLine - 1;
|
|
353
|
+
function findVarDecl(node) {
|
|
354
|
+
if (!node) return null;
|
|
355
|
+
if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) return null;
|
|
356
|
+
if (node.type === 'variable_declarator') {
|
|
357
|
+
// Check if this declarator's value is an arrow_function (or function_expression)
|
|
358
|
+
const valueNode = node.childForFieldName('value');
|
|
359
|
+
if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression' || valueNode.type === 'function')) {
|
|
360
|
+
// Confirm name matches and starts at our target row
|
|
361
|
+
const nameNode = node.childForFieldName('name');
|
|
362
|
+
if (nameNode && nameNode.text === def.name) {
|
|
363
|
+
return node;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
368
|
+
const result = findVarDecl(node.namedChild(i));
|
|
369
|
+
if (result) return result;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const declarator = findVarDecl(tree.rootNode);
|
|
374
|
+
if (!declarator) return null;
|
|
375
|
+
|
|
376
|
+
// Look for type_annotation child holding a function_type
|
|
377
|
+
let typeAnno = null;
|
|
378
|
+
for (let i = 0; i < declarator.namedChildCount; i++) {
|
|
379
|
+
const child = declarator.namedChild(i);
|
|
380
|
+
if (child.type === 'type_annotation') { typeAnno = child; break; }
|
|
381
|
+
}
|
|
382
|
+
if (!typeAnno) return null;
|
|
383
|
+
// type_annotation > function_type
|
|
384
|
+
let fnType = null;
|
|
385
|
+
for (let i = 0; i < typeAnno.namedChildCount; i++) {
|
|
386
|
+
const child = typeAnno.namedChild(i);
|
|
387
|
+
if (child.type === 'function_type') { fnType = child; break; }
|
|
388
|
+
}
|
|
389
|
+
if (!fnType) return null;
|
|
390
|
+
// function_type has formal_parameters + a return type sibling
|
|
391
|
+
const fp = fnType.childForFieldName('parameters') || (() => {
|
|
392
|
+
for (let i = 0; i < fnType.namedChildCount; i++) {
|
|
393
|
+
const c = fnType.namedChild(i);
|
|
394
|
+
if (c.type === 'formal_parameters') return c;
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
})();
|
|
398
|
+
let returnType = null;
|
|
399
|
+
// Return type is the last named child (predefined_type, type_identifier, etc.) that isn't formal_parameters
|
|
400
|
+
for (let i = fnType.namedChildCount - 1; i >= 0; i--) {
|
|
401
|
+
const c = fnType.namedChild(i);
|
|
402
|
+
if (c.type !== 'formal_parameters' && c.type !== 'type_parameters') {
|
|
403
|
+
returnType = c.text;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Build typed paramsStructured by reading param names + types out of fp.
|
|
408
|
+
// Pair against the existing inline params (from def.paramsStructured) so
|
|
409
|
+
// we preserve names declared at the arrow site if they differ.
|
|
410
|
+
let typedParams = [];
|
|
411
|
+
if (fp) {
|
|
412
|
+
for (let i = 0; i < fp.namedChildCount; i++) {
|
|
413
|
+
const param = fp.namedChild(i);
|
|
414
|
+
const info = {};
|
|
415
|
+
if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
|
|
416
|
+
const patternNode = param.childForFieldName('pattern');
|
|
417
|
+
const tnode = param.childForFieldName('type');
|
|
418
|
+
if (patternNode) info.name = patternNode.text;
|
|
419
|
+
if (tnode) info.type = tnode.text.replace(/^:\s*/, '');
|
|
420
|
+
if (param.type === 'optional_parameter') info.optional = true;
|
|
421
|
+
} else if (param.type === 'identifier') {
|
|
422
|
+
info.name = param.text;
|
|
423
|
+
}
|
|
424
|
+
if (info.name) typedParams.push(info);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// If inline params have names (from arrow), prefer those names but keep types from fnType
|
|
428
|
+
if (Array.isArray(ps) && ps.length === typedParams.length) {
|
|
429
|
+
typedParams = typedParams.map((tp, i) => ({
|
|
430
|
+
...ps[i], // start from existing (preserves rest, default, etc.)
|
|
431
|
+
...(tp.type ? { type: tp.type } : {}),
|
|
432
|
+
...(tp.optional ? { optional: true } : {}),
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
paramsStructured: typedParams.length ? typedParams : ps,
|
|
437
|
+
returnType: returnType || def.returnType || null,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* BUG-BX: A receiver like `Utils.helper()` may be a TS namespace member call
|
|
443
|
+
* for a regular (non-method) exported function. Returns true when the
|
|
444
|
+
* receiver matches a known namespace/class symbol that contains a function
|
|
445
|
+
* with the verified name.
|
|
446
|
+
* @param {object} index - ProjectIndex instance
|
|
447
|
+
* @param {string} receiver - Receiver text from the call site
|
|
448
|
+
* @param {string} funcName - Name being verified
|
|
449
|
+
* @param {string} defFile - The definition's file (to scope the match)
|
|
450
|
+
* @returns {boolean}
|
|
451
|
+
*/
|
|
452
|
+
function isNamespaceContainerFor(index, receiver, funcName, defFile) {
|
|
453
|
+
if (!receiver || !funcName) return false;
|
|
454
|
+
const candidates = index.symbols.get(receiver);
|
|
455
|
+
if (!candidates || candidates.length === 0) return false;
|
|
456
|
+
// Accept namespace, module, class, or interface containers
|
|
457
|
+
return candidates.some(c => {
|
|
458
|
+
const t = c.type;
|
|
459
|
+
if (t === 'namespace' || t === 'module' || t === 'class' || t === 'interface') {
|
|
460
|
+
// Same file as the def is the strongest signal; fall back to project-wide match.
|
|
461
|
+
if (!defFile || c.file === defFile) return true;
|
|
462
|
+
// Cross-file: only accept when receiver is a dedicated namespace/module
|
|
463
|
+
return t === 'namespace' || t === 'module';
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* BUG-BW: Build the list of call sites for `plan` using the SAME findCallers
|
|
471
|
+
* + className filter logic that verify uses. This guarantees plan and verify
|
|
472
|
+
* agree on which sites need updating — the previous implementation routed
|
|
473
|
+
* through `index.impact()` whose filter is stricter for unresolved receivers
|
|
474
|
+
* (e.g. `this.repo.save()`), causing plan to miss class-method call sites
|
|
475
|
+
* that verify finds.
|
|
476
|
+
*
|
|
477
|
+
* Returns an array of plan-shaped sites: { file, line, expression, args, argCount }.
|
|
478
|
+
*
|
|
479
|
+
* @param {object} index - ProjectIndex instance
|
|
480
|
+
* @param {string} name - Function name being refactored
|
|
481
|
+
* @param {object} def - Resolved definition
|
|
482
|
+
* @param {object} options - { file, className, line }
|
|
483
|
+
* @returns {Array}
|
|
484
|
+
*/
|
|
485
|
+
function computePlanCallSites(index, name, def, options) {
|
|
486
|
+
let callerResults = index.findCallers(name, {
|
|
487
|
+
includeMethods: true,
|
|
488
|
+
includeUncertain: false,
|
|
489
|
+
targetDefinitions: [def],
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Mirror verify's className filter (kept inline rather than re-extracted to
|
|
493
|
+
// avoid changing verify's behavior).
|
|
494
|
+
if (options.className && def.className) {
|
|
495
|
+
const targetClassName = def.className;
|
|
496
|
+
callerResults = callerResults.filter(c => {
|
|
497
|
+
if (!c.isMethod) return true;
|
|
498
|
+
const r = c.receiver;
|
|
499
|
+
if (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
500
|
+
if (r.toLowerCase().includes(targetClassName.toLowerCase())) return true;
|
|
501
|
+
// Local var type inference from constructor assignments
|
|
502
|
+
if (c.callerFile) {
|
|
503
|
+
const callerDef = c.callerStartLine ? { file: c.callerFile, startLine: c.callerStartLine, endLine: c.callerEndLine } : null;
|
|
504
|
+
if (callerDef) {
|
|
505
|
+
const callerCalls = index.getCachedCalls(c.callerFile);
|
|
506
|
+
if (callerCalls && Array.isArray(callerCalls)) {
|
|
507
|
+
const localTypes = new Map();
|
|
508
|
+
for (const call of callerCalls) {
|
|
509
|
+
if (call.line >= callerDef.startLine && call.line <= callerDef.endLine) {
|
|
510
|
+
if (!call.isMethod && !call.receiver) {
|
|
511
|
+
const syms = index.symbols.get(call.name);
|
|
512
|
+
if (syms && syms.some(s => s.type === 'class')) {
|
|
513
|
+
const content = index._readFile(c.callerFile);
|
|
514
|
+
const clines = content.split('\n');
|
|
515
|
+
const cline = clines[call.line - 1] || '';
|
|
516
|
+
const m = cline.match(/^\s*(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/);
|
|
517
|
+
if (m && m[2] === call.name) {
|
|
518
|
+
localTypes.set(m[1], call.name);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const receiverType = localTypes.get(r);
|
|
525
|
+
if (receiverType) return receiverType === targetClassName;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Param type annotations
|
|
530
|
+
if (c.callerFile && c.callerStartLine) {
|
|
531
|
+
const callerSymbol = index.findEnclosingFunction(c.callerFile, c.line, true);
|
|
532
|
+
if (callerSymbol && callerSymbol.paramsStructured) {
|
|
533
|
+
for (const param of callerSymbol.paramsStructured) {
|
|
534
|
+
if (param.name === r && param.type) {
|
|
535
|
+
const typeMatches = param.type.match(/\b([A-Za-z_]\w*)\b/g);
|
|
536
|
+
if (typeMatches && typeMatches.some(t => t === targetClassName)) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Unique method heuristic
|
|
545
|
+
const methodDefs = index.symbols.get(name);
|
|
546
|
+
if (methodDefs) {
|
|
547
|
+
const classNames = new Set();
|
|
548
|
+
for (const d of methodDefs) {
|
|
549
|
+
if (d.className) classNames.add(d.className);
|
|
550
|
+
}
|
|
551
|
+
if (classNames.size === 1 && classNames.has(targetClassName)) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Apply the same isMethodCall / non-method filter verify uses.
|
|
560
|
+
const defIsMethod = !!(def.isMethod || def.type === 'method' || def.className);
|
|
561
|
+
const targetBasename = path.basename(def.file, path.extname(def.file));
|
|
562
|
+
const defFileEntry = index.files.get(def.file);
|
|
563
|
+
const defLang = defFileEntry?.language;
|
|
564
|
+
|
|
565
|
+
const importNameCache = new Map();
|
|
566
|
+
function getImportedNames(filePath) {
|
|
567
|
+
if (importNameCache.has(filePath)) return importNameCache.get(filePath);
|
|
568
|
+
const names = new Set();
|
|
569
|
+
const fe = index.files.get(filePath);
|
|
570
|
+
if (!fe) { importNameCache.set(filePath, names); return names; }
|
|
571
|
+
try {
|
|
572
|
+
const content = index._readFile(filePath);
|
|
573
|
+
const { imports: rawImports, importAliases } = extractImports(content, fe.language);
|
|
574
|
+
for (const imp of rawImports) {
|
|
575
|
+
if (imp.names) for (const n of imp.names) names.add(n);
|
|
576
|
+
}
|
|
577
|
+
if (importAliases) for (const alias of importAliases) names.add(alias.local);
|
|
578
|
+
} catch (e) { /* skip */ }
|
|
579
|
+
importNameCache.set(filePath, names);
|
|
580
|
+
return names;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const sites = [];
|
|
584
|
+
for (const c of callerResults) {
|
|
585
|
+
const call = {
|
|
586
|
+
file: c.file,
|
|
587
|
+
relativePath: c.relativePath,
|
|
588
|
+
line: c.line,
|
|
589
|
+
content: c.content,
|
|
590
|
+
usageType: 'call',
|
|
591
|
+
receiver: c.receiver,
|
|
592
|
+
};
|
|
593
|
+
const analysis = analyzeCallSite(index, call, name);
|
|
594
|
+
|
|
595
|
+
if (analysis.isMethodCall && !defIsMethod) {
|
|
596
|
+
const callReceiver = call.receiver;
|
|
597
|
+
if (callReceiver && callReceiver === targetBasename) {
|
|
598
|
+
const importedNames = getImportedNames(call.file);
|
|
599
|
+
if (!importedNames.has(callReceiver)) continue;
|
|
600
|
+
} else if (callReceiver && langTraits(defLang)?.hasReceiverPackageCalls) {
|
|
601
|
+
const targetDir = path.basename(path.dirname(def.file));
|
|
602
|
+
if (callReceiver !== targetDir) continue;
|
|
603
|
+
} else if (callReceiver && isNamespaceContainerFor(index, callReceiver, name, def.file)) {
|
|
604
|
+
// BUG-BX: TS namespace-qualified call — accept.
|
|
605
|
+
} else {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
sites.push({
|
|
611
|
+
file: call.relativePath,
|
|
612
|
+
line: call.line,
|
|
613
|
+
expression: (call.content || '').trim(),
|
|
614
|
+
args: analysis.args,
|
|
615
|
+
argCount: analysis.argCount,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
clearTreeCache(index);
|
|
619
|
+
// Stable ordering (matches CLAUDE.md rule #11): files alphabetical, sites by line ascending.
|
|
620
|
+
sites.sort((a, b) => {
|
|
621
|
+
const fc = String(a.file).localeCompare(String(b.file));
|
|
622
|
+
if (fc !== 0) return fc;
|
|
623
|
+
return (a.line || 0) - (b.line || 0);
|
|
624
|
+
});
|
|
625
|
+
return sites;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Compute the same scopeWarning that impact() returns for plan output.
|
|
630
|
+
* @param {object} index - ProjectIndex instance
|
|
631
|
+
* @param {string} name - Function name
|
|
632
|
+
* @param {object} def - Resolved definition
|
|
633
|
+
* @param {object} options
|
|
634
|
+
* @returns {object|null}
|
|
635
|
+
*/
|
|
636
|
+
function computePlanScopeWarning(index, name, def, options) {
|
|
637
|
+
const defIsMethod = !!(def.isMethod || def.type === 'method' || def.className);
|
|
638
|
+
if (!defIsMethod) return null;
|
|
639
|
+
const allDefs = index.symbols.get(name);
|
|
640
|
+
if (!allDefs || allDefs.length <= 1) return null;
|
|
641
|
+
const classNames = [...new Set(allDefs
|
|
642
|
+
.filter(d => d.className && d.className !== def.className)
|
|
643
|
+
.map(d => d.className))];
|
|
644
|
+
if (classNames.length === 0) return null;
|
|
645
|
+
if (options.className || options.file) return null;
|
|
646
|
+
return {
|
|
647
|
+
targetClass: def.className || '(unknown)',
|
|
648
|
+
otherClasses: classNames,
|
|
649
|
+
hint: `Results may include calls to ${classNames.join(', ')}.${name}(). Use file= or className= to narrow scope.`
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
65
653
|
/**
|
|
66
654
|
* Analyze a call site to understand how it's being called (AST-based)
|
|
67
655
|
* @param {object} index - ProjectIndex instance
|
|
@@ -121,8 +709,17 @@ function analyzeCallSite(index, call, funcName) {
|
|
|
121
709
|
}
|
|
122
710
|
}
|
|
123
711
|
|
|
712
|
+
// Feature A/B: classify the call site by structural context.
|
|
713
|
+
// inLoop/inTry/inCallback come from walking up to the fn boundary.
|
|
714
|
+
// awaited comes from the immediate parent (await_expression).
|
|
715
|
+
// inTestCase is computed by the caller via the enclosing function's
|
|
716
|
+
// entry-point kind — analyzeCallSite doesn't have that info here, so
|
|
717
|
+
// it's left to be filled in by impact()/about() etc. that have
|
|
718
|
+
// access to the enclosing-function symbol.
|
|
719
|
+
const ctx = classifyCallContext(callNode, language);
|
|
720
|
+
|
|
124
721
|
const argsNode = callNode.childForFieldName('arguments');
|
|
125
|
-
if (!argsNode) return { args: [], argCount: 0, isMethodCall };
|
|
722
|
+
if (!argsNode) return { args: [], argCount: 0, isMethodCall, ...ctx };
|
|
126
723
|
|
|
127
724
|
const args = [];
|
|
128
725
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
@@ -134,13 +731,155 @@ function analyzeCallSite(index, call, funcName) {
|
|
|
134
731
|
argCount: args.length,
|
|
135
732
|
hasSpread: args.some(a => a.startsWith('...')),
|
|
136
733
|
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
137
|
-
isMethodCall
|
|
734
|
+
isMethodCall,
|
|
735
|
+
...ctx,
|
|
138
736
|
};
|
|
139
737
|
} catch (e) {
|
|
140
738
|
return { args: null, argCount: 0 };
|
|
141
739
|
}
|
|
142
740
|
}
|
|
143
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Argument shape analysis for a call site (used by `example --diverse`).
|
|
744
|
+
*
|
|
745
|
+
* Returns a per-arg list of AST node types ("string_literal", "number_literal",
|
|
746
|
+
* "identifier", "member_expression", "call_expression", "arrow_function",
|
|
747
|
+
* "object", "array", "spread", "other") derived directly from tree-sitter,
|
|
748
|
+
* plus a stable "shape key" that callers can use for clustering.
|
|
749
|
+
*
|
|
750
|
+
* Returns null when the call node can't be located (parse failure, file unreadable).
|
|
751
|
+
*
|
|
752
|
+
* @param {object} index - ProjectIndex instance
|
|
753
|
+
* @param {string} filePath - Absolute file path
|
|
754
|
+
* @param {number} lineNum - 1-indexed line of the call
|
|
755
|
+
* @param {string} funcName - Function name being called
|
|
756
|
+
* @returns {{argKinds: string[], argTexts: string[], argCount: number, shapeKey: string}|null}
|
|
757
|
+
*/
|
|
758
|
+
function analyzeCallShape(index, filePath, lineNum, funcName) {
|
|
759
|
+
try {
|
|
760
|
+
const language = detectLanguage(filePath);
|
|
761
|
+
if (!language) return null;
|
|
762
|
+
|
|
763
|
+
// Reuse tree cache to avoid re-parsing during a batch (clustering scans many sites)
|
|
764
|
+
let tree = index._treeCache?.get(filePath);
|
|
765
|
+
if (!tree) {
|
|
766
|
+
const content = index._readFile(filePath);
|
|
767
|
+
if (language === 'html') {
|
|
768
|
+
const htmlModule = getLanguageModule('html');
|
|
769
|
+
const htmlParser = getParser('html');
|
|
770
|
+
const jsParser = getParser('javascript');
|
|
771
|
+
if (!htmlParser || !jsParser) return null;
|
|
772
|
+
const blocks = htmlModule.extractScriptBlocks(content, htmlParser);
|
|
773
|
+
if (blocks.length === 0) return null;
|
|
774
|
+
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
775
|
+
tree = safeParse(jsParser, virtualJS);
|
|
776
|
+
} else {
|
|
777
|
+
const parser = getParser(language);
|
|
778
|
+
if (!parser) return null;
|
|
779
|
+
tree = safeParse(parser, content);
|
|
780
|
+
}
|
|
781
|
+
if (!tree) return null;
|
|
782
|
+
if (!index._treeCache) index._treeCache = new Map();
|
|
783
|
+
index._treeCache.set(filePath, tree);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const callTypes = new Set(['call_expression', 'call', 'method_invocation', 'object_creation_expression']);
|
|
787
|
+
const callNode = findCallNode(tree.rootNode, callTypes, lineNum - 1, funcName);
|
|
788
|
+
if (!callNode) return null;
|
|
789
|
+
|
|
790
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
791
|
+
if (!argsNode) {
|
|
792
|
+
return { argKinds: [], argTexts: [], argCount: 0, shapeKey: '0:' };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const argKinds = [];
|
|
796
|
+
const argTexts = [];
|
|
797
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
798
|
+
const argNode = argsNode.namedChild(i);
|
|
799
|
+
argKinds.push(classifyArgNode(argNode));
|
|
800
|
+
argTexts.push(argNode.text.trim());
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const shapeKey = `${argKinds.length}:${argKinds.join(',')}`;
|
|
804
|
+
return {
|
|
805
|
+
argKinds,
|
|
806
|
+
argTexts,
|
|
807
|
+
argCount: argKinds.length,
|
|
808
|
+
shapeKey,
|
|
809
|
+
};
|
|
810
|
+
} catch (e) {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Map a tree-sitter argument node to a coarse "kind" tag for shape clustering.
|
|
817
|
+
* The mapping is intentionally tight — a call passing `getUser()` should cluster
|
|
818
|
+
* with another call passing `loadConfig()` (both `call_expression`), but NOT
|
|
819
|
+
* with one passing `42` (a `number_literal`).
|
|
820
|
+
*
|
|
821
|
+
* Cross-language note: tree-sitter grammars use slightly different node names
|
|
822
|
+
* (`string_literal` vs `string`, `integer` vs `number_literal`). We canonicalize
|
|
823
|
+
* to a small set so a JS sample and a Python sample produce the same shape key.
|
|
824
|
+
*/
|
|
825
|
+
function classifyArgNode(node) {
|
|
826
|
+
if (!node) return 'other';
|
|
827
|
+
const t = node.type;
|
|
828
|
+
// Strings
|
|
829
|
+
if (t === 'string' || t === 'string_literal' || t === 'template_string' ||
|
|
830
|
+
t === 'raw_string_literal' || t === 'interpreted_string_literal') {
|
|
831
|
+
return 'string_literal';
|
|
832
|
+
}
|
|
833
|
+
// Numbers
|
|
834
|
+
if (t === 'number' || t === 'integer' || t === 'float' || t === 'number_literal' ||
|
|
835
|
+
t === 'integer_literal' || t === 'float_literal' || t === 'decimal_integer_literal' ||
|
|
836
|
+
t === 'hex_integer_literal' || t === 'real_literal') {
|
|
837
|
+
return 'number_literal';
|
|
838
|
+
}
|
|
839
|
+
// Booleans + null
|
|
840
|
+
if (t === 'true' || t === 'false' || t === 'null' || t === 'null_literal' ||
|
|
841
|
+
t === 'boolean_literal' || t === 'none' || t === 'nil') {
|
|
842
|
+
return 'literal';
|
|
843
|
+
}
|
|
844
|
+
// Identifiers (bare variable name)
|
|
845
|
+
if (t === 'identifier' || t === 'shorthand_property_identifier' ||
|
|
846
|
+
t === 'name' || t === 'simple_identifier' || t === 'type_identifier') {
|
|
847
|
+
return 'identifier';
|
|
848
|
+
}
|
|
849
|
+
// Member access: obj.attr / obj.method (no call)
|
|
850
|
+
if (t === 'member_expression' || t === 'attribute' || t === 'selector_expression' ||
|
|
851
|
+
t === 'field_expression' || t === 'field_access' || t === 'scoped_identifier') {
|
|
852
|
+
return 'member_expression';
|
|
853
|
+
}
|
|
854
|
+
// Nested calls: foo(getThing())
|
|
855
|
+
if (t === 'call_expression' || t === 'call' || t === 'method_invocation' ||
|
|
856
|
+
t === 'object_creation_expression' || t === 'macro_invocation') {
|
|
857
|
+
return 'call_expression';
|
|
858
|
+
}
|
|
859
|
+
// Anonymous functions
|
|
860
|
+
if (t === 'arrow_function' || t === 'function_expression' || t === 'function' ||
|
|
861
|
+
t === 'lambda' || t === 'closure_expression' || t === 'function_literal' ||
|
|
862
|
+
t === 'lambda_expression') {
|
|
863
|
+
return 'arrow_function';
|
|
864
|
+
}
|
|
865
|
+
// Object/struct literals
|
|
866
|
+
if (t === 'object' || t === 'object_expression' || t === 'dictionary' ||
|
|
867
|
+
t === 'struct_expression' || t === 'composite_literal') {
|
|
868
|
+
return 'object';
|
|
869
|
+
}
|
|
870
|
+
// Array/list literals
|
|
871
|
+
if (t === 'array' || t === 'array_expression' || t === 'list' || t === 'tuple' ||
|
|
872
|
+
t === 'array_literal') {
|
|
873
|
+
return 'array';
|
|
874
|
+
}
|
|
875
|
+
// Spread / unpacking
|
|
876
|
+
if (t === 'spread_element' || t === 'spread' || t === 'list_splat' ||
|
|
877
|
+
t === 'dictionary_splat') {
|
|
878
|
+
return 'spread';
|
|
879
|
+
}
|
|
880
|
+
return 'other';
|
|
881
|
+
}
|
|
882
|
+
|
|
144
883
|
/**
|
|
145
884
|
* Identify common calling patterns
|
|
146
885
|
* @param {Array} callSites - Array of call site objects
|
|
@@ -152,15 +891,25 @@ function identifyCallPatterns(callSites, funcName) {
|
|
|
152
891
|
constantArgs: 0, // Call sites with literal/constant arguments
|
|
153
892
|
variableArgs: 0, // Call sites passing variables
|
|
154
893
|
chainedCalls: 0, // Calls that are part of method chains
|
|
155
|
-
awaitedCalls: 0, // Async calls with await
|
|
156
|
-
spreadCalls: 0
|
|
894
|
+
awaitedCalls: 0, // Async calls with await (AST-derived from site.awaited)
|
|
895
|
+
spreadCalls: 0, // Calls using spread operator
|
|
896
|
+
// Feature A: structural classification counts.
|
|
897
|
+
inLoop: 0, // Call sites inside a loop construct
|
|
898
|
+
inTry: 0, // Call sites inside a try block
|
|
899
|
+
inCallback: 0, // Call sites inside a callback fn passed as an argument
|
|
900
|
+
inTestCase: 0 // Call sites whose enclosing function is a test entry
|
|
157
901
|
};
|
|
158
902
|
|
|
159
903
|
for (const site of callSites) {
|
|
160
904
|
const expr = site.expression;
|
|
161
905
|
|
|
162
906
|
if (site.hasSpread) patterns.spreadCalls++;
|
|
163
|
-
|
|
907
|
+
// Feature B: prefer the AST-derived `awaited` signal (set by
|
|
908
|
+
// analyzeCallSite's classifyCallContext walk). Fall back to a text
|
|
909
|
+
// check on the expression for callers that still pass legacy sites.
|
|
910
|
+
if (site.awaited === true || (site.awaited !== false && /\bawait\s/.test(expr))) {
|
|
911
|
+
patterns.awaitedCalls++;
|
|
912
|
+
}
|
|
164
913
|
if (new RegExp('\\.' + escapeRegExp(funcName) + '\\s*\\(').test(expr)) patterns.chainedCalls++;
|
|
165
914
|
|
|
166
915
|
if (site.args && site.args.length > 0) {
|
|
@@ -171,6 +920,14 @@ function identifyCallPatterns(callSites, funcName) {
|
|
|
171
920
|
if (hasLiteral) patterns.constantArgs++;
|
|
172
921
|
if (site.hasVariable) patterns.variableArgs++;
|
|
173
922
|
}
|
|
923
|
+
|
|
924
|
+
// Feature A counters — these flags are set on each site by
|
|
925
|
+
// analyzeCallSite (inLoop/inTry/inCallback) or by the caller after
|
|
926
|
+
// looking up the enclosing function (inTestCase).
|
|
927
|
+
if (site.inLoop) patterns.inLoop++;
|
|
928
|
+
if (site.inTry) patterns.inTry++;
|
|
929
|
+
if (site.inCallback) patterns.inCallback++;
|
|
930
|
+
if (site.inTestCase) patterns.inTestCase++;
|
|
174
931
|
}
|
|
175
932
|
|
|
176
933
|
return patterns;
|
|
@@ -186,7 +943,7 @@ function identifyCallPatterns(callSites, funcName) {
|
|
|
186
943
|
function verify(index, name, options = {}) {
|
|
187
944
|
index._beginOp();
|
|
188
945
|
try {
|
|
189
|
-
const { def } = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
946
|
+
const { def } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
190
947
|
if (!def) {
|
|
191
948
|
return { found: false, function: name };
|
|
192
949
|
}
|
|
@@ -194,7 +951,10 @@ function verify(index, name, options = {}) {
|
|
|
194
951
|
// (callers don't pass self/cls explicitly: obj.method(a, b) not obj.method(obj, a, b))
|
|
195
952
|
const fileEntry = index.files.get(def.file);
|
|
196
953
|
const lang = fileEntry?.language;
|
|
197
|
-
|
|
954
|
+
// BUG-BY: enrich types for arrow functions whose types live on the
|
|
955
|
+
// enclosing variable_declarator's type_annotation rather than inline.
|
|
956
|
+
const arrowTypes = extractArrowTypesFromVarDecl(index, def);
|
|
957
|
+
let params = (arrowTypes?.paramsStructured) || def.paramsStructured || [];
|
|
198
958
|
const selfParams = langTraits(lang)?.selfParam;
|
|
199
959
|
if (selfParams && params.length > 0 && selfParams.includes(params[0].name)) {
|
|
200
960
|
params = params.slice(1);
|
|
@@ -208,9 +968,18 @@ function verify(index, name, options = {}) {
|
|
|
208
968
|
|
|
209
969
|
// Get all call sites using findCallers for accurate resolution
|
|
210
970
|
// (usages-based approach misses calls when className is set or local names collide)
|
|
971
|
+
// BUG-H3: accept user-supplied flag. The default keeps the historical behavior:
|
|
972
|
+
// findCallers always returns method calls (so callers like obj.method() are seen),
|
|
973
|
+
// and the secondary filter below drops them when verifying a non-method def
|
|
974
|
+
// (e.g. dict.get() vs standalone get()). Passing --include-methods opts out of
|
|
975
|
+
// the secondary filter so all method-style calls are treated as candidates.
|
|
976
|
+
const verifyIncludeMethods = options.includeMethods === true;
|
|
977
|
+
const verifyIncludeUncertain = options.includeUncertain ?? false;
|
|
211
978
|
let callerResults = index.findCallers(name, {
|
|
979
|
+
// Always pass true to findCallers so method calls are visible — the secondary
|
|
980
|
+
// filter inside verify is the one that does the policy-driven filtering.
|
|
212
981
|
includeMethods: true,
|
|
213
|
-
includeUncertain:
|
|
982
|
+
includeUncertain: verifyIncludeUncertain,
|
|
214
983
|
targetDefinitions: [def],
|
|
215
984
|
});
|
|
216
985
|
|
|
@@ -285,7 +1054,8 @@ function verify(index, name, options = {}) {
|
|
|
285
1054
|
});
|
|
286
1055
|
}
|
|
287
1056
|
|
|
288
|
-
// Convert caller results to usage-like objects for analyzeCallSite
|
|
1057
|
+
// Convert caller results to usage-like objects for analyzeCallSite.
|
|
1058
|
+
// Carry callerFile/callerStartLine through so we can compute inTestCase.
|
|
289
1059
|
const calls = callerResults.map(c => ({
|
|
290
1060
|
file: c.file,
|
|
291
1061
|
relativePath: c.relativePath,
|
|
@@ -293,6 +1063,8 @@ function verify(index, name, options = {}) {
|
|
|
293
1063
|
content: c.content,
|
|
294
1064
|
usageType: 'call',
|
|
295
1065
|
receiver: c.receiver,
|
|
1066
|
+
callerFile: c.callerFile,
|
|
1067
|
+
callerStartLine: c.callerStartLine,
|
|
296
1068
|
}));
|
|
297
1069
|
|
|
298
1070
|
const valid = [];
|
|
@@ -330,6 +1102,18 @@ function verify(index, name, options = {}) {
|
|
|
330
1102
|
return names;
|
|
331
1103
|
}
|
|
332
1104
|
|
|
1105
|
+
// Helper: extract pattern flags (Feature A/B) from analyzeCallSite result.
|
|
1106
|
+
// Reused so each valid/mismatch/uncertain entry carries the same shape.
|
|
1107
|
+
function patternFlagsFrom(a) {
|
|
1108
|
+
return {
|
|
1109
|
+
inLoop: !!a.inLoop,
|
|
1110
|
+
inTry: !!a.inTry,
|
|
1111
|
+
inCallback: !!a.inCallback,
|
|
1112
|
+
awaited: !!a.awaited,
|
|
1113
|
+
// inTestCase filled in below via tagInTestCase
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
333
1117
|
for (const call of calls) {
|
|
334
1118
|
const analysis = analyzeCallSite(index, call, name);
|
|
335
1119
|
|
|
@@ -338,7 +1122,11 @@ function verify(index, name, options = {}) {
|
|
|
338
1122
|
// Allow module-level calls only when:
|
|
339
1123
|
// 1. Receiver matches target file's basename (e.g., jobs == jobs for jobs.py)
|
|
340
1124
|
// 2. Receiver is an imported name (not a local variable)
|
|
341
|
-
|
|
1125
|
+
// BUG-H3: when verifyIncludeMethods is true, keep method-style calls that
|
|
1126
|
+
// findCallers already accepted (binding/import resolution did the heavy lifting).
|
|
1127
|
+
// The receiver-based filtering below is a secondary safety net — skip it when
|
|
1128
|
+
// user explicitly opted into method calls.
|
|
1129
|
+
if (analysis.isMethodCall && !defIsMethod && !verifyIncludeMethods) {
|
|
342
1130
|
const callReceiver = call.receiver;
|
|
343
1131
|
if (callReceiver && callReceiver === targetBasename) {
|
|
344
1132
|
const importedNames = getImportedNames(call.file);
|
|
@@ -353,18 +1141,32 @@ function verify(index, name, options = {}) {
|
|
|
353
1141
|
continue;
|
|
354
1142
|
}
|
|
355
1143
|
// Receiver matches package directory — keep it
|
|
1144
|
+
} else if (callReceiver && isNamespaceContainerFor(index, callReceiver, name, def.file)) {
|
|
1145
|
+
// BUG-BX: TS namespace-qualified call (e.g. `Utils.helper()` where
|
|
1146
|
+
// `Utils` is a `namespace` symbol containing `helper`). Treat the
|
|
1147
|
+
// call as a direct invocation of the namespace member function.
|
|
1148
|
+
// Same handling for class static methods and module containers.
|
|
356
1149
|
} else {
|
|
357
1150
|
continue;
|
|
358
1151
|
}
|
|
359
1152
|
}
|
|
360
1153
|
|
|
1154
|
+
// Carry callerFile/callerStartLine so tagInTestCase can resolve the
|
|
1155
|
+
// enclosing function in a later pass.
|
|
1156
|
+
const carry = {
|
|
1157
|
+
callerFile: call.callerFile,
|
|
1158
|
+
callerStartLine: call.callerStartLine,
|
|
1159
|
+
};
|
|
1160
|
+
|
|
361
1161
|
if (analysis.args === null) {
|
|
362
1162
|
// Couldn't parse arguments
|
|
363
1163
|
uncertain.push({
|
|
364
1164
|
file: call.relativePath,
|
|
365
1165
|
line: call.line,
|
|
366
1166
|
expression: call.content.trim(),
|
|
367
|
-
reason: 'Could not parse call arguments'
|
|
1167
|
+
reason: 'Could not parse call arguments',
|
|
1168
|
+
patterns: patternFlagsFrom(analysis),
|
|
1169
|
+
...carry,
|
|
368
1170
|
});
|
|
369
1171
|
continue;
|
|
370
1172
|
}
|
|
@@ -375,7 +1177,9 @@ function verify(index, name, options = {}) {
|
|
|
375
1177
|
file: call.relativePath,
|
|
376
1178
|
line: call.line,
|
|
377
1179
|
expression: call.content.trim(),
|
|
378
|
-
reason: 'Uses spread operator'
|
|
1180
|
+
reason: 'Uses spread operator',
|
|
1181
|
+
patterns: patternFlagsFrom(analysis),
|
|
1182
|
+
...carry,
|
|
379
1183
|
});
|
|
380
1184
|
continue;
|
|
381
1185
|
}
|
|
@@ -386,7 +1190,12 @@ function verify(index, name, options = {}) {
|
|
|
386
1190
|
if (hasRest) {
|
|
387
1191
|
// With rest param, need at least minArgs
|
|
388
1192
|
if (argCount >= minArgs) {
|
|
389
|
-
valid.push({
|
|
1193
|
+
valid.push({
|
|
1194
|
+
file: call.relativePath,
|
|
1195
|
+
line: call.line,
|
|
1196
|
+
patterns: patternFlagsFrom(analysis),
|
|
1197
|
+
...carry,
|
|
1198
|
+
});
|
|
390
1199
|
} else {
|
|
391
1200
|
mismatches.push({
|
|
392
1201
|
file: call.relativePath,
|
|
@@ -394,13 +1203,20 @@ function verify(index, name, options = {}) {
|
|
|
394
1203
|
expression: call.content.trim(),
|
|
395
1204
|
expected: `at least ${minArgs} arg(s)`,
|
|
396
1205
|
actual: argCount,
|
|
397
|
-
args: analysis.args
|
|
1206
|
+
args: analysis.args,
|
|
1207
|
+
patterns: patternFlagsFrom(analysis),
|
|
1208
|
+
...carry,
|
|
398
1209
|
});
|
|
399
1210
|
}
|
|
400
1211
|
} else {
|
|
401
1212
|
// Without rest, need between minArgs and expectedParamCount
|
|
402
1213
|
if (argCount >= minArgs && argCount <= expectedParamCount) {
|
|
403
|
-
valid.push({
|
|
1214
|
+
valid.push({
|
|
1215
|
+
file: call.relativePath,
|
|
1216
|
+
line: call.line,
|
|
1217
|
+
patterns: patternFlagsFrom(analysis),
|
|
1218
|
+
...carry,
|
|
1219
|
+
});
|
|
404
1220
|
} else {
|
|
405
1221
|
mismatches.push({
|
|
406
1222
|
file: call.relativePath,
|
|
@@ -410,13 +1226,47 @@ function verify(index, name, options = {}) {
|
|
|
410
1226
|
? `${expectedParamCount} arg(s)`
|
|
411
1227
|
: `${minArgs}-${expectedParamCount} arg(s)`,
|
|
412
1228
|
actual: argCount,
|
|
413
|
-
args: analysis.args
|
|
1229
|
+
args: analysis.args,
|
|
1230
|
+
patterns: patternFlagsFrom(analysis),
|
|
1231
|
+
...carry,
|
|
414
1232
|
});
|
|
415
1233
|
}
|
|
416
1234
|
}
|
|
417
1235
|
}
|
|
418
1236
|
clearTreeCache(index);
|
|
419
1237
|
|
|
1238
|
+
// Feature A: tag each entry with `inTestCase` based on its enclosing function.
|
|
1239
|
+
// Done after the per-call loop because tagInTestCase prefers a single pass
|
|
1240
|
+
// through file metadata to avoid repeated lookups.
|
|
1241
|
+
{
|
|
1242
|
+
const { tagInTestCase } = require('./analysis');
|
|
1243
|
+
// Build a flat list of entries that need tagging — each carries
|
|
1244
|
+
// callerFile + callerStartLine + line. tagInTestCase mutates in place.
|
|
1245
|
+
const allSites = [...valid, ...mismatches, ...uncertain].map(s => ({
|
|
1246
|
+
...s,
|
|
1247
|
+
// Mirror inputs tagInTestCase expects
|
|
1248
|
+
line: s.line,
|
|
1249
|
+
callerFile: s.callerFile,
|
|
1250
|
+
callerStartLine: s.callerStartLine,
|
|
1251
|
+
}));
|
|
1252
|
+
// Use a parallel array so we can write back patterns.inTestCase.
|
|
1253
|
+
tagInTestCase(index, allSites);
|
|
1254
|
+
let i = 0;
|
|
1255
|
+
for (const s of valid) { s.patterns.inTestCase = !!allSites[i++].inTestCase; }
|
|
1256
|
+
for (const s of mismatches) { s.patterns.inTestCase = !!allSites[i++].inTestCase; }
|
|
1257
|
+
for (const s of uncertain) { s.patterns.inTestCase = !!allSites[i++].inTestCase; }
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Strip carry fields — they were internal scaffolding for tagInTestCase
|
|
1261
|
+
// and shouldn't appear in the public result.
|
|
1262
|
+
function strip(arr) {
|
|
1263
|
+
for (const s of arr) {
|
|
1264
|
+
delete s.callerFile;
|
|
1265
|
+
delete s.callerStartLine;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
strip(valid); strip(mismatches); strip(uncertain);
|
|
1269
|
+
|
|
420
1270
|
// Detect scope pollution for methods
|
|
421
1271
|
let scopeWarning = null;
|
|
422
1272
|
if (defIsMethod) {
|
|
@@ -435,12 +1285,35 @@ function verify(index, name, options = {}) {
|
|
|
435
1285
|
}
|
|
436
1286
|
}
|
|
437
1287
|
|
|
1288
|
+
// Feature A/B: build a top-level patterns aggregate across all call
|
|
1289
|
+
// sites verify saw (valid + mismatches + uncertain). Mirrors the shape
|
|
1290
|
+
// identifyCallPatterns returns in impact() so consumers can compare.
|
|
1291
|
+
const allSitesForAgg = [...valid, ...mismatches, ...uncertain].map(s => ({
|
|
1292
|
+
// identifyCallPatterns reads site.expression / site.args / site.hasSpread /
|
|
1293
|
+
// site.hasVariable and the boolean pattern flags.
|
|
1294
|
+
expression: s.expression || '',
|
|
1295
|
+
args: s.args || null,
|
|
1296
|
+
hasSpread: false, // already filtered out into uncertain
|
|
1297
|
+
hasVariable: false, // not propagated from analyzeCallSite here; harmless
|
|
1298
|
+
awaited: !!(s.patterns && s.patterns.awaited),
|
|
1299
|
+
inLoop: !!(s.patterns && s.patterns.inLoop),
|
|
1300
|
+
inTry: !!(s.patterns && s.patterns.inTry),
|
|
1301
|
+
inCallback: !!(s.patterns && s.patterns.inCallback),
|
|
1302
|
+
inTestCase: !!(s.patterns && s.patterns.inTestCase),
|
|
1303
|
+
}));
|
|
1304
|
+
const patternsAgg = identifyCallPatterns(allSitesForAgg, name);
|
|
1305
|
+
|
|
438
1306
|
return {
|
|
439
1307
|
found: true,
|
|
440
1308
|
function: name,
|
|
441
1309
|
file: def.relativePath,
|
|
442
1310
|
startLine: def.startLine,
|
|
443
|
-
|
|
1311
|
+
// BUG-BV: use local TS-correct param formatter (`opt?: number`, not `opt: number?`).
|
|
1312
|
+
// BUG-BY: when the def is a typed arrow declaration, render with enriched types.
|
|
1313
|
+
signature: formatTypedSignature(def, arrowTypes ? {
|
|
1314
|
+
paramsStructured: arrowTypes.paramsStructured,
|
|
1315
|
+
returnType: arrowTypes.returnType
|
|
1316
|
+
} : {}),
|
|
444
1317
|
params: params.map(p => ({
|
|
445
1318
|
name: p.name,
|
|
446
1319
|
optional: p.optional || p.default !== undefined,
|
|
@@ -451,8 +1324,10 @@ function verify(index, name, options = {}) {
|
|
|
451
1324
|
valid: valid.length,
|
|
452
1325
|
mismatches: mismatches.length,
|
|
453
1326
|
uncertain: uncertain.length,
|
|
1327
|
+
validDetails: valid,
|
|
454
1328
|
mismatchDetails: mismatches,
|
|
455
1329
|
uncertainDetails: uncertain,
|
|
1330
|
+
patterns: patternsAgg,
|
|
456
1331
|
scopeWarning
|
|
457
1332
|
};
|
|
458
1333
|
} finally { index._endOp(); }
|
|
@@ -473,11 +1348,40 @@ function plan(index, name, options = {}) {
|
|
|
473
1348
|
return { found: false, function: name };
|
|
474
1349
|
}
|
|
475
1350
|
|
|
476
|
-
const resolved = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
1351
|
+
const resolved = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
477
1352
|
const def = resolved.def || definitions[0];
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
const
|
|
1353
|
+
// BUG-BY: enrich types for typed-arrow-fn declarations.
|
|
1354
|
+
const arrowTypes = extractArrowTypesFromVarDecl(index, def);
|
|
1355
|
+
const currentParams = (arrowTypes?.paramsStructured) || def.paramsStructured || [];
|
|
1356
|
+
// BUG-BV: render with TS-correct param formatting (`opt?: number`).
|
|
1357
|
+
const currentSignature = formatTypedSignature(def, arrowTypes ? {
|
|
1358
|
+
paramsStructured: arrowTypes.paramsStructured,
|
|
1359
|
+
returnType: arrowTypes.returnType
|
|
1360
|
+
} : {});
|
|
1361
|
+
|
|
1362
|
+
// BUG-BW: plan must discover call sites the same way verify does for class
|
|
1363
|
+
// methods. Previously plan relied on `index.impact()` whose filter rejected
|
|
1364
|
+
// calls with unresolved receivers (e.g. `this.field.method()`), even when
|
|
1365
|
+
// verify's filter accepts them. Compute call sites locally to keep plan
|
|
1366
|
+
// and verify in lock-step.
|
|
1367
|
+
const planCallSites = computePlanCallSites(index, name, def, options);
|
|
1368
|
+
const impactScopeWarning = computePlanScopeWarning(index, name, def, options);
|
|
1369
|
+
|
|
1370
|
+
// Reject ambiguous multi-op invocations rather than silently coalescing.
|
|
1371
|
+
// The previous behavior reported only the *last* operation in the
|
|
1372
|
+
// headline, which made plan output untrustworthy for multi-op refactors.
|
|
1373
|
+
const requestedOps = [
|
|
1374
|
+
options.addParam ? 'addParam' : null,
|
|
1375
|
+
options.removeParam ? 'removeParam' : null,
|
|
1376
|
+
options.renameTo ? 'renameTo' : null,
|
|
1377
|
+
].filter(Boolean);
|
|
1378
|
+
if (requestedOps.length > 1) {
|
|
1379
|
+
return {
|
|
1380
|
+
found: true,
|
|
1381
|
+
function: name,
|
|
1382
|
+
error: `plan accepts one operation at a time; got ${requestedOps.length}: ${requestedOps.join(', ')}. Run separately and compose results.`,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
481
1385
|
|
|
482
1386
|
let newParams = [...currentParams];
|
|
483
1387
|
let newSignature = currentSignature;
|
|
@@ -522,32 +1426,28 @@ function plan(index, name, options = {}) {
|
|
|
522
1426
|
}
|
|
523
1427
|
}
|
|
524
1428
|
|
|
525
|
-
// Generate new signature
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
newSignature = `${asyncPrefix}${name}(${paramsList})`;
|
|
535
|
-
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
1429
|
+
// Generate new signature with TS-correct optional marker (BUG-BV)
|
|
1430
|
+
// and arrow-fn enriched return type (BUG-BY).
|
|
1431
|
+
// BUG-5: preserve all modifier tokens (async/static/public/...).
|
|
1432
|
+
const paramsList = newParams.map(formatTypedParam).filter(Boolean).join(', ');
|
|
1433
|
+
const modTokens = computeModifierTokens(def);
|
|
1434
|
+
const modPrefix = modTokens.length ? modTokens.join(' ') + ' ' : '';
|
|
1435
|
+
newSignature = `${modPrefix}${name}(${paramsList})`;
|
|
1436
|
+
const newRet = arrowTypes?.returnType || def.returnType;
|
|
1437
|
+
if (newRet) newSignature += `: ${newRet}`;
|
|
536
1438
|
|
|
537
1439
|
// Describe changes needed at each call site
|
|
538
|
-
for (const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
}
|
|
1440
|
+
for (const site of planCallSites) {
|
|
1441
|
+
const suggestion = options.defaultValue
|
|
1442
|
+
? `No change needed (has default value)`
|
|
1443
|
+
: `Add argument: ${options.addParam}`;
|
|
1444
|
+
changes.push({
|
|
1445
|
+
file: site.file,
|
|
1446
|
+
line: site.line,
|
|
1447
|
+
expression: site.expression,
|
|
1448
|
+
suggestion,
|
|
1449
|
+
args: site.args
|
|
1450
|
+
});
|
|
551
1451
|
}
|
|
552
1452
|
}
|
|
553
1453
|
|
|
@@ -570,17 +1470,15 @@ function plan(index, name, options = {}) {
|
|
|
570
1470
|
|
|
571
1471
|
newParams = currentParams.filter(p => p.name !== removeTarget);
|
|
572
1472
|
|
|
573
|
-
// Generate new signature
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
newSignature = `${asyncPrefix}${name}(${paramsList})`;
|
|
583
|
-
if (def.returnType) newSignature += `: ${def.returnType}`;
|
|
1473
|
+
// Generate new signature with TS-correct optional marker (BUG-BV)
|
|
1474
|
+
// and arrow-fn enriched return type (BUG-BY).
|
|
1475
|
+
// BUG-5: preserve all modifier tokens (async/static/public/...).
|
|
1476
|
+
const paramsList = newParams.map(formatTypedParam).filter(Boolean).join(', ');
|
|
1477
|
+
const modTokens = computeModifierTokens(def);
|
|
1478
|
+
const modPrefix = modTokens.length ? modTokens.join(' ') + ' ' : '';
|
|
1479
|
+
newSignature = `${modPrefix}${name}(${paramsList})`;
|
|
1480
|
+
const newRet = arrowTypes?.returnType || def.returnType;
|
|
1481
|
+
if (newRet) newSignature += `: ${newRet}`;
|
|
584
1482
|
|
|
585
1483
|
// For Python/Rust methods, self/cls/&self/&mut self is in paramsStructured
|
|
586
1484
|
// but callers don't pass it. Adjust paramIndex to caller-side position.
|
|
@@ -594,17 +1492,15 @@ function plan(index, name, options = {}) {
|
|
|
594
1492
|
const callerArgIndex = paramIndex - selfOffset;
|
|
595
1493
|
|
|
596
1494
|
// Describe changes at each call site
|
|
597
|
-
for (const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
});
|
|
607
|
-
}
|
|
1495
|
+
for (const site of planCallSites) {
|
|
1496
|
+
if (site.args && site.argCount > callerArgIndex) {
|
|
1497
|
+
changes.push({
|
|
1498
|
+
file: site.file,
|
|
1499
|
+
line: site.line,
|
|
1500
|
+
expression: site.expression,
|
|
1501
|
+
suggestion: `Remove argument ${callerArgIndex + 1}: ${site.args[callerArgIndex] || '?'}`,
|
|
1502
|
+
args: site.args
|
|
1503
|
+
});
|
|
608
1504
|
}
|
|
609
1505
|
}
|
|
610
1506
|
}
|
|
@@ -614,20 +1510,18 @@ function plan(index, name, options = {}) {
|
|
|
614
1510
|
newSignature = currentSignature.replace(new RegExp('\\b' + escapeRegExp(name) + '\\b'), options.renameTo);
|
|
615
1511
|
|
|
616
1512
|
// All call sites need renaming
|
|
617
|
-
for (const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
});
|
|
630
|
-
}
|
|
1513
|
+
for (const site of planCallSites) {
|
|
1514
|
+
const newExpression = site.expression.replace(
|
|
1515
|
+
new RegExp('\\b' + escapeRegExp(name) + '\\b'),
|
|
1516
|
+
options.renameTo
|
|
1517
|
+
);
|
|
1518
|
+
changes.push({
|
|
1519
|
+
file: site.file,
|
|
1520
|
+
line: site.line,
|
|
1521
|
+
expression: site.expression,
|
|
1522
|
+
suggestion: `Rename to: ${newExpression}`,
|
|
1523
|
+
newExpression
|
|
1524
|
+
});
|
|
631
1525
|
}
|
|
632
1526
|
|
|
633
1527
|
// Also include import statements that reference the renamed function
|
|
@@ -662,26 +1556,19 @@ function plan(index, name, options = {}) {
|
|
|
662
1556
|
operation,
|
|
663
1557
|
before: {
|
|
664
1558
|
signature: currentSignature,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
return n;
|
|
670
|
-
})
|
|
1559
|
+
// BUG-BV: TS-correct optional marker (`opt?: number`); test contract
|
|
1560
|
+
// expects name-keyed array entries (no ` = default`, no rest prefix)
|
|
1561
|
+
// so callers can `.includes('paramName')` for exact match.
|
|
1562
|
+
params: currentParams.map(p => formatPlanParamName(p)).filter(Boolean)
|
|
671
1563
|
},
|
|
672
1564
|
after: {
|
|
673
1565
|
signature: newSignature,
|
|
674
|
-
params: newParams.map(p =>
|
|
675
|
-
let n = p.name;
|
|
676
|
-
if (p.optional && !p.default) n += '?';
|
|
677
|
-
if (p.type) n += `: ${p.type}`;
|
|
678
|
-
return n;
|
|
679
|
-
})
|
|
1566
|
+
params: newParams.map(p => formatPlanParamName(p)).filter(Boolean)
|
|
680
1567
|
},
|
|
681
1568
|
totalChanges: changes.length,
|
|
682
1569
|
filesAffected: new Set(changes.map(c => c.file)).size,
|
|
683
1570
|
changes,
|
|
684
|
-
scopeWarning:
|
|
1571
|
+
scopeWarning: impactScopeWarning
|
|
685
1572
|
};
|
|
686
1573
|
} finally { index._endOp(); }
|
|
687
1574
|
}
|
|
@@ -760,4 +1647,4 @@ function analyzeCallSiteAST(index, filePath, lineNum, funcName) {
|
|
|
760
1647
|
return result;
|
|
761
1648
|
}
|
|
762
1649
|
|
|
763
|
-
module.exports = { verify, plan, analyzeCallSite, analyzeCallSiteAST, findCallNode, clearTreeCache, identifyCallPatterns };
|
|
1650
|
+
module.exports = { verify, plan, analyzeCallSite, analyzeCallSiteAST, analyzeCallShape, classifyArgNode, findCallNode, clearTreeCache, identifyCallPatterns };
|