ucn 4.2.3 → 5.0.2
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/verify.js
CHANGED
|
@@ -5,9 +5,89 @@
|
|
|
5
5
|
* as the first argument instead of using `this`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { detectLanguage, getParser,
|
|
8
|
+
const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits } = require('../languages');
|
|
9
9
|
const { escapeRegExp, codeUnitCompare } = require('./shared');
|
|
10
10
|
|
|
11
|
+
function codeUnitColumnForByteColumn(line, byteColumn) {
|
|
12
|
+
if (!Number.isInteger(byteColumn) || byteColumn < 0) return null;
|
|
13
|
+
let bytes = 0;
|
|
14
|
+
for (let i = 0; i <= line.length; i++) {
|
|
15
|
+
if (bytes === byteColumn) return i;
|
|
16
|
+
if (i === line.length) break;
|
|
17
|
+
const cp = line.codePointAt(i);
|
|
18
|
+
const ch = String.fromCodePoint(cp);
|
|
19
|
+
bytes += Buffer.byteLength(ch);
|
|
20
|
+
if (ch.length === 2) i++;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Replace only AST identifier tokens on one source line. */
|
|
26
|
+
function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
27
|
+
preferredByteColumns = null, expectedCallCount = null) {
|
|
28
|
+
const absolute = filePath && require('path').isAbsolute(filePath)
|
|
29
|
+
? filePath : require('path').join(index.root, filePath || '');
|
|
30
|
+
const content = index._readFile(absolute);
|
|
31
|
+
const sourceLine = content.split('\n')[lineNumber - 1] || '';
|
|
32
|
+
let byteColumns = Array.isArray(preferredByteColumns)
|
|
33
|
+
? preferredByteColumns.filter(Number.isInteger) : [];
|
|
34
|
+
|
|
35
|
+
if (byteColumns.length === 0) {
|
|
36
|
+
const language = index.files.get(absolute)?.language ||
|
|
37
|
+
detectLanguage(absolute, index.root);
|
|
38
|
+
const parser = language && getParser(language);
|
|
39
|
+
const tree = parser && (index._getParsedTree?.(absolute, content, language) ||
|
|
40
|
+
safeParse(parser, content));
|
|
41
|
+
if (tree) {
|
|
42
|
+
const targetRow = lineNumber - 1;
|
|
43
|
+
const stack = [tree.rootNode];
|
|
44
|
+
while (stack.length > 0) {
|
|
45
|
+
const node = stack.pop();
|
|
46
|
+
if (node.endPosition.row < targetRow ||
|
|
47
|
+
node.startPosition.row > targetRow) continue;
|
|
48
|
+
if (node.startPosition.row === targetRow && node.text === oldName &&
|
|
49
|
+
/identifier(?:_pattern)?$/.test(node.type)) {
|
|
50
|
+
let eligible = preferredByteColumns == null;
|
|
51
|
+
if (!eligible) {
|
|
52
|
+
const callTypes = new Set([
|
|
53
|
+
'call', 'call_expression', 'method_invocation',
|
|
54
|
+
'invocation_expression', 'method_call_expression',
|
|
55
|
+
]);
|
|
56
|
+
for (let parent = node.parent, depth = 0;
|
|
57
|
+
parent && depth < 5; parent = parent.parent, depth++) {
|
|
58
|
+
if (!callTypes.has(parent.type)) continue;
|
|
59
|
+
const target = parent.childForFieldName('function') ||
|
|
60
|
+
parent.childForFieldName('name') ||
|
|
61
|
+
parent.childForFieldName('method') ||
|
|
62
|
+
parent.namedChild(0);
|
|
63
|
+
if (target && node.startIndex >= target.startIndex &&
|
|
64
|
+
node.endIndex <= target.endIndex) eligible = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (eligible) byteColumns.push(node.startPosition.column);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
stack.push(...(node.namedChildren || []));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const columns = [...new Set(byteColumns
|
|
77
|
+
.map(column => codeUnitColumnForByteColumn(sourceLine, column))
|
|
78
|
+
.filter(Number.isInteger))].sort((a, b) => b - a);
|
|
79
|
+
if (expectedCallCount != null && columns.length !== expectedCallCount) {
|
|
80
|
+
return { source: sourceLine.trim(), renamed: sourceLine.trim(), count: 0 };
|
|
81
|
+
}
|
|
82
|
+
let renamed = sourceLine;
|
|
83
|
+
for (const column of columns) {
|
|
84
|
+
if (renamed.slice(column, column + oldName.length) !== oldName) continue;
|
|
85
|
+
renamed = renamed.slice(0, column) + newName +
|
|
86
|
+
renamed.slice(column + oldName.length);
|
|
87
|
+
}
|
|
88
|
+
return { source: sourceLine.trim(), renamed: renamed.trim(), count: columns.length };
|
|
89
|
+
}
|
|
90
|
+
|
|
11
91
|
// ============================================================================
|
|
12
92
|
// CALL-SITE CLASSIFICATION (Feature A)
|
|
13
93
|
// ============================================================================
|
|
@@ -169,7 +249,8 @@ function _collectCallNodes(node, callTypes, targetRow, funcName, limit, out = []
|
|
|
169
249
|
// JS/TS constructor: new ClassName(args) — class is in 'constructor'
|
|
170
250
|
// field (fix #230: these sites used to fall out as "Could not
|
|
171
251
|
// parse call arguments" and every class verify went uncertain).
|
|
172
|
-
const ctorNode = node.childForFieldName('constructor')
|
|
252
|
+
const ctorNode = node.childForFieldName('constructor') ||
|
|
253
|
+
node.childForFieldName('type');
|
|
173
254
|
if (ctorNode) {
|
|
174
255
|
const typeName = ctorNode.text.replace(/<.*>$/, '').split('.').pop();
|
|
175
256
|
if (typeName === funcName) out.push(node);
|
|
@@ -183,12 +264,30 @@ function _collectCallNodes(node, callTypes, targetRow, funcName, limit, out = []
|
|
|
183
264
|
funcNode = funcNode.childForFieldName('function') || funcNode.namedChild(0);
|
|
184
265
|
}
|
|
185
266
|
if (funcNode) {
|
|
186
|
-
const
|
|
187
|
-
?
|
|
188
|
-
:
|
|
267
|
+
const memberProperty = funcNode.type === 'member_expression'
|
|
268
|
+
? funcNode.childForFieldName('property')
|
|
269
|
+
: null;
|
|
270
|
+
const indirectKind = memberProperty &&
|
|
271
|
+
['call', 'apply', 'bind'].includes(memberProperty.text)
|
|
272
|
+
? memberProperty.text
|
|
273
|
+
: null;
|
|
274
|
+
const indirectObject = indirectKind
|
|
275
|
+
? funcNode.childForFieldName('object')
|
|
276
|
+
: null;
|
|
277
|
+
const indirectTarget = indirectObject?.type === 'member_expression'
|
|
278
|
+
? indirectObject.childForFieldName('property')?.text
|
|
279
|
+
: indirectObject?.text;
|
|
280
|
+
const funcText = funcNode.type === 'member_expression' ||
|
|
281
|
+
funcNode.type === 'member_access_expression' ||
|
|
282
|
+
funcNode.type === 'selector_expression' ||
|
|
283
|
+
funcNode.type === 'field_expression' || funcNode.type === 'attribute'
|
|
284
|
+
? (funcNode.childForFieldName('property') || funcNode.childForFieldName('name') ||
|
|
285
|
+
funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') ||
|
|
286
|
+
funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
287
|
+
: funcNode.type === 'scoped_identifier' || funcNode.type === 'qualified_identifier'
|
|
189
288
|
? (funcNode.childForFieldName('name') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
190
289
|
: funcNode.text;
|
|
191
|
-
if (funcText === funcName) out.push(node);
|
|
290
|
+
if (funcText === funcName || indirectTarget === funcName) out.push(node);
|
|
192
291
|
}
|
|
193
292
|
}
|
|
194
293
|
if (out.length >= limit) return out;
|
|
@@ -474,7 +573,7 @@ function extractArrowTypesFromVarDecl(index, def) {
|
|
|
474
573
|
* @returns {Array<Array<object>>|null}
|
|
475
574
|
*/
|
|
476
575
|
function _constructorParamLists(index, def, lang) {
|
|
477
|
-
if (!def || !def.file || !['class', 'enum', 'record'].includes(def.type)) return null;
|
|
576
|
+
if (!def || !def.file || !['class', 'struct', 'enum', 'record'].includes(def.type)) return null;
|
|
478
577
|
const lists = [];
|
|
479
578
|
const endLine = def.endLine != null ? def.endLine : Infinity;
|
|
480
579
|
const inRange = (d) => d.file === def.file &&
|
|
@@ -652,10 +751,13 @@ function computePlanCallSites(index, name, def) {
|
|
|
652
751
|
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
653
752
|
sites.push({
|
|
654
753
|
file: call.relativePath,
|
|
754
|
+
absoluteFile: call.file,
|
|
655
755
|
line: call.line,
|
|
756
|
+
...(Number.isInteger(c.column) && { column: c.column }),
|
|
656
757
|
expression: (call.content || '').trim(),
|
|
657
758
|
args: analysis.args,
|
|
658
759
|
argCount: analysis.argCount,
|
|
760
|
+
...(c.calledAs && { calledAs: c.calledAs }),
|
|
659
761
|
});
|
|
660
762
|
}
|
|
661
763
|
clearTreeCache(index);
|
|
@@ -711,7 +813,7 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
711
813
|
const content = index._readFile(call.file);
|
|
712
814
|
// HTML files need special handling: parse script blocks as JS
|
|
713
815
|
if (language === 'html') {
|
|
714
|
-
const htmlModule =
|
|
816
|
+
const htmlModule = getLanguageAdapter('html');
|
|
715
817
|
const htmlParser = getParser('html');
|
|
716
818
|
const jsParser = getParser('javascript');
|
|
717
819
|
if (!htmlParser || !jsParser) return { args: null, argCount: 0 };
|
|
@@ -731,7 +833,7 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
731
833
|
|
|
732
834
|
// Call node types vary by language
|
|
733
835
|
const callTypes = new Set(['call_expression', 'call', 'method_invocation',
|
|
734
|
-
'object_creation_expression', 'new_expression']);
|
|
836
|
+
'invocation_expression', 'object_creation_expression', 'new_expression']);
|
|
735
837
|
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
736
838
|
|
|
737
839
|
// Find the call expression at the target line matching funcName
|
|
@@ -765,9 +867,56 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
765
867
|
const argsNode = callNode.childForFieldName('arguments');
|
|
766
868
|
if (!argsNode) return { args: [], argCount: 0, isMethodCall, ...ctx };
|
|
767
869
|
|
|
768
|
-
|
|
870
|
+
let args = [];
|
|
769
871
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
770
|
-
|
|
872
|
+
const argNode = argsNode.namedChild(i);
|
|
873
|
+
if (argNode.type.includes('comment')) continue;
|
|
874
|
+
args.push(argNode.text.trim());
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Function.prototype indirection has a precise, AST-visible argument
|
|
878
|
+
// mapping. `fn.call(thisArg, a, b)` invokes fn(a, b). `fn.apply`
|
|
879
|
+
// is countable only when its argument array is a literal. `bind`
|
|
880
|
+
// creates a partially applied function rather than invoking it, so it
|
|
881
|
+
// remains explicitly uncertain instead of looking like a parser bug.
|
|
882
|
+
let indirectKind = null;
|
|
883
|
+
if (language === 'javascript' || language === 'typescript' ||
|
|
884
|
+
language === 'tsx' || language === 'html') {
|
|
885
|
+
const property = funcNode?.type === 'member_expression'
|
|
886
|
+
? funcNode.childForFieldName('property')?.text
|
|
887
|
+
: null;
|
|
888
|
+
if (['call', 'apply', 'bind'].includes(property)) indirectKind = property;
|
|
889
|
+
}
|
|
890
|
+
if (indirectKind === 'bind') {
|
|
891
|
+
return {
|
|
892
|
+
args: null,
|
|
893
|
+
argCount: 0,
|
|
894
|
+
indirectKind,
|
|
895
|
+
uncertainReason: 'Function.bind creates a partial application; final invocation arguments are not known here',
|
|
896
|
+
isMethodCall,
|
|
897
|
+
...ctx,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
if (indirectKind === 'call') {
|
|
901
|
+
args = args.slice(1);
|
|
902
|
+
} else if (indirectKind === 'apply') {
|
|
903
|
+
const arrayArg = argsNode.namedChild(1);
|
|
904
|
+
if (!arrayArg || arrayArg.type !== 'array') {
|
|
905
|
+
return {
|
|
906
|
+
args: null,
|
|
907
|
+
argCount: 0,
|
|
908
|
+
indirectKind,
|
|
909
|
+
uncertainReason: 'Function.apply argument list is not a static array literal',
|
|
910
|
+
isMethodCall,
|
|
911
|
+
...ctx,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
args = [];
|
|
915
|
+
for (let i = 0; i < arrayArg.namedChildCount; i++) {
|
|
916
|
+
const argNode = arrayArg.namedChild(i);
|
|
917
|
+
if (argNode.type.includes('comment')) continue;
|
|
918
|
+
args.push(argNode.text.trim());
|
|
919
|
+
}
|
|
771
920
|
}
|
|
772
921
|
|
|
773
922
|
return {
|
|
@@ -776,6 +925,7 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
776
925
|
hasSpread: args.some(a => a.startsWith('...')),
|
|
777
926
|
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a)),
|
|
778
927
|
isMethodCall,
|
|
928
|
+
...(indirectKind && { indirectKind }),
|
|
779
929
|
...ctx,
|
|
780
930
|
};
|
|
781
931
|
} catch (e) {
|
|
@@ -809,7 +959,7 @@ function analyzeCallShape(index, filePath, lineNum, funcName) {
|
|
|
809
959
|
if (!tree) {
|
|
810
960
|
const content = index._readFile(filePath);
|
|
811
961
|
if (language === 'html') {
|
|
812
|
-
const htmlModule =
|
|
962
|
+
const htmlModule = getLanguageAdapter('html');
|
|
813
963
|
const htmlParser = getParser('html');
|
|
814
964
|
const jsParser = getParser('javascript');
|
|
815
965
|
if (!htmlParser || !jsParser) return null;
|
|
@@ -827,7 +977,8 @@ function analyzeCallShape(index, filePath, lineNum, funcName) {
|
|
|
827
977
|
index._treeCache.set(filePath, tree);
|
|
828
978
|
}
|
|
829
979
|
|
|
830
|
-
const callTypes = new Set(['call_expression', 'call', 'method_invocation',
|
|
980
|
+
const callTypes = new Set(['call_expression', 'call', 'method_invocation',
|
|
981
|
+
'invocation_expression', 'object_creation_expression', 'new_expression']);
|
|
831
982
|
const callNode = findCallNode(tree.rootNode, callTypes, lineNum - 1, funcName);
|
|
832
983
|
if (!callNode) return null;
|
|
833
984
|
|
|
@@ -840,6 +991,7 @@ function analyzeCallShape(index, filePath, lineNum, funcName) {
|
|
|
840
991
|
const argTexts = [];
|
|
841
992
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
842
993
|
const argNode = argsNode.namedChild(i);
|
|
994
|
+
if (argNode.type.includes('comment')) continue;
|
|
843
995
|
argKinds.push(classifyArgNode(argNode));
|
|
844
996
|
argTexts.push(argNode.text.trim());
|
|
845
997
|
}
|
|
@@ -987,7 +1139,7 @@ function identifyCallPatterns(callSites, funcName) {
|
|
|
987
1139
|
function verify(index, name, options = {}) {
|
|
988
1140
|
index._beginOp();
|
|
989
1141
|
try {
|
|
990
|
-
const { def } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
1142
|
+
const { def, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
991
1143
|
if (!def) {
|
|
992
1144
|
return { found: false, function: name };
|
|
993
1145
|
}
|
|
@@ -1008,7 +1160,17 @@ function verify(index, name, options = {}) {
|
|
|
1008
1160
|
const selfParams = langTraits(lang)?.selfParam;
|
|
1009
1161
|
const stripSelf = (list) => (selfParams && list.length > 0 && list[0] && selfParams.includes(list[0].name))
|
|
1010
1162
|
? list.slice(1) : list;
|
|
1163
|
+
let callableIdentityParams = null;
|
|
1164
|
+
if (!ctorParamLists && ['c', 'cpp'].includes(lang)) {
|
|
1165
|
+
const { _closeCallableIdentityGroup } = require('./callers');
|
|
1166
|
+
const family = _closeCallableIdentityGroup(
|
|
1167
|
+
index, [def], index.symbols.get(name) || [def]);
|
|
1168
|
+
callableIdentityParams = family
|
|
1169
|
+
.filter(member => Array.isArray(member.paramsStructured))
|
|
1170
|
+
.map(member => member.paramsStructured);
|
|
1171
|
+
}
|
|
1011
1172
|
const rawParamLists = ctorParamLists ||
|
|
1173
|
+
(callableIdentityParams?.length ? callableIdentityParams : null) ||
|
|
1012
1174
|
[(arrowTypes?.paramsStructured) || def.paramsStructured || []];
|
|
1013
1175
|
const params = stripSelf(rawParamLists[0]);
|
|
1014
1176
|
const arities = rawParamLists.map(l => {
|
|
@@ -1039,6 +1201,11 @@ function verify(index, name, options = {}) {
|
|
|
1039
1201
|
content: c.content,
|
|
1040
1202
|
usageType: 'call',
|
|
1041
1203
|
receiver: c.receiver,
|
|
1204
|
+
// Preserve receiver identity through the usage-shaped adapter. Go
|
|
1205
|
+
// permits a local value to have the same spelling as its type; only
|
|
1206
|
+
// a type-qualified call is a method expression with an explicit
|
|
1207
|
+
// receiver argument.
|
|
1208
|
+
receiverType: c.receiverType,
|
|
1042
1209
|
callerFile: c.callerFile,
|
|
1043
1210
|
callerStartLine: c.callerStartLine,
|
|
1044
1211
|
}));
|
|
@@ -1081,7 +1248,7 @@ function verify(index, name, options = {}) {
|
|
|
1081
1248
|
file: call.relativePath,
|
|
1082
1249
|
line: call.line,
|
|
1083
1250
|
expression: call.content.trim(),
|
|
1084
|
-
reason: 'Could not parse call arguments',
|
|
1251
|
+
reason: analysis.uncertainReason || 'Could not parse call arguments',
|
|
1085
1252
|
patterns: patternFlagsFrom(analysis),
|
|
1086
1253
|
...carry,
|
|
1087
1254
|
});
|
|
@@ -1110,7 +1277,8 @@ function verify(index, name, options = {}) {
|
|
|
1110
1277
|
const targetTypeName = def.className || (def.receiver || '').replace(/^\*/, '');
|
|
1111
1278
|
if (targetTypeName && call.receiver === targetTypeName && argCount > 0) {
|
|
1112
1279
|
const qualStyle = langTraits(lang)?.typeQualifiedCallStyle;
|
|
1113
|
-
if ((qualStyle === 'method-expr' && def.receiver
|
|
1280
|
+
if ((qualStyle === 'method-expr' && def.receiver &&
|
|
1281
|
+
!call.receiverType) ||
|
|
1114
1282
|
(qualStyle === 'path' && def.isMethod)) {
|
|
1115
1283
|
argCount -= 1;
|
|
1116
1284
|
}
|
|
@@ -1266,7 +1434,8 @@ function verify(index, name, options = {}) {
|
|
|
1266
1434
|
unverifiedSites: sweepUnverified.map(unverifiedSiteShape),
|
|
1267
1435
|
account,
|
|
1268
1436
|
patterns: patternsAgg,
|
|
1269
|
-
scopeWarning
|
|
1437
|
+
scopeWarning,
|
|
1438
|
+
...(warnings.length > 0 && { warnings }),
|
|
1270
1439
|
};
|
|
1271
1440
|
} finally { index._endOp(); }
|
|
1272
1441
|
}
|
|
@@ -1278,6 +1447,44 @@ function verify(index, name, options = {}) {
|
|
|
1278
1447
|
* @param {object} options - { addParam, removeParam, renameTo, defaultValue }
|
|
1279
1448
|
* @returns {object} Plan with before/after signatures and affected call sites
|
|
1280
1449
|
*/
|
|
1450
|
+
// Strict reserved words per language — names that can never be identifiers.
|
|
1451
|
+
// Contextual/soft keywords (TS `interface`, Python `match`, C# `var`) are
|
|
1452
|
+
// deliberately absent: they are legal identifiers, and over-blocking a rename
|
|
1453
|
+
// is worse than trusting the compiler for the soft cases. Fail-open for
|
|
1454
|
+
// languages without an entry.
|
|
1455
|
+
const JS_RESERVED = new Set(('break case catch class const continue debugger default delete do else enum export ' +
|
|
1456
|
+
'extends false finally for function if import in instanceof new null return super switch this throw true try ' +
|
|
1457
|
+
'typeof var void while with yield let static await').split(' '));
|
|
1458
|
+
const C_RESERVED = new Set(('auto break case char const continue default do double else enum extern float for goto ' +
|
|
1459
|
+
'if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned ' +
|
|
1460
|
+
'void volatile while _Bool').split(' '));
|
|
1461
|
+
const RESERVED_WORDS_BY_LANGUAGE = {
|
|
1462
|
+
javascript: JS_RESERVED,
|
|
1463
|
+
typescript: JS_RESERVED,
|
|
1464
|
+
tsx: JS_RESERVED,
|
|
1465
|
+
python: new Set(('False None True and as assert async await break class continue def del elif else except ' +
|
|
1466
|
+
'finally for from global if import in is lambda nonlocal not or pass raise return try while with yield').split(' ')),
|
|
1467
|
+
go: new Set(('break case chan const continue default defer else fallthrough for func go goto if import ' +
|
|
1468
|
+
'interface map package range return select struct switch type var').split(' ')),
|
|
1469
|
+
rust: new Set(('as async await break const continue crate dyn else enum extern false fn for if impl in let ' +
|
|
1470
|
+
'loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where ' +
|
|
1471
|
+
'while').split(' ')),
|
|
1472
|
+
java: new Set(('abstract assert boolean break byte case catch char class const continue default do double ' +
|
|
1473
|
+
'else enum extends final finally float for goto if implements import instanceof int interface long native ' +
|
|
1474
|
+
'new package private protected public return short static strictfp super switch synchronized this throw ' +
|
|
1475
|
+
'throws transient try void volatile while true false null').split(' ')),
|
|
1476
|
+
c: C_RESERVED,
|
|
1477
|
+
cpp: new Set([...C_RESERVED, ...('bool catch class constexpr delete explicit false friend mutable namespace ' +
|
|
1478
|
+
'new noexcept nullptr operator private protected public template this throw true try typename using ' +
|
|
1479
|
+
'virtual wchar_t').split(' ')]),
|
|
1480
|
+
csharp: new Set(('abstract as base bool break byte case catch char checked class const continue decimal ' +
|
|
1481
|
+
'default delegate do double else enum event explicit extern false finally fixed float for foreach goto if ' +
|
|
1482
|
+
'implicit in int interface internal is lock long namespace new null object operator out override params ' +
|
|
1483
|
+
'private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct ' +
|
|
1484
|
+
'switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile ' +
|
|
1485
|
+
'while').split(' ')),
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1281
1488
|
function plan(index, name, options = {}) {
|
|
1282
1489
|
index._beginOp();
|
|
1283
1490
|
try {
|
|
@@ -1331,10 +1538,33 @@ function plan(index, name, options = {}) {
|
|
|
1331
1538
|
};
|
|
1332
1539
|
}
|
|
1333
1540
|
|
|
1541
|
+
// Rename-target sanity: a same-name rename is a 0-change request, and a
|
|
1542
|
+
// reserved word in the target's language would write syntax errors into
|
|
1543
|
+
// the declaration and every call site. (Identifier SHAPE is validated at
|
|
1544
|
+
// the execute layer; keywords need the resolved symbol's language.)
|
|
1545
|
+
if (options.renameTo) {
|
|
1546
|
+
if (options.renameTo === def.name || options.renameTo === name) {
|
|
1547
|
+
return {
|
|
1548
|
+
found: true,
|
|
1549
|
+
function: name,
|
|
1550
|
+
error: `renameTo "${options.renameTo}" matches the current name — nothing to rename.`,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
const reserved = RESERVED_WORDS_BY_LANGUAGE[planLang === 'html' ? 'javascript' : planLang];
|
|
1554
|
+
if (reserved && reserved.has(options.renameTo)) {
|
|
1555
|
+
return {
|
|
1556
|
+
found: true,
|
|
1557
|
+
function: name,
|
|
1558
|
+
error: `renameTo "${options.renameTo}" is a reserved word in ${planLang === 'html' ? 'javascript' : planLang} and cannot be used as an identifier.`,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1334
1563
|
let newParams = [...currentParams];
|
|
1335
1564
|
let newSignature = currentSignature;
|
|
1336
1565
|
let operation = null;
|
|
1337
1566
|
let changes = [];
|
|
1567
|
+
let unchangedSites = 0;
|
|
1338
1568
|
|
|
1339
1569
|
if (options.addParam) {
|
|
1340
1570
|
// Check if parameter already exists
|
|
@@ -1394,7 +1624,11 @@ function plan(index, name, options = {}) {
|
|
|
1394
1624
|
for (const site of planCallSites) {
|
|
1395
1625
|
let suggestion;
|
|
1396
1626
|
if (options.defaultValue && langHasDefaults) {
|
|
1397
|
-
|
|
1627
|
+
// The default makes the existing call valid. Keep the fact in
|
|
1628
|
+
// metadata, but do not inflate the concrete edit plan with a
|
|
1629
|
+
// no-op entry (UCN5-166).
|
|
1630
|
+
unchangedSites++;
|
|
1631
|
+
continue;
|
|
1398
1632
|
} else if (options.defaultValue) {
|
|
1399
1633
|
suggestion = `Add argument: ${options.defaultValue} (no default parameter values in ${planFileEntry?.language || 'this language'})`;
|
|
1400
1634
|
} else {
|
|
@@ -1405,7 +1639,8 @@ function plan(index, name, options = {}) {
|
|
|
1405
1639
|
line: site.line,
|
|
1406
1640
|
expression: site.expression,
|
|
1407
1641
|
suggestion,
|
|
1408
|
-
args: site.args
|
|
1642
|
+
args: site.args,
|
|
1643
|
+
editKind: 'call',
|
|
1409
1644
|
});
|
|
1410
1645
|
}
|
|
1411
1646
|
}
|
|
@@ -1462,7 +1697,8 @@ function plan(index, name, options = {}) {
|
|
|
1462
1697
|
line: site.line,
|
|
1463
1698
|
expression: site.expression,
|
|
1464
1699
|
suggestion: `Remove argument ${callerArgIndex + 1}: ${site.args[callerArgIndex] || '?'}`,
|
|
1465
|
-
args: site.args
|
|
1700
|
+
args: site.args,
|
|
1701
|
+
editKind: 'call',
|
|
1466
1702
|
});
|
|
1467
1703
|
} else if (!site.args) {
|
|
1468
1704
|
// Arguments unparseable (macro bodies, generated code) —
|
|
@@ -1473,7 +1709,8 @@ function plan(index, name, options = {}) {
|
|
|
1473
1709
|
line: site.line,
|
|
1474
1710
|
expression: site.expression,
|
|
1475
1711
|
suggestion: 'Could not parse arguments — review this call site manually',
|
|
1476
|
-
needsReview: true
|
|
1712
|
+
needsReview: true,
|
|
1713
|
+
editKind: 'call',
|
|
1477
1714
|
});
|
|
1478
1715
|
}
|
|
1479
1716
|
}
|
|
@@ -1489,21 +1726,63 @@ function plan(index, name, options = {}) {
|
|
|
1489
1726
|
// line appears ONCE however many call records it holds (fix #230 —
|
|
1490
1727
|
// the non-global regex left the inner call behind and emitted a
|
|
1491
1728
|
// duplicate entry per record).
|
|
1492
|
-
const
|
|
1729
|
+
const callLines = new Map();
|
|
1493
1730
|
for (const site of planCallSites) {
|
|
1494
1731
|
const lineKey = `${site.file}:${site.line}`;
|
|
1495
|
-
|
|
1496
|
-
renamedLines.add(lineKey);
|
|
1497
|
-
const newExpression = site.expression.replace(
|
|
1498
|
-
new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g'),
|
|
1499
|
-
options.renameTo
|
|
1500
|
-
);
|
|
1501
|
-
changes.push({
|
|
1732
|
+
const group = callLines.get(lineKey) || {
|
|
1502
1733
|
file: site.file,
|
|
1734
|
+
absoluteFile: site.absoluteFile,
|
|
1503
1735
|
line: site.line,
|
|
1504
1736
|
expression: site.expression,
|
|
1737
|
+
columns: [],
|
|
1738
|
+
missingColumn: false,
|
|
1739
|
+
callCount: 0,
|
|
1740
|
+
calledAs: site.calledAs,
|
|
1741
|
+
};
|
|
1742
|
+
if (group.calledAs !== site.calledAs) group.calledAs = null;
|
|
1743
|
+
group.callCount++;
|
|
1744
|
+
if (Number.isInteger(site.column)) group.columns.push(site.column);
|
|
1745
|
+
else group.missingColumn = true;
|
|
1746
|
+
callLines.set(lineKey, group);
|
|
1747
|
+
}
|
|
1748
|
+
for (const site of callLines.values()) {
|
|
1749
|
+
// A renamed import preserves its local alias (`old as local` /
|
|
1750
|
+
// `{ old: local }`). The caller engine carries the authored name
|
|
1751
|
+
// in calledAs; that token must remain unchanged while the import's
|
|
1752
|
+
// source-side identifier is edited below.
|
|
1753
|
+
if (site.calledAs && site.calledAs !== name) continue;
|
|
1754
|
+
const edit = renameIdentifierTokens(index,
|
|
1755
|
+
site.absoluteFile || site.file, site.line, name,
|
|
1756
|
+
options.renameTo, site.missingColumn ? [] : site.columns,
|
|
1757
|
+
site.callCount);
|
|
1758
|
+
const newExpression = edit.renamed;
|
|
1759
|
+
// A confirmed call through an import alias (`xf()`) is a real
|
|
1760
|
+
// caller but the alias spelling does not change. The required
|
|
1761
|
+
// edit is the import's source-side name; never emit a byte-for-
|
|
1762
|
+
// byte no-op that makes the plan look complete.
|
|
1763
|
+
if (newExpression === edit.source) {
|
|
1764
|
+
// Missing parser columns mean UCN can prove the edit is
|
|
1765
|
+
// required but cannot safely synthesize it. Never fall back
|
|
1766
|
+
// to whole-line regex replacement.
|
|
1767
|
+
if (site.missingColumn) {
|
|
1768
|
+
changes.push({
|
|
1769
|
+
file: site.file,
|
|
1770
|
+
line: site.line,
|
|
1771
|
+
expression: edit.source,
|
|
1772
|
+
suggestion: `Rename call identifier "${name}" to "${options.renameTo}" manually`,
|
|
1773
|
+
needsReview: true,
|
|
1774
|
+
editKind: 'call',
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
changes.push({
|
|
1780
|
+
file: site.file,
|
|
1781
|
+
line: site.line,
|
|
1782
|
+
expression: edit.source,
|
|
1505
1783
|
suggestion: `Rename to: ${newExpression}`,
|
|
1506
|
-
newExpression
|
|
1784
|
+
newExpression,
|
|
1785
|
+
editKind: 'call',
|
|
1507
1786
|
});
|
|
1508
1787
|
}
|
|
1509
1788
|
|
|
@@ -1530,21 +1809,236 @@ function plan(index, name, options = {}) {
|
|
|
1530
1809
|
_nameBindingReaches(index, imp.file, name, renameTargetFiles) === 'no') {
|
|
1531
1810
|
continue;
|
|
1532
1811
|
}
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
);
|
|
1812
|
+
const edit = renameIdentifierTokens(index, imp.file,
|
|
1813
|
+
imp.line, name, options.renameTo);
|
|
1814
|
+
const newImport = edit.renamed;
|
|
1815
|
+
if (newImport === edit.source) continue;
|
|
1537
1816
|
changes.push({
|
|
1538
1817
|
file: imp.relativePath || imp.file,
|
|
1539
1818
|
line: imp.line,
|
|
1540
|
-
expression:
|
|
1819
|
+
expression: edit.source,
|
|
1541
1820
|
suggestion: `Update import: ${newImport}`,
|
|
1542
1821
|
newExpression: newImport,
|
|
1543
|
-
isImport: true
|
|
1822
|
+
isImport: true,
|
|
1823
|
+
editKind: 'import',
|
|
1544
1824
|
});
|
|
1545
1825
|
}
|
|
1826
|
+
|
|
1827
|
+
// Renamed CJS/Python imports are intentionally surfaced as reference
|
|
1828
|
+
// usages by their parsers, so usageType alone cannot find the import
|
|
1829
|
+
// line. importBindings retains the original/local pair and line.
|
|
1830
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
1831
|
+
for (const binding of fileEntry.importBindings || []) {
|
|
1832
|
+
const localAlias = binding.alias || (fileEntry.importAliases || [])
|
|
1833
|
+
.find(alias => alias.original === binding.name)?.local;
|
|
1834
|
+
if (binding.name !== name || !localAlias ||
|
|
1835
|
+
localAlias === name || !binding.line) continue;
|
|
1836
|
+
if (_nameBindingReaches(index, filePath, name,
|
|
1837
|
+
renameTargetFiles) === 'no') continue;
|
|
1838
|
+
const rel = fileEntry.relativePath || filePath;
|
|
1839
|
+
if (changes.some(change =>
|
|
1840
|
+
change.file === rel && change.line === binding.line)) continue;
|
|
1841
|
+
const edit = renameIdentifierTokens(index, filePath,
|
|
1842
|
+
binding.line, name, options.renameTo);
|
|
1843
|
+
const sourceLine = edit.source;
|
|
1844
|
+
const newImport = edit.renamed;
|
|
1845
|
+
if (newImport === sourceLine) continue;
|
|
1846
|
+
changes.push({
|
|
1847
|
+
file: rel,
|
|
1848
|
+
line: binding.line,
|
|
1849
|
+
expression: sourceLine,
|
|
1850
|
+
suggestion: `Update import: ${newImport}`,
|
|
1851
|
+
newExpression: newImport,
|
|
1852
|
+
isImport: true,
|
|
1853
|
+
editKind: 'import',
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Export surfaces owned by the selected definition are declaration
|
|
1859
|
+
// references too. In particular, CommonJS shorthand
|
|
1860
|
+
// `module.exports = { helper }` must be renamed or the otherwise
|
|
1861
|
+
// complete caller/import edit set breaks the module API.
|
|
1862
|
+
for (const [exportPath, targetEntry] of index.files) {
|
|
1863
|
+
for (const exported of targetEntry.exportDetails || []) {
|
|
1864
|
+
if ((exported.name !== name && exported.alias !== name) ||
|
|
1865
|
+
!exported.line) continue;
|
|
1866
|
+
const ownership = _nameBindingReaches(
|
|
1867
|
+
index, exportPath, name, renameTargetFiles, 8);
|
|
1868
|
+
// Transitive file reachability is not name ownership. A file
|
|
1869
|
+
// can import the target somewhere in its dependency closure
|
|
1870
|
+
// while exporting its own same-spelled local declaration.
|
|
1871
|
+
// Edit the target's own export, or a positively-resolved
|
|
1872
|
+
// re-export chain; unknown CJS/dynamic surfaces are unsafe to
|
|
1873
|
+
// rewrite mechanically.
|
|
1874
|
+
if (exportPath !== def.file && ownership !== 'yes') continue;
|
|
1875
|
+
const exportFile = targetEntry.relativePath || exportPath;
|
|
1876
|
+
if (changes.some(change =>
|
|
1877
|
+
change.file === exportFile && change.line === exported.line)) {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
const edit = renameIdentifierTokens(index, exportPath,
|
|
1881
|
+
exported.line, name, options.renameTo);
|
|
1882
|
+
const sourceLine = edit.source;
|
|
1883
|
+
const occurrences = edit.count;
|
|
1884
|
+
const newExpression = edit.renamed;
|
|
1885
|
+
if (newExpression === sourceLine) continue;
|
|
1886
|
+
changes.push({
|
|
1887
|
+
file: exportFile,
|
|
1888
|
+
line: exported.line,
|
|
1889
|
+
expression: sourceLine,
|
|
1890
|
+
suggestion: `Update export: ${newExpression}`,
|
|
1891
|
+
newExpression,
|
|
1892
|
+
isExport: true,
|
|
1893
|
+
editKind: 'export',
|
|
1894
|
+
...(occurrences > 1 && { needsReview: true }),
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// Renaming a virtual/overridden member is one hierarchy-wide change.
|
|
1900
|
+
// Leaving descendant declarations behind either fails compilation
|
|
1901
|
+
// (Java/C#/TS override) or silently changes dispatch (Python/JS).
|
|
1902
|
+
if (def.className) {
|
|
1903
|
+
// Inheritance identity is (class name, defining file), not just
|
|
1904
|
+
// the spelling. Repositories routinely contain two Handler/Base
|
|
1905
|
+
// classes in unrelated packages. Follow only children whose base
|
|
1906
|
+
// resolves from the child's scope to this exact parent file.
|
|
1907
|
+
const identityKey = (className, file) => `${file || ''}\0${className}`;
|
|
1908
|
+
const descendants = new Map();
|
|
1909
|
+
const queue = [{ name: def.className, file: def.file }];
|
|
1910
|
+
const visited = new Set([identityKey(def.className, def.file)]);
|
|
1911
|
+
while (queue.length > 0 && descendants.size < 5000) {
|
|
1912
|
+
const parent = queue.shift();
|
|
1913
|
+
for (const child of index.extendedByGraph.get(parent.name) || []) {
|
|
1914
|
+
const childName = typeof child === 'string' ? child : child.name;
|
|
1915
|
+
const childFile = typeof child === 'string'
|
|
1916
|
+
? index._resolveClassFile(childName, parent.file)
|
|
1917
|
+
: child.file;
|
|
1918
|
+
if (!childName || !childFile) continue;
|
|
1919
|
+
const parentFile = index._resolveClassFile(parent.name, childFile);
|
|
1920
|
+
if (parentFile !== parent.file) continue;
|
|
1921
|
+
const key = identityKey(childName, childFile);
|
|
1922
|
+
if (visited.has(key)) continue;
|
|
1923
|
+
visited.add(key);
|
|
1924
|
+
descendants.set(key, { name: childName, file: childFile });
|
|
1925
|
+
queue.push({ name: childName, file: childFile });
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
for (const override of index.symbols.get(name) || []) {
|
|
1929
|
+
if (!override.className ||
|
|
1930
|
+
!descendants.has(identityKey(override.className, override.file))) continue;
|
|
1931
|
+
const line = override.nameLine || override.startLine;
|
|
1932
|
+
const rel = override.relativePath || override.file;
|
|
1933
|
+
if (changes.some(change => change.file === rel && change.line === line)) continue;
|
|
1934
|
+
const edit = renameIdentifierTokens(index, override.file,
|
|
1935
|
+
line, name, options.renameTo);
|
|
1936
|
+
const sourceLine = edit.source;
|
|
1937
|
+
const newExpression = edit.renamed;
|
|
1938
|
+
if (newExpression === sourceLine) continue;
|
|
1939
|
+
changes.push({
|
|
1940
|
+
file: rel,
|
|
1941
|
+
line,
|
|
1942
|
+
expression: sourceLine,
|
|
1943
|
+
suggestion: `Update overriding definition: ${newExpression}`,
|
|
1944
|
+
newExpression,
|
|
1945
|
+
isDefinition: true,
|
|
1946
|
+
editKind: 'definition',
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// C/C++ declarations and definitions are one compiler symbol. A
|
|
1952
|
+
// selected implementation must carry its matching header prototype,
|
|
1953
|
+
// and selecting the prototype must carry the implementation.
|
|
1954
|
+
if (planLang === 'c' || planLang === 'cpp') {
|
|
1955
|
+
const ownerOf = symbol => symbol.className ||
|
|
1956
|
+
(symbol.receiver || '').replace(/^\*/, '') || null;
|
|
1957
|
+
const signatureOf = symbol => (symbol.paramsStructured || []).map(param =>
|
|
1958
|
+
String(param.type || param.name || '').replace(/\s+/g, '')).join(',');
|
|
1959
|
+
const linked = candidate => candidate.file === def.file ||
|
|
1960
|
+
index.importGraph.get(def.file)?.has(candidate.file) ||
|
|
1961
|
+
index.importGraph.get(candidate.file)?.has(def.file);
|
|
1962
|
+
for (const sibling of index.symbols.get(name) || []) {
|
|
1963
|
+
if (sibling === def || ownerOf(sibling) !== ownerOf(def) ||
|
|
1964
|
+
signatureOf(sibling) !== signatureOf(def) ||
|
|
1965
|
+
!(sibling.isSignature || def.isSignature) || !linked(sibling)) continue;
|
|
1966
|
+
const siblingLang = index.files.get(sibling.file)?.language;
|
|
1967
|
+
if (siblingLang !== planLang &&
|
|
1968
|
+
!new Set(['c', 'cpp']).has(siblingLang)) continue;
|
|
1969
|
+
const line = sibling.nameLine || sibling.startLine;
|
|
1970
|
+
const rel = sibling.relativePath || sibling.file;
|
|
1971
|
+
if (changes.some(change => change.file === rel && change.line === line)) continue;
|
|
1972
|
+
const edit = renameIdentifierTokens(index, sibling.file,
|
|
1973
|
+
line, name, options.renameTo);
|
|
1974
|
+
if (edit.renamed === edit.source) continue;
|
|
1975
|
+
changes.push({
|
|
1976
|
+
file: rel,
|
|
1977
|
+
line,
|
|
1978
|
+
expression: edit.source,
|
|
1979
|
+
suggestion: `Update paired declaration: ${edit.renamed}`,
|
|
1980
|
+
newExpression: edit.renamed,
|
|
1981
|
+
isDefinition: true,
|
|
1982
|
+
editKind: 'definition',
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Every operation changes the selected declaration. Historically `plan`
|
|
1989
|
+
// only listed callers/imports in changes[], while rendering the new
|
|
1990
|
+
// signature separately. An agent applying the advertised edit array
|
|
1991
|
+
// therefore produced uncompilable code. Keep the declaration in the same
|
|
1992
|
+
// concrete list and count as every other required edit.
|
|
1993
|
+
const definitionLine = def.nameLine || def.startLine;
|
|
1994
|
+
const definitionFile = def.relativePath || def.file;
|
|
1995
|
+
const definitionSource = index.getLineContent(def.file, definitionLine).trim();
|
|
1996
|
+
const existingDefinitionLine = changes.find(change =>
|
|
1997
|
+
change.file === definitionFile && change.line === definitionLine);
|
|
1998
|
+
if (existingDefinitionLine) {
|
|
1999
|
+
existingDefinitionLine.isDefinition = true;
|
|
2000
|
+
existingDefinitionLine.editKind = 'definition';
|
|
2001
|
+
if (options.renameTo) {
|
|
2002
|
+
const renamed = renameIdentifierTokens(index, def.file,
|
|
2003
|
+
definitionLine, name, options.renameTo).renamed;
|
|
2004
|
+
existingDefinitionLine.newExpression = renamed;
|
|
2005
|
+
existingDefinitionLine.suggestion = `Update definition: ${renamed}`;
|
|
2006
|
+
}
|
|
2007
|
+
} else {
|
|
2008
|
+
const definitionChange = {
|
|
2009
|
+
file: definitionFile,
|
|
2010
|
+
line: definitionLine,
|
|
2011
|
+
expression: definitionSource,
|
|
2012
|
+
isDefinition: true,
|
|
2013
|
+
editKind: 'definition',
|
|
2014
|
+
};
|
|
2015
|
+
if (options.renameTo) {
|
|
2016
|
+
const renamed = renameIdentifierTokens(index, def.file,
|
|
2017
|
+
definitionLine, name, options.renameTo).renamed;
|
|
2018
|
+
definitionChange.newExpression = renamed;
|
|
2019
|
+
definitionChange.suggestion = `Update definition: ${renamed}`;
|
|
2020
|
+
} else {
|
|
2021
|
+
definitionChange.suggestion =
|
|
2022
|
+
`Update declaration signature to: ${newSignature}`;
|
|
2023
|
+
// Signature layouts can span multiple lines and differ by
|
|
2024
|
+
// language. The AST proves this edit is required, while the
|
|
2025
|
+
// preview explicitly withholds a fake one-line replacement.
|
|
2026
|
+
definitionChange.needsReview = true;
|
|
2027
|
+
}
|
|
2028
|
+
changes.unshift(definitionChange);
|
|
1546
2029
|
}
|
|
1547
2030
|
|
|
2031
|
+
const changeSummary = {
|
|
2032
|
+
// editKind is a partition: a public declaration that is also an
|
|
2033
|
+
// export remains one definition edit, never two summary buckets.
|
|
2034
|
+
definitions: changes.filter(change => change.editKind === 'definition').length,
|
|
2035
|
+
calls: changes.filter(change => change.editKind === 'call' ||
|
|
2036
|
+
!change.editKind).length,
|
|
2037
|
+
imports: changes.filter(change => change.editKind === 'import').length,
|
|
2038
|
+
exports: changes.filter(change => change.editKind === 'export').length,
|
|
2039
|
+
reviewRequired: changes.filter(change => change.needsReview).length,
|
|
2040
|
+
};
|
|
2041
|
+
|
|
1548
2042
|
return {
|
|
1549
2043
|
found: true,
|
|
1550
2044
|
function: name,
|
|
@@ -1564,13 +2058,16 @@ function plan(index, name, options = {}) {
|
|
|
1564
2058
|
},
|
|
1565
2059
|
totalChanges: changes.length,
|
|
1566
2060
|
filesAffected: new Set(changes.map(c => c.file)).size,
|
|
2061
|
+
changeSummary,
|
|
1567
2062
|
changes,
|
|
2063
|
+
...(unchangedSites > 0 && { unchangedSites }),
|
|
1568
2064
|
// v4 tiered contract: sites that MAY also need this change but lack
|
|
1569
2065
|
// binding/receiver evidence — review manually before refactoring.
|
|
1570
2066
|
unverifiedCount: planUnverified.length,
|
|
1571
2067
|
unverifiedSites: planUnverified,
|
|
1572
2068
|
account: planAccount,
|
|
1573
|
-
scopeWarning: impactScopeWarning
|
|
2069
|
+
scopeWarning: impactScopeWarning,
|
|
2070
|
+
...(resolved.warnings.length > 0 && { warnings: resolved.warnings }),
|
|
1574
2071
|
};
|
|
1575
2072
|
} finally { index._endOp(); }
|
|
1576
2073
|
}
|