ucn 4.0.2 → 4.1.0
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 +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +4 -1
package/core/verify.js
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits } = require('../languages');
|
|
10
|
-
const { escapeRegExp } = require('./shared');
|
|
11
|
-
const { extractImports } = require('./imports');
|
|
10
|
+
const { escapeRegExp, codeUnitCompare } = require('./shared');
|
|
12
11
|
|
|
13
12
|
// ============================================================================
|
|
14
13
|
// CALL-SITE CLASSIFICATION (Feature A)
|
|
@@ -141,9 +140,21 @@ function classifyCallContext(callNode, language) {
|
|
|
141
140
|
/**
|
|
142
141
|
* Find a call expression node at the target line matching funcName
|
|
143
142
|
*/
|
|
144
|
-
function findCallNode(node, callTypes, targetRow, funcName) {
|
|
143
|
+
function findCallNode(node, callTypes, targetRow, funcName, occurrence = 0) {
|
|
144
|
+
// Several same-name calls can share one line (`greet("a") + greet("b")`,
|
|
145
|
+
// f-strings) — fix #231: callers pass the site's per-line ordinal so each
|
|
146
|
+
// record is arg-checked against ITS OWN node, not the line's first.
|
|
147
|
+
// Records and this walk are both pre-order, so ordinals align; an
|
|
148
|
+
// out-of-range ordinal falls back to the first match (never worse than
|
|
149
|
+
// the pre-fix behavior when a parse shape hides a node).
|
|
150
|
+
const matches = _collectCallNodes(node, callTypes, targetRow, funcName, occurrence + 1);
|
|
151
|
+
return matches[occurrence] || matches[0] || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _collectCallNodes(node, callTypes, targetRow, funcName, limit, out = []) {
|
|
155
|
+
if (out.length >= limit) return out;
|
|
145
156
|
if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) {
|
|
146
|
-
return
|
|
157
|
+
return out; // Skip nodes that don't contain the target line
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
if (callTypes.has(node.type) && node.startPosition.row <= targetRow && node.endPosition.row >= targetRow) {
|
|
@@ -153,7 +164,16 @@ function findCallNode(node, callTypes, targetRow, funcName) {
|
|
|
153
164
|
if (typeNode) {
|
|
154
165
|
// Strip generics and package qualifiers: com.foo.Bar<T> -> Bar
|
|
155
166
|
const typeName = typeNode.text.replace(/<.*>$/, '').split('.').pop();
|
|
156
|
-
if (typeName === funcName)
|
|
167
|
+
if (typeName === funcName) out.push(node);
|
|
168
|
+
}
|
|
169
|
+
} else if (node.type === 'new_expression') {
|
|
170
|
+
// JS/TS constructor: new ClassName(args) — class is in 'constructor'
|
|
171
|
+
// field (fix #230: these sites used to fall out as "Could not
|
|
172
|
+
// parse call arguments" and every class verify went uncertain).
|
|
173
|
+
const ctorNode = node.childForFieldName('constructor');
|
|
174
|
+
if (ctorNode) {
|
|
175
|
+
const typeName = ctorNode.text.replace(/<.*>$/, '').split('.').pop();
|
|
176
|
+
if (typeName === funcName) out.push(node);
|
|
157
177
|
}
|
|
158
178
|
} else {
|
|
159
179
|
// Check if this call is for our target function
|
|
@@ -169,17 +189,19 @@ function findCallNode(node, callTypes, targetRow, funcName) {
|
|
|
169
189
|
: funcNode.type === 'scoped_identifier'
|
|
170
190
|
? (funcNode.childForFieldName('name') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
171
191
|
: funcNode.text;
|
|
172
|
-
if (funcText === funcName)
|
|
192
|
+
if (funcText === funcName) out.push(node);
|
|
173
193
|
}
|
|
174
194
|
}
|
|
195
|
+
if (out.length >= limit) return out;
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
// Recurse into children
|
|
198
|
+
// Recurse into children — nested same-name calls (`greet(greet(x))`)
|
|
199
|
+
// are separate records, so a match's children are still scanned.
|
|
178
200
|
for (let i = 0; i < node.childCount; i++) {
|
|
179
|
-
|
|
180
|
-
if (
|
|
201
|
+
_collectCallNodes(node.child(i), callTypes, targetRow, funcName, limit, out);
|
|
202
|
+
if (out.length >= limit) return out;
|
|
181
203
|
}
|
|
182
|
-
return
|
|
204
|
+
return out;
|
|
183
205
|
}
|
|
184
206
|
|
|
185
207
|
/**
|
|
@@ -439,149 +461,184 @@ function extractArrowTypesFromVarDecl(index, def) {
|
|
|
439
461
|
}
|
|
440
462
|
|
|
441
463
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
464
|
+
* Constructor parameter lists for a CLASS verify/plan target (fix #230): a
|
|
465
|
+
* class def carries no paramsStructured, so `verify Task` used to arg-check
|
|
466
|
+
* `new Task(id, name)` against 0..0 — a false red on every parameterized
|
|
467
|
+
* constructor, in every language. Sources: indexed constructor members
|
|
468
|
+
* (JS/TS `constructor`, Python `__init__` — emitted with type
|
|
469
|
+
* 'constructor'), or a Java AST walk (constructors are deliberately not
|
|
470
|
+
* indexed as members there). Returns an array of paramsStructured lists —
|
|
471
|
+
* one per constructor overload — or null when the class declares none.
|
|
446
472
|
* @param {object} index - ProjectIndex instance
|
|
447
|
-
* @param {
|
|
448
|
-
* @param {string}
|
|
449
|
-
* @
|
|
450
|
-
* @returns {boolean}
|
|
473
|
+
* @param {object} def - Resolved definition (any type; non-class returns null)
|
|
474
|
+
* @param {string} lang - The definition file's language
|
|
475
|
+
* @returns {Array<Array<object>>|null}
|
|
451
476
|
*/
|
|
452
|
-
function
|
|
453
|
-
if (!
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
// Cross-file: only accept when receiver is a dedicated namespace/module
|
|
463
|
-
return t === 'namespace' || t === 'module';
|
|
477
|
+
function _constructorParamLists(index, def, lang) {
|
|
478
|
+
if (!def || !def.file || !['class', 'enum', 'record'].includes(def.type)) return null;
|
|
479
|
+
const lists = [];
|
|
480
|
+
const endLine = def.endLine != null ? def.endLine : Infinity;
|
|
481
|
+
const inRange = (d) => d.file === def.file &&
|
|
482
|
+
d.startLine >= def.startLine && d.startLine <= endLine &&
|
|
483
|
+
Array.isArray(d.paramsStructured);
|
|
484
|
+
for (const ctorName of ['constructor', '__init__']) {
|
|
485
|
+
for (const d of (index.symbols.get(ctorName) || [])) {
|
|
486
|
+
if (d.className === def.name && inRange(d)) lists.push(d.paramsStructured);
|
|
464
487
|
}
|
|
465
|
-
|
|
466
|
-
|
|
488
|
+
}
|
|
489
|
+
// Java constructor members are named after the CLASS (enum-body
|
|
490
|
+
// constructors carry paramsStructured since fix #230).
|
|
491
|
+
for (const d of (index.symbols.get(def.name) || [])) {
|
|
492
|
+
if (d.type === 'constructor' && d.className === def.name && inRange(d)) {
|
|
493
|
+
lists.push(d.paramsStructured);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (lists.length > 0) return lists;
|
|
497
|
+
if (lang !== 'java') return null;
|
|
498
|
+
let parser, content;
|
|
499
|
+
try {
|
|
500
|
+
parser = getParser('java');
|
|
501
|
+
content = index._readFile(def.file);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
if (!parser || content == null) return null;
|
|
506
|
+
const tree = safeParse(parser, content);
|
|
507
|
+
if (!tree) return null;
|
|
508
|
+
const { parseStructuredParams } = require('../languages/utils');
|
|
509
|
+
const targetRow = def.startLine - 1;
|
|
510
|
+
let classNode = null;
|
|
511
|
+
(function findClass(node) {
|
|
512
|
+
if (classNode || !node) return;
|
|
513
|
+
if ((node.type === 'class_declaration' || node.type === 'enum_declaration' ||
|
|
514
|
+
node.type === 'record_declaration') &&
|
|
515
|
+
node.startPosition.row <= targetRow && node.endPosition.row >= targetRow) {
|
|
516
|
+
const nameNode = node.childForFieldName('name');
|
|
517
|
+
if (nameNode && nameNode.text === def.name) {
|
|
518
|
+
classNode = node;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (let i = 0; i < node.namedChildCount; i++) findClass(node.namedChild(i));
|
|
523
|
+
})(tree.rootNode);
|
|
524
|
+
if (!classNode) return null;
|
|
525
|
+
// Records declare their canonical constructor's params on the header.
|
|
526
|
+
if (classNode.type === 'record_declaration') {
|
|
527
|
+
const recParams = classNode.childForFieldName('parameters');
|
|
528
|
+
if (recParams) lists.push(parseStructuredParams(recParams, 'java') || []);
|
|
529
|
+
}
|
|
530
|
+
const collectCtors = (body) => {
|
|
531
|
+
if (!body) return;
|
|
532
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
533
|
+
const child = body.namedChild(i);
|
|
534
|
+
if (child.type === 'constructor_declaration') {
|
|
535
|
+
const paramsNode = child.childForFieldName('parameters');
|
|
536
|
+
lists.push(parseStructuredParams(paramsNode, 'java') || []);
|
|
537
|
+
} else if (child.type === 'enum_body_declarations') {
|
|
538
|
+
collectCtors(child);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
collectCtors(classNode.childForFieldName('body'));
|
|
543
|
+
return lists.length > 0 ? lists : null;
|
|
467
544
|
}
|
|
468
545
|
|
|
469
546
|
/**
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
547
|
+
* v4 tiered caller sweep shared by verify and plan (BUG-BW lockstep): run
|
|
548
|
+
* findCallers in collectAccount mode and partition candidates into the
|
|
549
|
+
* confirmed band (arg-checked / planned) and the VISIBLE unverified band
|
|
550
|
+
* (rendered with reasons, never silently dropped). The pre-v4 className and
|
|
551
|
+
* receiver heuristics are gone — engine receiver physics decide tier and
|
|
552
|
+
* exclusion, and their fallback branches could silently drop true callers.
|
|
553
|
+
* Namespace-container receivers (BUG-BX `Utils.helper()`) confirm in the
|
|
554
|
+
* ENGINE since fix #254 (range-based containment + scope evidence), so no
|
|
555
|
+
* verify-local promotion remains — the bands are the sweep's verbatim.
|
|
478
556
|
*
|
|
479
557
|
* @param {object} index - ProjectIndex instance
|
|
480
|
-
* @param {string} name -
|
|
481
|
-
* @param {object} def - Resolved definition
|
|
482
|
-
* @
|
|
483
|
-
* @returns {Array}
|
|
558
|
+
* @param {string} name - Symbol name
|
|
559
|
+
* @param {object} def - Resolved definition (pinned target)
|
|
560
|
+
* @returns {{ confirmed: Array, unverified: Array, account: object }}
|
|
484
561
|
*/
|
|
485
|
-
function
|
|
486
|
-
|
|
562
|
+
function contractedCallerSweep(index, name, def) {
|
|
563
|
+
const rawCallers = index.findCallers(name, {
|
|
487
564
|
includeMethods: true,
|
|
488
|
-
includeUncertain: false,
|
|
489
565
|
targetDefinitions: [def],
|
|
566
|
+
collectAccount: true,
|
|
490
567
|
});
|
|
491
568
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
});
|
|
569
|
+
const confirmed = [];
|
|
570
|
+
const unverified = [];
|
|
571
|
+
for (const c of rawCallers) {
|
|
572
|
+
if (c.tier !== 'unverified') confirmed.push(c);
|
|
573
|
+
else unverified.push(c);
|
|
574
|
+
}
|
|
575
|
+
for (const u of rawCallers.unverifiedEntries || []) {
|
|
576
|
+
unverified.push(u);
|
|
557
577
|
}
|
|
558
578
|
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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;
|
|
579
|
+
// Conservation account from the sweep's claims (impact's manual
|
|
580
|
+
// composition).
|
|
581
|
+
const { computeGroundSet, buildAccount } = require('./account');
|
|
582
|
+
const groundSet = computeGroundSet(index, name);
|
|
583
|
+
const accountRaw = rawCallers.accountRaw || { unverifiedLines: [], excludedEntries: [] };
|
|
584
|
+
const confirmedEntries = confirmed.map(c => ({ file: c.file, line: c.line }));
|
|
585
|
+
const unverifiedEntries = [
|
|
586
|
+
...accountRaw.unverifiedLines,
|
|
587
|
+
...unverified.map(u => ({ file: u.file, line: u.line })),
|
|
588
|
+
];
|
|
589
|
+
for (const s of rawCallers.shadowEntries || []) {
|
|
590
|
+
(s.tier === 'unverified' ? unverifiedEntries : confirmedEntries).push({ file: s.file, line: s.line });
|
|
581
591
|
}
|
|
592
|
+
const account = buildAccount(index, name, {
|
|
593
|
+
groundSet,
|
|
594
|
+
confirmedEntries,
|
|
595
|
+
unverifiedEntries,
|
|
596
|
+
excludedEntries: accountRaw.excludedEntries,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Ground call-lines no engine candidate claimed: visible one-liners
|
|
600
|
+
// (already counted unverified in the account arithmetic).
|
|
601
|
+
const { callNotResolvedEntries } = require('./analysis');
|
|
602
|
+
for (const e of callNotResolvedEntries(index, account)) unverified.push(e);
|
|
603
|
+
unverified.sort((a, b) => {
|
|
604
|
+
const ap = a.relativePath || '';
|
|
605
|
+
const bp = b.relativePath || '';
|
|
606
|
+
if (ap !== bp) return codeUnitCompare(ap, bp);
|
|
607
|
+
return (a.line || 0) - (b.line || 0);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return { confirmed, unverified, account };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Map an unverified sweep entry to the public site shape (relative `file`). */
|
|
614
|
+
function unverifiedSiteShape(u) {
|
|
615
|
+
return {
|
|
616
|
+
file: u.relativePath,
|
|
617
|
+
line: u.line,
|
|
618
|
+
expression: (u.content || '').trim(),
|
|
619
|
+
callerName: u.callerName ?? null,
|
|
620
|
+
tier: 'unverified',
|
|
621
|
+
...(u.reason && { reason: u.reason }),
|
|
622
|
+
...(u.dispatchVia && { dispatchVia: u.dispatchVia }),
|
|
623
|
+
...(u.dispatchCandidates != null && { dispatchCandidates: u.dispatchCandidates }),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* BUG-BW: Build the list of call sites for `plan` using the SAME sweep verify
|
|
629
|
+
* uses. This guarantees plan and verify agree on which sites need updating.
|
|
630
|
+
*
|
|
631
|
+
* @param {object} index - ProjectIndex instance
|
|
632
|
+
* @param {string} name - Function name being refactored
|
|
633
|
+
* @param {object} def - Resolved definition
|
|
634
|
+
* @returns {{ sites: Array, unverifiedSites: Array, account: object }}
|
|
635
|
+
*/
|
|
636
|
+
function computePlanCallSites(index, name, def) {
|
|
637
|
+
const { confirmed, unverified, account } = contractedCallerSweep(index, name, def);
|
|
582
638
|
|
|
583
639
|
const sites = [];
|
|
584
|
-
|
|
640
|
+
const planLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
641
|
+
for (const c of confirmed) {
|
|
585
642
|
const call = {
|
|
586
643
|
file: c.file,
|
|
587
644
|
relativePath: c.relativePath,
|
|
@@ -590,23 +647,10 @@ function computePlanCallSites(index, name, def, options) {
|
|
|
590
647
|
usageType: 'call',
|
|
591
648
|
receiver: c.receiver,
|
|
592
649
|
};
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
650
|
+
const siteKey = `${c.file}:${c.line}`;
|
|
651
|
+
const occurrence = planLineSeen.get(siteKey) || 0;
|
|
652
|
+
planLineSeen.set(siteKey, occurrence + 1);
|
|
653
|
+
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
610
654
|
sites.push({
|
|
611
655
|
file: call.relativePath,
|
|
612
656
|
line: call.line,
|
|
@@ -618,11 +662,11 @@ function computePlanCallSites(index, name, def, options) {
|
|
|
618
662
|
clearTreeCache(index);
|
|
619
663
|
// Stable ordering (matches CLAUDE.md rule #11): files alphabetical, sites by line ascending.
|
|
620
664
|
sites.sort((a, b) => {
|
|
621
|
-
const fc = String(a.file)
|
|
665
|
+
const fc = codeUnitCompare(String(a.file), String(b.file));
|
|
622
666
|
if (fc !== 0) return fc;
|
|
623
667
|
return (a.line || 0) - (b.line || 0);
|
|
624
668
|
});
|
|
625
|
-
return sites;
|
|
669
|
+
return { sites, unverifiedSites: unverified.map(unverifiedSiteShape), account };
|
|
626
670
|
}
|
|
627
671
|
|
|
628
672
|
/**
|
|
@@ -657,7 +701,7 @@ function computePlanScopeWarning(index, name, def, options) {
|
|
|
657
701
|
* @param {string} funcName - Function name to find
|
|
658
702
|
* @returns {object} { args, argCount, hasSpread, hasVariable }
|
|
659
703
|
*/
|
|
660
|
-
function analyzeCallSite(index, call, funcName) {
|
|
704
|
+
function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
661
705
|
try {
|
|
662
706
|
const language = detectLanguage(call.file);
|
|
663
707
|
if (!language) return { args: null, argCount: 0 };
|
|
@@ -687,11 +731,12 @@ function analyzeCallSite(index, call, funcName) {
|
|
|
687
731
|
}
|
|
688
732
|
|
|
689
733
|
// Call node types vary by language
|
|
690
|
-
const callTypes = new Set(['call_expression', 'call', 'method_invocation',
|
|
734
|
+
const callTypes = new Set(['call_expression', 'call', 'method_invocation',
|
|
735
|
+
'object_creation_expression', 'new_expression']);
|
|
691
736
|
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
692
737
|
|
|
693
738
|
// Find the call expression at the target line matching funcName
|
|
694
|
-
const callNode = findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
739
|
+
const callNode = findCallNode(tree.rootNode, callTypes, targetRow, funcName, occurrence);
|
|
695
740
|
if (!callNode) return { args: null, argCount: 0 };
|
|
696
741
|
|
|
697
742
|
// Check if this is a method call (obj.func()) vs a direct call (func())
|
|
@@ -954,105 +999,37 @@ function verify(index, name, options = {}) {
|
|
|
954
999
|
// BUG-BY: enrich types for arrow functions whose types live on the
|
|
955
1000
|
// enclosing variable_declarator's type_annotation rather than inline.
|
|
956
1001
|
const arrowTypes = extractArrowTypesFromVarDecl(index, def);
|
|
957
|
-
|
|
1002
|
+
// Class target: arg-check against CONSTRUCTOR parameters (fix #230).
|
|
1003
|
+
// Multiple lists = constructor overloads (Java): a call is valid when it
|
|
1004
|
+
// fits the combined range; a class with only an inherited constructor
|
|
1005
|
+
// (extends, no own ctor) has an arity UCN can't see — accept any count
|
|
1006
|
+
// rather than false-flag every call against the implicit 0-arg default.
|
|
1007
|
+
const ctorParamLists = _constructorParamLists(index, def, lang);
|
|
1008
|
+
const inheritedCtorOnly = !ctorParamLists && def.type === 'class' && !!def.extends;
|
|
958
1009
|
const selfParams = langTraits(lang)?.selfParam;
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
// Get all call sites using findCallers for accurate resolution
|
|
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;
|
|
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.
|
|
981
|
-
includeMethods: true,
|
|
982
|
-
includeUncertain: verifyIncludeUncertain,
|
|
983
|
-
targetDefinitions: [def],
|
|
1010
|
+
const stripSelf = (list) => (selfParams && list.length > 0 && list[0] && selfParams.includes(list[0].name))
|
|
1011
|
+
? list.slice(1) : list;
|
|
1012
|
+
const rawParamLists = ctorParamLists ||
|
|
1013
|
+
[(arrowTypes?.paramsStructured) || def.paramsStructured || []];
|
|
1014
|
+
const params = stripSelf(rawParamLists[0]);
|
|
1015
|
+
const arities = rawParamLists.map(l => {
|
|
1016
|
+
const list = stripSelf(l);
|
|
1017
|
+
const nonRest = list.filter(p => !p.rest);
|
|
1018
|
+
const optional = nonRest.filter(p => p.optional || p.default !== undefined).length;
|
|
1019
|
+
return { hasRest: list.some(p => p.rest), max: nonRest.length, min: nonRest.length - optional };
|
|
984
1020
|
});
|
|
1021
|
+
const hasRest = inheritedCtorOnly || arities.some(a => a.hasRest);
|
|
1022
|
+
// Rest params don't count toward expected/min — they accept 0+ extra args
|
|
1023
|
+
const expectedParamCount = Math.max(...arities.map(a => a.max));
|
|
1024
|
+
const minArgs = inheritedCtorOnly ? 0 : Math.min(...arities.map(a => a.min));
|
|
985
1025
|
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (!r || ['self', 'cls', 'this', 'super'].includes(r)) return true;
|
|
994
|
-
if (r.toLowerCase().includes(targetClassName.toLowerCase())) return true;
|
|
995
|
-
// Check local variable type inference from constructor assignments
|
|
996
|
-
if (c.callerFile) {
|
|
997
|
-
const callerDef = c.callerStartLine ? { file: c.callerFile, startLine: c.callerStartLine, endLine: c.callerEndLine } : null;
|
|
998
|
-
if (callerDef) {
|
|
999
|
-
const callerCalls = index.getCachedCalls(c.callerFile);
|
|
1000
|
-
if (callerCalls && Array.isArray(callerCalls)) {
|
|
1001
|
-
const localTypes = new Map();
|
|
1002
|
-
for (const call of callerCalls) {
|
|
1003
|
-
if (call.line >= callerDef.startLine && call.line <= callerDef.endLine) {
|
|
1004
|
-
if (!call.isMethod && !call.receiver) {
|
|
1005
|
-
const syms = index.symbols.get(call.name);
|
|
1006
|
-
if (syms && syms.some(s => s.type === 'class')) {
|
|
1007
|
-
const content = index._readFile(c.callerFile);
|
|
1008
|
-
const clines = content.split('\n');
|
|
1009
|
-
const cline = clines[call.line - 1] || '';
|
|
1010
|
-
const m = cline.match(/^\s*(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/);
|
|
1011
|
-
if (m && m[2] === call.name) {
|
|
1012
|
-
localTypes.set(m[1], call.name);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
const receiverType = localTypes.get(r);
|
|
1019
|
-
if (receiverType) {
|
|
1020
|
-
return receiverType === targetClassName;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
// Check parameter type annotations: def foo(tracker: SourceTracker) → tracker.record()
|
|
1026
|
-
if (c.callerFile && c.callerStartLine) {
|
|
1027
|
-
const callerSymbol = index.findEnclosingFunction(c.callerFile, c.line, true);
|
|
1028
|
-
if (callerSymbol && callerSymbol.paramsStructured) {
|
|
1029
|
-
for (const param of callerSymbol.paramsStructured) {
|
|
1030
|
-
if (param.name === r && param.type) {
|
|
1031
|
-
const typeMatches = param.type.match(/\b([A-Za-z_]\w*)\b/g);
|
|
1032
|
-
if (typeMatches && typeMatches.some(t => t === targetClassName)) {
|
|
1033
|
-
return true;
|
|
1034
|
-
}
|
|
1035
|
-
return false;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
// Unique method heuristic: if the called method exists on exactly one class
|
|
1041
|
-
// and it matches the target, include the call (no other class could match)
|
|
1042
|
-
const methodDefs = index.symbols.get(name);
|
|
1043
|
-
if (methodDefs) {
|
|
1044
|
-
const classNames = new Set();
|
|
1045
|
-
for (const d of methodDefs) {
|
|
1046
|
-
if (d.className) classNames.add(d.className);
|
|
1047
|
-
}
|
|
1048
|
-
if (classNames.size === 1 && classNames.has(targetClassName)) {
|
|
1049
|
-
return true;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
// className explicitly set but receiver type unknown — filter it out
|
|
1053
|
-
return false;
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1026
|
+
// v4 tiered contract: the confirmed band is arg-checked below; unverified
|
|
1027
|
+
// candidates stay VISIBLE in their own band with reasons (never silently
|
|
1028
|
+
// dropped). Engine receiver physics replace the pre-v4 className filter
|
|
1029
|
+
// and the isMethodCall secondary filter — --include-methods and
|
|
1030
|
+
// --include-uncertain are implied no-ops for verify.
|
|
1031
|
+
const { confirmed: callerResults, unverified: sweepUnverified, account } =
|
|
1032
|
+
contractedCallerSweep(index, name, def);
|
|
1056
1033
|
|
|
1057
1034
|
// Convert caller results to usage-like objects for analyzeCallSite.
|
|
1058
1035
|
// Carry callerFile/callerStartLine through so we can compute inTestCase.
|
|
@@ -1071,36 +1048,7 @@ function verify(index, name, options = {}) {
|
|
|
1071
1048
|
const mismatches = [];
|
|
1072
1049
|
const uncertain = [];
|
|
1073
1050
|
|
|
1074
|
-
// If the definition is NOT a method, filter out method calls (e.g., dict.get() vs get())
|
|
1075
|
-
// This prevents false positives where a standalone function name matches method calls.
|
|
1076
|
-
// Exception: module-level calls (module.func()) are kept when the receiver matches the
|
|
1077
|
-
// target module's name and is an imported name (e.g., jobs.submit() where jobs is imported
|
|
1078
|
-
// and the function lives in jobs.py).
|
|
1079
1051
|
const defIsMethod = !!(def.isMethod || def.type === 'method' || def.className);
|
|
1080
|
-
const targetBasename = path.basename(def.file, path.extname(def.file));
|
|
1081
|
-
const defFileEntry = index.files.get(def.file);
|
|
1082
|
-
const defLang = defFileEntry?.language;
|
|
1083
|
-
|
|
1084
|
-
// Build import-name lookup for receiver checking (module.func() vs dict.get())
|
|
1085
|
-
const importNameCache = new Map();
|
|
1086
|
-
function getImportedNames(filePath) {
|
|
1087
|
-
if (importNameCache.has(filePath)) return importNameCache.get(filePath);
|
|
1088
|
-
const names = new Set();
|
|
1089
|
-
const fe = index.files.get(filePath);
|
|
1090
|
-
if (!fe) { importNameCache.set(filePath, names); return names; }
|
|
1091
|
-
try {
|
|
1092
|
-
const content = index._readFile(filePath);
|
|
1093
|
-
const { imports: rawImports, importAliases } = extractImports(content, fe.language);
|
|
1094
|
-
for (const imp of rawImports) {
|
|
1095
|
-
if (imp.names) for (const n of imp.names) names.add(n);
|
|
1096
|
-
}
|
|
1097
|
-
if (importAliases) {
|
|
1098
|
-
for (const alias of importAliases) names.add(alias.local);
|
|
1099
|
-
}
|
|
1100
|
-
} catch (e) { /* skip */ }
|
|
1101
|
-
importNameCache.set(filePath, names);
|
|
1102
|
-
return names;
|
|
1103
|
-
}
|
|
1104
1052
|
|
|
1105
1053
|
// Helper: extract pattern flags (Feature A/B) from analyzeCallSite result.
|
|
1106
1054
|
// Reused so each valid/mismatch/uncertain entry carries the same shape.
|
|
@@ -1114,42 +1062,12 @@ function verify(index, name, options = {}) {
|
|
|
1114
1062
|
};
|
|
1115
1063
|
}
|
|
1116
1064
|
|
|
1065
|
+
const verifyLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
1117
1066
|
for (const call of calls) {
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
// Allow module-level calls only when:
|
|
1123
|
-
// 1. Receiver matches target file's basename (e.g., jobs == jobs for jobs.py)
|
|
1124
|
-
// 2. Receiver is an imported name (not a local variable)
|
|
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) {
|
|
1130
|
-
const callReceiver = call.receiver;
|
|
1131
|
-
if (callReceiver && callReceiver === targetBasename) {
|
|
1132
|
-
const importedNames = getImportedNames(call.file);
|
|
1133
|
-
if (!importedNames.has(callReceiver)) continue;
|
|
1134
|
-
// Receiver matches target module and is imported — keep it
|
|
1135
|
-
} else if (callReceiver && langTraits(defLang)?.hasReceiverPackageCalls) {
|
|
1136
|
-
// Go: receiver is package alias (last segment of import path, e.g., "controller"
|
|
1137
|
-
// from "k8s.io/.../pkg/controller"), not the filename ("controller_utils").
|
|
1138
|
-
// Check if receiver matches the directory name of the target file.
|
|
1139
|
-
const targetDir = path.basename(path.dirname(def.file));
|
|
1140
|
-
if (callReceiver !== targetDir) {
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
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.
|
|
1149
|
-
} else {
|
|
1150
|
-
continue;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1067
|
+
const siteKey = `${call.file}:${call.line}`;
|
|
1068
|
+
const occurrence = verifyLineSeen.get(siteKey) || 0;
|
|
1069
|
+
verifyLineSeen.set(siteKey, occurrence + 1);
|
|
1070
|
+
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
1153
1071
|
|
|
1154
1072
|
// Carry callerFile/callerStartLine so tagInTestCase can resolve the
|
|
1155
1073
|
// enclosing function in a later pass.
|
|
@@ -1184,7 +1102,20 @@ function verify(index, name, options = {}) {
|
|
|
1184
1102
|
continue;
|
|
1185
1103
|
}
|
|
1186
1104
|
|
|
1187
|
-
|
|
1105
|
+
let argCount = analysis.argCount;
|
|
1106
|
+
// Method-expression / UFCS receiver shift (fix #230): Go
|
|
1107
|
+
// `M.Add(*m, 2)` and Rust `Engine::run(&e, 1)` pass the receiver as
|
|
1108
|
+
// the FIRST argument — the same +1 shift the #205 arity discipline
|
|
1109
|
+
// already applies when confirming these sites. Without it the
|
|
1110
|
+
// arg-check false-flagged every confirmed method-expression call.
|
|
1111
|
+
const targetTypeName = def.className || (def.receiver || '').replace(/^\*/, '');
|
|
1112
|
+
if (targetTypeName && call.receiver === targetTypeName && argCount > 0) {
|
|
1113
|
+
const qualStyle = langTraits(lang)?.typeQualifiedCallStyle;
|
|
1114
|
+
if ((qualStyle === 'method-expr' && def.receiver) ||
|
|
1115
|
+
(qualStyle === 'path' && def.isMethod)) {
|
|
1116
|
+
argCount -= 1;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1188
1119
|
|
|
1189
1120
|
// Check if arg count is valid
|
|
1190
1121
|
if (hasRest) {
|
|
@@ -1319,7 +1250,10 @@ function verify(index, name, options = {}) {
|
|
|
1319
1250
|
optional: p.optional || p.default !== undefined,
|
|
1320
1251
|
hasDefault: p.default !== undefined
|
|
1321
1252
|
})),
|
|
1322
|
-
|
|
1253
|
+
// max: null = unbounded (rest param) — typed for JSON consumers;
|
|
1254
|
+
// the text formatter renders it as `${min}+` (fix #230, was the
|
|
1255
|
+
// string '∞' leaking into JSON output).
|
|
1256
|
+
expectedArgs: { min: minArgs, max: hasRest ? null : expectedParamCount },
|
|
1323
1257
|
totalCalls: valid.length + mismatches.length + uncertain.length,
|
|
1324
1258
|
valid: valid.length,
|
|
1325
1259
|
mismatches: mismatches.length,
|
|
@@ -1327,6 +1261,11 @@ function verify(index, name, options = {}) {
|
|
|
1327
1261
|
validDetails: valid,
|
|
1328
1262
|
mismatchDetails: mismatches,
|
|
1329
1263
|
uncertainDetails: uncertain,
|
|
1264
|
+
// v4 tiered contract: candidates without binding/receiver evidence are
|
|
1265
|
+
// NOT arg-checked (they may target another symbol) but stay visible.
|
|
1266
|
+
unverifiedCount: sweepUnverified.length,
|
|
1267
|
+
unverifiedSites: sweepUnverified.map(unverifiedSiteShape),
|
|
1268
|
+
account,
|
|
1330
1269
|
patterns: patternsAgg,
|
|
1331
1270
|
scopeWarning
|
|
1332
1271
|
};
|
|
@@ -1352,19 +1291,29 @@ function plan(index, name, options = {}) {
|
|
|
1352
1291
|
const def = resolved.def || definitions[0];
|
|
1353
1292
|
// BUG-BY: enrich types for typed-arrow-fn declarations.
|
|
1354
1293
|
const arrowTypes = extractArrowTypesFromVarDecl(index, def);
|
|
1355
|
-
|
|
1294
|
+
// Class target: the signature being planned is the CONSTRUCTOR's
|
|
1295
|
+
// (fix #230, same rule as verify) — single-ctor classes only; with
|
|
1296
|
+
// overloads the class def's own (empty) list stays, since plan cannot
|
|
1297
|
+
// know which overload the user means.
|
|
1298
|
+
const planLang = index.files.get(def.file)?.language;
|
|
1299
|
+
const planCtorLists = _constructorParamLists(index, def, planLang);
|
|
1300
|
+
const currentParams = (planCtorLists && planCtorLists.length === 1 && planCtorLists[0]) ||
|
|
1301
|
+
(arrowTypes?.paramsStructured) || def.paramsStructured || [];
|
|
1356
1302
|
// BUG-BV: render with TS-correct param formatting (`opt?: number`).
|
|
1357
|
-
const currentSignature = formatTypedSignature(def,
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
//
|
|
1366
|
-
// and verify
|
|
1367
|
-
|
|
1303
|
+
const currentSignature = formatTypedSignature(def,
|
|
1304
|
+
(planCtorLists && planCtorLists.length === 1)
|
|
1305
|
+
? { paramsStructured: currentParams }
|
|
1306
|
+
: arrowTypes ? {
|
|
1307
|
+
paramsStructured: arrowTypes.paramsStructured,
|
|
1308
|
+
returnType: arrowTypes.returnType
|
|
1309
|
+
} : {});
|
|
1310
|
+
|
|
1311
|
+
// BUG-BW: plan must discover call sites the same way verify does — both
|
|
1312
|
+
// run contractedCallerSweep (v4 tiered contract), so plan and verify stay
|
|
1313
|
+
// in lock-step by construction. Unverified candidates are NOT planned
|
|
1314
|
+
// (they may target another symbol) but stay visible with reasons.
|
|
1315
|
+
const { sites: planCallSites, unverifiedSites: planUnverified, account: planAccount } =
|
|
1316
|
+
computePlanCallSites(index, name, def);
|
|
1368
1317
|
const impactScopeWarning = computePlanScopeWarning(index, name, def, options);
|
|
1369
1318
|
|
|
1370
1319
|
// Reject ambiguous multi-op invocations rather than silently coalescing.
|
|
@@ -1398,9 +1347,14 @@ function plan(index, name, options = {}) {
|
|
|
1398
1347
|
};
|
|
1399
1348
|
}
|
|
1400
1349
|
operation = 'add-param';
|
|
1350
|
+
// Default parameter values only exist in some languages (trait).
|
|
1351
|
+
// For Go/Java/Rust a --default value is a suggested ARGUMENT for the
|
|
1352
|
+
// call sites, never signature syntax — `opt = null` is not valid Go.
|
|
1353
|
+
const planFileEntry = index.files.get(def.file);
|
|
1354
|
+
const langHasDefaults = langTraits(planFileEntry?.language)?.hasDefaultParams !== false;
|
|
1401
1355
|
const newParam = {
|
|
1402
1356
|
name: options.addParam,
|
|
1403
|
-
...(options.defaultValue && { default: options.defaultValue })
|
|
1357
|
+
...(options.defaultValue && langHasDefaults && { default: options.defaultValue })
|
|
1404
1358
|
};
|
|
1405
1359
|
|
|
1406
1360
|
// When adding a param, insert before rest params (*args/**kwargs) and
|
|
@@ -1436,11 +1390,17 @@ function plan(index, name, options = {}) {
|
|
|
1436
1390
|
const newRet = arrowTypes?.returnType || def.returnType;
|
|
1437
1391
|
if (newRet) newSignature += `: ${newRet}`;
|
|
1438
1392
|
|
|
1439
|
-
// Describe changes needed at each call site
|
|
1393
|
+
// Describe changes needed at each call site. Without language support
|
|
1394
|
+
// for default values, every call site must pass the new argument.
|
|
1440
1395
|
for (const site of planCallSites) {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1396
|
+
let suggestion;
|
|
1397
|
+
if (options.defaultValue && langHasDefaults) {
|
|
1398
|
+
suggestion = `No change needed (has default value)`;
|
|
1399
|
+
} else if (options.defaultValue) {
|
|
1400
|
+
suggestion = `Add argument: ${options.defaultValue} (no default parameter values in ${planFileEntry?.language || 'this language'})`;
|
|
1401
|
+
} else {
|
|
1402
|
+
suggestion = `Add argument: ${options.addParam}`;
|
|
1403
|
+
}
|
|
1444
1404
|
changes.push({
|
|
1445
1405
|
file: site.file,
|
|
1446
1406
|
line: site.line,
|
|
@@ -1491,16 +1451,32 @@ function plan(index, name, options = {}) {
|
|
|
1491
1451
|
}
|
|
1492
1452
|
const callerArgIndex = paramIndex - selfOffset;
|
|
1493
1453
|
|
|
1494
|
-
//
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1454
|
+
// Removing the receiver param itself (self/cls/&self): bound calls
|
|
1455
|
+
// pass it implicitly — no caller-side change exists (fix #230; used
|
|
1456
|
+
// to emit "Remove argument 0: ?" at every site).
|
|
1457
|
+
if (callerArgIndex >= 0) {
|
|
1458
|
+
// Describe changes at each call site
|
|
1459
|
+
for (const site of planCallSites) {
|
|
1460
|
+
if (site.args && site.argCount > callerArgIndex) {
|
|
1461
|
+
changes.push({
|
|
1462
|
+
file: site.file,
|
|
1463
|
+
line: site.line,
|
|
1464
|
+
expression: site.expression,
|
|
1465
|
+
suggestion: `Remove argument ${callerArgIndex + 1}: ${site.args[callerArgIndex] || '?'}`,
|
|
1466
|
+
args: site.args
|
|
1467
|
+
});
|
|
1468
|
+
} else if (!site.args) {
|
|
1469
|
+
// Arguments unparseable (macro bodies, generated code) —
|
|
1470
|
+
// surface for manual review instead of dropping silently
|
|
1471
|
+
// (fix #230).
|
|
1472
|
+
changes.push({
|
|
1473
|
+
file: site.file,
|
|
1474
|
+
line: site.line,
|
|
1475
|
+
expression: site.expression,
|
|
1476
|
+
suggestion: 'Could not parse arguments — review this call site manually',
|
|
1477
|
+
needsReview: true
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1504
1480
|
}
|
|
1505
1481
|
}
|
|
1506
1482
|
}
|
|
@@ -1509,10 +1485,18 @@ function plan(index, name, options = {}) {
|
|
|
1509
1485
|
operation = 'rename';
|
|
1510
1486
|
newSignature = currentSignature.replace(new RegExp('\\b' + escapeRegExp(name) + '\\b'), options.renameTo);
|
|
1511
1487
|
|
|
1512
|
-
// All call sites need renaming
|
|
1488
|
+
// All call sites need renaming. Global replace: a line with several
|
|
1489
|
+
// calls (`compute(compute(1))`) renames every occurrence, and the
|
|
1490
|
+
// line appears ONCE however many call records it holds (fix #230 —
|
|
1491
|
+
// the non-global regex left the inner call behind and emitted a
|
|
1492
|
+
// duplicate entry per record).
|
|
1493
|
+
const renamedLines = new Set();
|
|
1513
1494
|
for (const site of planCallSites) {
|
|
1495
|
+
const lineKey = `${site.file}:${site.line}`;
|
|
1496
|
+
if (renamedLines.has(lineKey)) continue;
|
|
1497
|
+
renamedLines.add(lineKey);
|
|
1514
1498
|
const newExpression = site.expression.replace(
|
|
1515
|
-
new RegExp('\\b' + escapeRegExp(name) + '\\b'),
|
|
1499
|
+
new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g'),
|
|
1516
1500
|
options.renameTo
|
|
1517
1501
|
);
|
|
1518
1502
|
changes.push({
|
|
@@ -1524,7 +1508,17 @@ function plan(index, name, options = {}) {
|
|
|
1524
1508
|
});
|
|
1525
1509
|
}
|
|
1526
1510
|
|
|
1527
|
-
// Also include import statements that reference the renamed function
|
|
1511
|
+
// Also include import statements that reference the renamed function.
|
|
1512
|
+
// Name ownership (fix #230, the #217 rule): an import of the same
|
|
1513
|
+
// NAME from an unrelated module is not this rename's import —
|
|
1514
|
+
// renaming alpha.compute must not rewrite `from beta import compute`
|
|
1515
|
+
// (the plan's own call-site sweep already excludes caller_b's calls
|
|
1516
|
+
// as other-definition-import; the import pass has to agree).
|
|
1517
|
+
// 'no' (the binding provably resolves elsewhere) skips; 'unknown'
|
|
1518
|
+
// (CJS surfaces, star imports, resolver gaps) keeps the import —
|
|
1519
|
+
// a missed import breaks the rename just as surely.
|
|
1520
|
+
const { _nameBindingReaches } = require('./callers');
|
|
1521
|
+
const renameTargetFiles = new Set([def.file]);
|
|
1528
1522
|
const usages = index.usages(name, { codeOnly: true });
|
|
1529
1523
|
const importUsages = usages.filter(u => u.usageType === 'import' && !u.isDefinition);
|
|
1530
1524
|
for (const imp of importUsages) {
|
|
@@ -1533,6 +1527,10 @@ function plan(index, name, options = {}) {
|
|
|
1533
1527
|
c.file === (imp.relativePath || imp.file) && c.line === imp.line
|
|
1534
1528
|
);
|
|
1535
1529
|
if (alreadyCovered) continue;
|
|
1530
|
+
if (imp.file && def.file &&
|
|
1531
|
+
_nameBindingReaches(index, imp.file, name, renameTargetFiles) === 'no') {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1536
1534
|
const newImport = imp.content.trim().replace(
|
|
1537
1535
|
new RegExp('\\b' + escapeRegExp(name) + '\\b'),
|
|
1538
1536
|
options.renameTo
|
|
@@ -1568,6 +1566,11 @@ function plan(index, name, options = {}) {
|
|
|
1568
1566
|
totalChanges: changes.length,
|
|
1569
1567
|
filesAffected: new Set(changes.map(c => c.file)).size,
|
|
1570
1568
|
changes,
|
|
1569
|
+
// v4 tiered contract: sites that MAY also need this change but lack
|
|
1570
|
+
// binding/receiver evidence — review manually before refactoring.
|
|
1571
|
+
unverifiedCount: planUnverified.length,
|
|
1572
|
+
unverifiedSites: planUnverified,
|
|
1573
|
+
account: planAccount,
|
|
1571
1574
|
scopeWarning: impactScopeWarning
|
|
1572
1575
|
};
|
|
1573
1576
|
} finally { index._endOp(); }
|