ucn 5.4.2 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/verify.js
CHANGED
|
@@ -7,9 +7,29 @@
|
|
|
7
7
|
|
|
8
8
|
const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits } = require('../languages');
|
|
9
9
|
const { sameNode } = require('../languages/utils');
|
|
10
|
-
const { escapeRegExp, codeUnitCompare, NON_CALLABLE_TYPES } = require('./shared');
|
|
11
|
-
const {
|
|
10
|
+
const { escapeRegExp, codeUnitCompare, NON_CALLABLE_TYPES, isOverrideMarked, isMacroNamespaceDefinition } = require('./shared');
|
|
11
|
+
const { classDefsNamed, distinctOwnerDefinitions } = require('./class-identity');
|
|
12
|
+
const { isTypeRenamePin } = require('./type-references');
|
|
13
|
+
const { findAccessorReferences, isAccessorDefinition } = require('./accessors');
|
|
12
14
|
const { validateCallMismatch, declarationIdentity } = require('./provenance');
|
|
15
|
+
const { UcnError } = require('./errors');
|
|
16
|
+
const { execFileSync, spawn } = require('child_process');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The string index of a token that a parser column points at. The
|
|
23
|
+
* tree-sitter binding reports columns of string input in UTF-16 code units;
|
|
24
|
+
* a column that does not spell the token there is read as a UTF-8 byte
|
|
25
|
+
* column (fix #386: tokens after a non-ASCII character on the same line
|
|
26
|
+
* were skipped).
|
|
27
|
+
*/
|
|
28
|
+
function tokenColumn(line, column, token) {
|
|
29
|
+
if (!Number.isInteger(column) || column < 0) return null;
|
|
30
|
+
if (token && line.slice(column, column + token.length) === token) return column;
|
|
31
|
+
return codeUnitColumnForByteColumn(line, column);
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
function codeUnitColumnForByteColumn(line, byteColumn) {
|
|
15
35
|
if (!Number.isInteger(byteColumn) || byteColumn < 0) return null;
|
|
@@ -25,6 +45,101 @@ function codeUnitColumnForByteColumn(line, byteColumn) {
|
|
|
25
45
|
return null;
|
|
26
46
|
}
|
|
27
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Choose the declaration's own name token among name-field identifiers on a
|
|
50
|
+
* definition line (fix #359). Candidates carry `{column, decl}` where decl is
|
|
51
|
+
* the node whose name/declarator field the identifier is.
|
|
52
|
+
* 1. The declaration must end where the indexed definition ends (when the
|
|
53
|
+
* definition's endLine is known): a multi-line body's nested parameter
|
|
54
|
+
* or type declarations end earlier.
|
|
55
|
+
* 2. A member definition (className set) whose class lexically encloses it
|
|
56
|
+
* binds the candidate INSIDE that class (a one-line `class A { A() {} }`
|
|
57
|
+
* or `type T struct { T int }` pins the member, not the container).
|
|
58
|
+
* 3. Otherwise the outermost declaration wins: parameters, named results
|
|
59
|
+
* and qualified type names are always nested inside the declaration
|
|
60
|
+
* they belong to.
|
|
61
|
+
*/
|
|
62
|
+
function pickDeclarationNameToken(candidates, definition) {
|
|
63
|
+
if (!candidates || candidates.length === 0) return null;
|
|
64
|
+
let pool = candidates;
|
|
65
|
+
if (definition && Number.isInteger(definition.endLine)) {
|
|
66
|
+
const spanMatch = pool.filter(c => c.decl.endPosition.row + 1 === definition.endLine);
|
|
67
|
+
if (spanMatch.length > 0) pool = spanMatch;
|
|
68
|
+
}
|
|
69
|
+
const className = definition && definition.className;
|
|
70
|
+
if (className) {
|
|
71
|
+
const inside = pool.filter(c => {
|
|
72
|
+
for (let anc = c.decl.parent, depth = 0; anc && depth < 12;
|
|
73
|
+
anc = anc.parent, depth++) {
|
|
74
|
+
const ancName = anc.childForFieldName?.('name');
|
|
75
|
+
if (ancName && ancName.text === className &&
|
|
76
|
+
!sameNode(anc, c.decl)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
if (inside.length > 0) pool = inside;
|
|
81
|
+
}
|
|
82
|
+
let best = null;
|
|
83
|
+
for (const c of pool) {
|
|
84
|
+
if (!best) { best = c; continue; }
|
|
85
|
+
const a = c.decl, b = best.decl;
|
|
86
|
+
if (a.startIndex <= b.startIndex && a.endIndex >= b.endIndex &&
|
|
87
|
+
(a.startIndex < b.startIndex || a.endIndex > b.endIndex)) {
|
|
88
|
+
best = c;
|
|
89
|
+
} else if (!(b.startIndex <= a.startIndex && b.endIndex >= a.endIndex) &&
|
|
90
|
+
c.column < best.column) {
|
|
91
|
+
// Disjoint declarations: earliest on the line.
|
|
92
|
+
best = c;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return best ? best.column : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Byte columns of `NAME` tokens on one line that are member accesses on the
|
|
100
|
+
* enclosing instance or class receiver (`this.NAME`, `self.NAME`,
|
|
101
|
+
* `cls.NAME`, `Self::NAME`), by AST: the token is the member part of its
|
|
102
|
+
* parent node and the parent's receiver part is a receiver keyword of the
|
|
103
|
+
* language (traits.selfParam) or a `this`/`self` node (fix #376).
|
|
104
|
+
*/
|
|
105
|
+
function selfMemberTokenColumns(index, filePath, lineNumber, name) {
|
|
106
|
+
const path = require('path');
|
|
107
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.join(index.root, filePath);
|
|
108
|
+
const language = index.files.get(absolute)?.language;
|
|
109
|
+
const parser = language && getParser(language);
|
|
110
|
+
if (!parser) return [];
|
|
111
|
+
let content;
|
|
112
|
+
try { content = index._readFile(absolute); } catch { return []; }
|
|
113
|
+
const tree = index._getParsedTree?.(absolute, content, language) || safeParse(parser, content);
|
|
114
|
+
if (!tree) return [];
|
|
115
|
+
const receivers = new Set([...(langTraits(language)?.selfParam || []), 'this', 'self', 'Self']);
|
|
116
|
+
const row = lineNumber - 1;
|
|
117
|
+
const out = [];
|
|
118
|
+
const stack = [tree.rootNode];
|
|
119
|
+
while (stack.length > 0) {
|
|
120
|
+
const node = stack.pop();
|
|
121
|
+
if (node.endPosition.row < row || node.startPosition.row > row) continue;
|
|
122
|
+
if (node.startPosition.row === row && node.text === name &&
|
|
123
|
+
/identifier$/.test(node.type) && node.parent) {
|
|
124
|
+
const parent = node.parent;
|
|
125
|
+
const receiver = parent.namedChild(0);
|
|
126
|
+
if (receiver && !sameNode(receiver, node) &&
|
|
127
|
+
parent.namedChildCount === 2 && sameNode(parent.namedChild(1), node) &&
|
|
128
|
+
receivers.has(receiver.text.replace(/\s+/g, ''))) {
|
|
129
|
+
out.push(node.startPosition.column);
|
|
130
|
+
} else if (parent.type === 'member_access_expression' &&
|
|
131
|
+
sameNode(parent.childForFieldName('name'), node) &&
|
|
132
|
+
receivers.has(String(parent.childForFieldName('expression')?.text || '').replace(/\s+/g, ''))) {
|
|
133
|
+
// C# `this.m`: the keyword receiver is an anonymous node.
|
|
134
|
+
out.push(node.startPosition.column);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (let i = 0; i < node.namedChildCount; i++) stack.push(node.namedChild(i));
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
28
143
|
/** Replace only AST identifier tokens on one source line. */
|
|
29
144
|
function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
30
145
|
preferredByteColumns = null, expectedCallCount = null, tokenOptions = {}) {
|
|
@@ -77,9 +192,15 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
77
192
|
// Wrapper identity via sameNode (#233 — wrappers are
|
|
78
193
|
// not reference-stable across walks).
|
|
79
194
|
if (tokenOptions.definitionNameOnly && node.parent) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
195
|
+
const decl = node.parent;
|
|
196
|
+
const nameChild = decl.childForFieldName?.('name');
|
|
197
|
+
const declaratorChild = decl.childForFieldName?.('declarator');
|
|
198
|
+
if ((nameChild && sameNode(nameChild, node)) ||
|
|
199
|
+
(declaratorChild && sameNode(declaratorChild, node))) {
|
|
200
|
+
defNameByteColumns.push({
|
|
201
|
+
column: node.startPosition.column,
|
|
202
|
+
decl,
|
|
203
|
+
});
|
|
83
204
|
}
|
|
84
205
|
}
|
|
85
206
|
}
|
|
@@ -95,14 +216,20 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
95
216
|
// BuildVarsFunc) *Route`) — renaming every token breaks the type
|
|
96
217
|
// reference. Rename only the definition's own NAME token (AST name field;
|
|
97
218
|
// first occurrence as fallback) — javac/gopls rename semantics.
|
|
219
|
+
//
|
|
220
|
+
// Declaration identity (fix #359, gin-measured): several identifiers on
|
|
221
|
+
// a definition line can be their parent's name field (a Go named result
|
|
222
|
+
// `(head string)`, a qualified return type `http.Pusher`, a Java/C
|
|
223
|
+
// parameter declarator). The definition token is the name of the
|
|
224
|
+
// symbol's OWN declaration node, never a nested declaration's name.
|
|
98
225
|
if (tokenOptions.definitionNameOnly && byteColumns.length > 1) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
226
|
+
const chosen = pickDeclarationNameToken(defNameByteColumns,
|
|
227
|
+
tokenOptions.definition || null);
|
|
228
|
+
byteColumns = chosen != null ? [chosen] : [Math.min(...byteColumns)];
|
|
102
229
|
}
|
|
103
230
|
|
|
104
231
|
const columns = [...new Set(byteColumns
|
|
105
|
-
.map(column =>
|
|
232
|
+
.map(column => tokenColumn(sourceLine, column, oldName))
|
|
106
233
|
.filter(Number.isInteger))].sort((a, b) => b - a);
|
|
107
234
|
if (expectedCallCount != null && columns.length !== expectedCallCount) {
|
|
108
235
|
return { source: sourceLine.trim(), renamed: sourceLine.trim(), count: 0 };
|
|
@@ -113,7 +240,102 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
113
240
|
renamed = renamed.slice(0, column) + newName +
|
|
114
241
|
renamed.slice(column + oldName.length);
|
|
115
242
|
}
|
|
116
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
source: sourceLine.trim(), renamed: renamed.trim(), count: columns.length,
|
|
245
|
+
columns, rawSource: sourceLine, oldName, newName,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The column of the called name inside one call's source range (fix #392):
|
|
251
|
+
* two same-name calls on a line (`a.run(b.run())`) are two tokens, and the
|
|
252
|
+
* record of each call spans its own call node. Returns null when the range
|
|
253
|
+
* does not resolve to one name token on the line.
|
|
254
|
+
*/
|
|
255
|
+
function callTokenColumn(index, filePath, range, name, line) {
|
|
256
|
+
if (!range || !filePath) return null;
|
|
257
|
+
const absolute = require('path').isAbsolute(filePath)
|
|
258
|
+
? filePath : require('path').join(index.root, filePath);
|
|
259
|
+
const language = index.files.get(absolute)?.language;
|
|
260
|
+
const parser = language && getParser(language);
|
|
261
|
+
if (!parser) return null;
|
|
262
|
+
let content;
|
|
263
|
+
try { content = index._readFile(absolute); } catch { return null; }
|
|
264
|
+
const tree = index._getParsedTree?.(absolute, content, language) || safeParse(parser, content);
|
|
265
|
+
if (!tree) return null;
|
|
266
|
+
let node = tree.rootNode.descendantForIndex(range.start, Math.max(range.start, range.end - 1));
|
|
267
|
+
while (node && !(node.startIndex <= range.start && node.endIndex >= range.end)) node = node.parent;
|
|
268
|
+
if (!node) return null;
|
|
269
|
+
// A call spelled inside a Python string annotation (fix #398F): the
|
|
270
|
+
// parser read it from the annotation's expression; the range is the
|
|
271
|
+
// called name inside the string's content.
|
|
272
|
+
if (node.type === 'string_content' && content.slice(range.start, range.end) === name &&
|
|
273
|
+
node.startPosition.row === line - 1 && node.endPosition.row === line - 1) {
|
|
274
|
+
return node.startPosition.column + (range.start - node.startIndex);
|
|
275
|
+
}
|
|
276
|
+
const isName = n => n && n.text === name && /identifier$/.test(n.type) &&
|
|
277
|
+
n.startPosition.row === line - 1;
|
|
278
|
+
if (isName(node)) return node.startPosition.column;
|
|
279
|
+
// The callee part of the call node: its last same-name token outside
|
|
280
|
+
// the argument list names the call (`x.run(y.run())` -> the outer run).
|
|
281
|
+
const callee = node.childForFieldName?.('function') || node.childForFieldName?.('name') ||
|
|
282
|
+
node.childForFieldName?.('method') || node.childForFieldName?.('constructor') ||
|
|
283
|
+
node.childForFieldName?.('macro') || null;
|
|
284
|
+
if (!callee) return null;
|
|
285
|
+
if (isName(callee)) return callee.startPosition.column;
|
|
286
|
+
const named = ['property', 'attribute', 'field', 'name'];
|
|
287
|
+
for (const field of named) {
|
|
288
|
+
const part = callee.childForFieldName?.(field);
|
|
289
|
+
if (isName(part)) return part.startPosition.column;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Merge a second token edit into an existing change on the same line
|
|
296
|
+
* (fix #376): a one-line method whose body calls a same-named slot member
|
|
297
|
+
* (`@Override public String m() { return delegate.m(); }`) needs its
|
|
298
|
+
* declaration token and its call token renamed in ONE line edit. Changes
|
|
299
|
+
* carry their token edit as a non-enumerable `_edit`.
|
|
300
|
+
*/
|
|
301
|
+
function mergeTokenEdit(change, edit) {
|
|
302
|
+
const prior = change?._edit;
|
|
303
|
+
if (!prior || !edit || prior.rawSource !== edit.rawSource ||
|
|
304
|
+
prior.oldName !== edit.oldName || prior.newName !== edit.newName) return false;
|
|
305
|
+
const columns = [...new Set([...prior.columns, ...edit.columns])].sort((a, b) => b - a);
|
|
306
|
+
if (columns.length === prior.columns.length) return true;
|
|
307
|
+
let renamed = prior.rawSource;
|
|
308
|
+
for (const column of columns) {
|
|
309
|
+
if (renamed.slice(column, column + prior.oldName.length) !== prior.oldName) continue;
|
|
310
|
+
renamed = renamed.slice(0, column) + prior.newName + renamed.slice(column + prior.oldName.length);
|
|
311
|
+
}
|
|
312
|
+
const previous = change.newExpression;
|
|
313
|
+
change.newExpression = renamed.trim();
|
|
314
|
+
if (typeof change.suggestion === 'string' && previous) {
|
|
315
|
+
change.suggestion = change.suggestion.replace(previous, change.newExpression);
|
|
316
|
+
}
|
|
317
|
+
tagTokenEdit(change, { ...prior, columns, renamed: change.newExpression });
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Mark a change whose tokens a dedicated pass proved (a macro argument that
|
|
323
|
+
* declares the renamed class, an exactly resolved using-declaration): the
|
|
324
|
+
* type-reference pass keeps them even where its own lookup is unsure.
|
|
325
|
+
*/
|
|
326
|
+
function markEvidence(change) {
|
|
327
|
+
Object.defineProperty(change, '_evidence', {
|
|
328
|
+
value: true, enumerable: false, writable: true, configurable: true,
|
|
329
|
+
});
|
|
330
|
+
return change;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function tagTokenEdit(change, edit) {
|
|
334
|
+
if (!change || !edit || !Array.isArray(edit.columns)) return change;
|
|
335
|
+
Object.defineProperty(change, '_edit', {
|
|
336
|
+
value: edit, enumerable: false, writable: true, configurable: true,
|
|
337
|
+
});
|
|
338
|
+
return change;
|
|
117
339
|
}
|
|
118
340
|
|
|
119
341
|
// ============================================================================
|
|
@@ -335,8 +557,20 @@ function _collectCallNodes(node, callTypes, targetRow, funcName, limit, out = []
|
|
|
335
557
|
|
|
336
558
|
// Recurse into children — nested same-name calls (`greet(greet(x))`)
|
|
337
559
|
// are separate records, so a match's children are still scanned.
|
|
338
|
-
|
|
339
|
-
|
|
560
|
+
// Children are in source order: binary-search the first one that can
|
|
561
|
+
// reach the target row instead of visiting every earlier sibling.
|
|
562
|
+
const count = node.childCount;
|
|
563
|
+
let lo = 0;
|
|
564
|
+
let hi = count;
|
|
565
|
+
while (lo < hi) {
|
|
566
|
+
const mid = (lo + hi) >> 1;
|
|
567
|
+
if (node.child(mid).endPosition.row < targetRow) lo = mid + 1;
|
|
568
|
+
else hi = mid;
|
|
569
|
+
}
|
|
570
|
+
for (let i = lo; i < count; i++) {
|
|
571
|
+
const child = node.child(i);
|
|
572
|
+
if (child.startPosition.row > targetRow) break;
|
|
573
|
+
_collectCallNodes(child, callTypes, targetRow, funcName, limit, out);
|
|
340
574
|
if (out.length >= limit) return out;
|
|
341
575
|
}
|
|
342
576
|
return out;
|
|
@@ -712,7 +946,7 @@ function _constructorParamLists(index, def, lang) {
|
|
|
712
946
|
* @param {object} def - Resolved definition (pinned target)
|
|
713
947
|
* @returns {{ confirmed: Array, unverified: Array, account: object }}
|
|
714
948
|
*/
|
|
715
|
-
function contractedCallerSweep(index, name, def) {
|
|
949
|
+
function contractedCallerSweep(index, name, def, options = {}) {
|
|
716
950
|
const rawCallers = index.findCallers(name, {
|
|
717
951
|
includeMethods: true,
|
|
718
952
|
targetDefinitions: [def],
|
|
@@ -728,11 +962,38 @@ function contractedCallerSweep(index, name, def) {
|
|
|
728
962
|
for (const u of rawCallers.unverifiedEntries || []) {
|
|
729
963
|
unverified.push(u);
|
|
730
964
|
}
|
|
965
|
+
// Call sites that bound the definition's base declaration and that the
|
|
966
|
+
// sweep of the changed declaration excludes (fix #394): they leave the
|
|
967
|
+
// excluded bucket and join the unverified band, to be checked against
|
|
968
|
+
// the new signature.
|
|
969
|
+
const rebound = options.baseChanges
|
|
970
|
+
? sitesReboundByChange(index, name, def, rawCallers, confirmed, unverified, options.baseChanges)
|
|
971
|
+
: [];
|
|
972
|
+
if (rebound.length > 0 && rawCallers.accountRaw) {
|
|
973
|
+
const moved = new Set(rebound.filter(site => !site.stillUnverified)
|
|
974
|
+
.map(site => `${site.file}\0${site.line}`));
|
|
975
|
+
rawCallers.accountRaw = {
|
|
976
|
+
...rawCallers.accountRaw,
|
|
977
|
+
excludedEntries: rawCallers.accountRaw.excludedEntries.filter(entry =>
|
|
978
|
+
!moved.has(`${entry.file}\0${entry.line}`)),
|
|
979
|
+
};
|
|
980
|
+
for (const site of rebound) {
|
|
981
|
+
// A site already in the band is replaced by its checked record.
|
|
982
|
+
const at = site.stillUnverified ? unverified.findIndex(u => u.file === site.file &&
|
|
983
|
+
u.line === site.line && !u.rebind && (site.column == null || u.column == null || u.column === site.column)) : -1;
|
|
984
|
+
if (at >= 0) unverified[at] = site;
|
|
985
|
+
else unverified.push(site);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
731
988
|
|
|
732
989
|
// Conservation account from the sweep's claims (impact's manual
|
|
733
990
|
// composition).
|
|
734
991
|
const { computeGroundSet, buildAccount } = require('./account');
|
|
735
|
-
|
|
992
|
+
let groundSet = index._opGroundSetCache?.get(name);
|
|
993
|
+
if (!groundSet) {
|
|
994
|
+
groundSet = computeGroundSet(index, name);
|
|
995
|
+
index._opGroundSetCache?.set(name, groundSet);
|
|
996
|
+
}
|
|
736
997
|
const accountRaw = rawCallers.accountRaw || { unverifiedLines: [], excludedEntries: [] };
|
|
737
998
|
const confirmedEntries = confirmed.map(c => ({ file: c.file, line: c.line }));
|
|
738
999
|
const unverifiedEntries = [
|
|
@@ -760,7 +1021,295 @@ function contractedCallerSweep(index, name, def) {
|
|
|
760
1021
|
return (a.line || 0) - (b.line || 0);
|
|
761
1022
|
});
|
|
762
1023
|
|
|
763
|
-
return {
|
|
1024
|
+
return {
|
|
1025
|
+
confirmed, unverified, account, groundSet, rebound, baseChanges: rebound.baseChanges || null,
|
|
1026
|
+
accountParts: { confirmedEntries, unverifiedEntries, excludedEntries: accountRaw.excludedEntries },
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ── Signature changes against a base revision (fix #394) ─────────────────
|
|
1031
|
+
//
|
|
1032
|
+
// The caller engine binds call sites the way the compiler does for code
|
|
1033
|
+
// that compiles: after `isEmpty(String)` became `isEmpty(String, boolean)`,
|
|
1034
|
+
// `Util.isEmpty(s)` fits no parameter list of the pin and the sibling
|
|
1035
|
+
// `isEmpty(Object[])` takes one argument, so the site is excluded as an
|
|
1036
|
+
// arity/overload mismatch - exactly the site `check` exists to report. The
|
|
1037
|
+
// base declaration decides which sites bound the definition before the
|
|
1038
|
+
// change; each is then judged against the new signature and the remaining
|
|
1039
|
+
// overloads.
|
|
1040
|
+
|
|
1041
|
+
// Exclusions whose verdict depends on the pin's declared parameters.
|
|
1042
|
+
const SIGNATURE_DEPENDENT_EXCLUSIONS = new Set(['arity-mismatch', 'overload-mismatch', 'generic-arity-mismatch']);
|
|
1043
|
+
|
|
1044
|
+
/** Declared parameter shape (types, optional/default/rest, method type parameters); names ignored. */
|
|
1045
|
+
function declaredSignatureKey(symbol) {
|
|
1046
|
+
const ps = symbol?.paramsStructured;
|
|
1047
|
+
const params = Array.isArray(ps)
|
|
1048
|
+
? ps.map(p => [String(p?.type ?? '').replace(/\s+/g, ''), p?.optional ? 'o' : '',
|
|
1049
|
+
p?.default !== undefined ? 'd' : '', p?.rest ? 'r' : '', p?.type ? '' : String(p?.name ?? '')].join(':')).join(',')
|
|
1050
|
+
: `raw:${String(symbol?.params ?? '').replace(/\s+/g, '')}`;
|
|
1051
|
+
return `${params}|${String(symbol?.generics ?? '').replace(/\s+/g, '')}`;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* The definition (and, for C/C++, its declarations elsewhere) whose declared
|
|
1056
|
+
* parameters differ at `base`: [{ def, before }] with `before` the base
|
|
1057
|
+
* declaration of the same name and owner, or [] (no git, untracked, file
|
|
1058
|
+
* unchanged, parameters unchanged, or a new overload).
|
|
1059
|
+
*/
|
|
1060
|
+
const GIT_REF_FORMAT = /^[a-zA-Z0-9._\-~\/^@{}:]+$/; // eslint-disable-line no-useless-escape
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Start reading `file` at `base` in the background (a shell running `git
|
|
1064
|
+
* show` into a temporary file), so the read overlaps the caller sweep of the
|
|
1065
|
+
* current declaration; null where that is not available.
|
|
1066
|
+
*/
|
|
1067
|
+
let baseReadSerial = 0;
|
|
1068
|
+
|
|
1069
|
+
function startBaseRead(file, base) {
|
|
1070
|
+
if (process.platform === 'win32' || !file || !GIT_REF_FORMAT.test(base)) return null;
|
|
1071
|
+
const out = path.join(os.tmpdir(),
|
|
1072
|
+
`ucn-base-${process.pid}-${++baseReadSerial}-${Math.random().toString(36).slice(2, 10)}`);
|
|
1073
|
+
let child;
|
|
1074
|
+
try {
|
|
1075
|
+
// The content appears under `out` only once complete; `out.none`
|
|
1076
|
+
// marks a revision that does not hold the file.
|
|
1077
|
+
child = spawn('/bin/sh', ['-c',
|
|
1078
|
+
'git show "$1" > "$2.part" 2>/dev/null && mv "$2.part" "$2" || { rm -f "$2.part"; : > "$2.none"; }',
|
|
1079
|
+
'ucn-base', `${base}:./${path.basename(file)}`, out],
|
|
1080
|
+
{ cwd: path.dirname(file), stdio: 'ignore' });
|
|
1081
|
+
} catch {
|
|
1082
|
+
child = null;
|
|
1083
|
+
}
|
|
1084
|
+
if (!child?.pid) return null;
|
|
1085
|
+
child.on('error', () => {});
|
|
1086
|
+
child.unref();
|
|
1087
|
+
return { file, out };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Throw a UcnError unless `base` names a commit of the repository holding `file`. */
|
|
1091
|
+
function assertGitCommit(file, base) {
|
|
1092
|
+
try {
|
|
1093
|
+
execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
1094
|
+
cwd: path.dirname(file), stdio: ['ignore', 'pipe', 'ignore'],
|
|
1095
|
+
});
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
throw new UcnError('Not a git repository. check --base requires git.', { cause: e });
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
execFileSync('git', ['rev-parse', '--verify', '--quiet', '--end-of-options', `${base}^{commit}`], {
|
|
1101
|
+
cwd: path.dirname(file), stdio: ['ignore', 'pipe', 'ignore'],
|
|
1102
|
+
});
|
|
1103
|
+
} catch (e) {
|
|
1104
|
+
throw new UcnError(`Unknown git ref: ${base}`, { cause: e });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/** The content a background read produced: a string, null (not at base), or undefined (no answer in time). */
|
|
1109
|
+
function finishBaseRead(handle, timeoutMs = 15000) {
|
|
1110
|
+
const cell = new Int32Array(new SharedArrayBuffer(4));
|
|
1111
|
+
const started = Date.now();
|
|
1112
|
+
const none = `${handle.out}.none`;
|
|
1113
|
+
try {
|
|
1114
|
+
for (;;) {
|
|
1115
|
+
if (fs.existsSync(handle.out)) return fs.readFileSync(handle.out, 'utf-8');
|
|
1116
|
+
if (fs.existsSync(none)) return null;
|
|
1117
|
+
if (Date.now() - started > timeoutMs) return undefined;
|
|
1118
|
+
Atomics.wait(cell, 0, 0, 1);
|
|
1119
|
+
}
|
|
1120
|
+
} catch {
|
|
1121
|
+
return undefined;
|
|
1122
|
+
} finally {
|
|
1123
|
+
handle.done = true;
|
|
1124
|
+
for (const leftover of [handle.out, none]) {
|
|
1125
|
+
try { fs.unlinkSync(leftover); } catch { /* not created */ }
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function baseDeclarationChanges(index, name, def, base = 'HEAD', baseRead = null) {
|
|
1131
|
+
if (!GIT_REF_FORMAT.test(base)) {
|
|
1132
|
+
throw new UcnError(`Invalid git ref format: ${base}`);
|
|
1133
|
+
}
|
|
1134
|
+
const language = index.files.get(def.file)?.language;
|
|
1135
|
+
let members = [def];
|
|
1136
|
+
if (language === 'c' || language === 'cpp') {
|
|
1137
|
+
const { _closeCallableIdentityGroup } = require('./callers');
|
|
1138
|
+
members = _closeCallableIdentityGroup(index, [def], index.symbols.get(name) || [def]);
|
|
1139
|
+
}
|
|
1140
|
+
const byFile = new Map();
|
|
1141
|
+
for (const member of members) {
|
|
1142
|
+
if (!member.file || !index.files.has(member.file)) continue;
|
|
1143
|
+
if (!byFile.has(member.file)) byFile.set(member.file, []);
|
|
1144
|
+
byFile.get(member.file).push(member);
|
|
1145
|
+
}
|
|
1146
|
+
const { extractCallableSymbols } = require('./analysis');
|
|
1147
|
+
const changes = [];
|
|
1148
|
+
for (const [file, defs] of [...byFile].sort(([a], [b]) => codeUnitCompare(a, b))) {
|
|
1149
|
+
let baseContent = baseRead && !baseRead.done && baseRead.file === file ? finishBaseRead(baseRead) : undefined;
|
|
1150
|
+
if (baseContent === null) continue;
|
|
1151
|
+
if (baseContent === undefined) {
|
|
1152
|
+
try {
|
|
1153
|
+
baseContent = execFileSync('git', ['show', `${base}:./${path.basename(file)}`], {
|
|
1154
|
+
cwd: path.dirname(file), encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024,
|
|
1155
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1156
|
+
});
|
|
1157
|
+
} catch {
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// Unchanged since base: the index's content hash of the file says so
|
|
1162
|
+
// without reading it again.
|
|
1163
|
+
const indexedHash = index.files.get(file)?.hash;
|
|
1164
|
+
if (indexedHash && require('crypto').createHash('md5').update(baseContent).digest('hex') === indexedHash) continue;
|
|
1165
|
+
if (!indexedHash) {
|
|
1166
|
+
let current;
|
|
1167
|
+
try { current = index._readFile(file); } catch { continue; }
|
|
1168
|
+
if (baseContent === current) continue;
|
|
1169
|
+
}
|
|
1170
|
+
const entry = index.files.get(file);
|
|
1171
|
+
let baseSymbols;
|
|
1172
|
+
try {
|
|
1173
|
+
baseSymbols = extractCallableSymbols(require('./parser').parse(baseContent, entry.language));
|
|
1174
|
+
} catch {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
for (const d of defs) {
|
|
1178
|
+
const before = baseCounterpartOf(entry.symbols, baseSymbols, d);
|
|
1179
|
+
if (before) changes.push({ def: d, before });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return changes;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* The base declaration of `d` (same name and owner) when its declared
|
|
1187
|
+
* parameters changed, else null: base overloads a current sibling still
|
|
1188
|
+
* declares are that sibling; among the rest the nearest line wins.
|
|
1189
|
+
*/
|
|
1190
|
+
function baseCounterpartOf(currentSymbols, baseSymbols, d) {
|
|
1191
|
+
const identity = symbol => `${symbol.name}\0${symbol.className || ''}`;
|
|
1192
|
+
const callable = symbol => {
|
|
1193
|
+
const type = symbol.type || symbol.memberType || 'method';
|
|
1194
|
+
return !NON_CALLABLE_TYPES.has(type) || (type === 'field' && symbol.isMethod);
|
|
1195
|
+
};
|
|
1196
|
+
const same = baseSymbols.filter(symbol => identity(symbol) === identity(d));
|
|
1197
|
+
const key = declaredSignatureKey(d);
|
|
1198
|
+
if (same.length === 0 || same.some(symbol => declaredSignatureKey(symbol) === key)) return null;
|
|
1199
|
+
const siblings = new Set((currentSymbols || []).filter(symbol => symbol !== d && callable(symbol) &&
|
|
1200
|
+
identity(symbol) === identity(d)).map(declaredSignatureKey));
|
|
1201
|
+
const changed = same.filter(symbol => !siblings.has(declaredSignatureKey(symbol)));
|
|
1202
|
+
if (changed.length === 0) return null;
|
|
1203
|
+
changed.sort((a, b) => Math.abs(a.startLine - d.startLine) - Math.abs(b.startLine - d.startLine) ||
|
|
1204
|
+
a.startLine - b.startLine);
|
|
1205
|
+
return changed[0];
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const DECLARED_SIGNATURE_FIELDS = ['params', 'paramsStructured', 'generics'];
|
|
1209
|
+
|
|
1210
|
+
/** Callers of `def` as the index binds them with each changed declaration's base parameters. */
|
|
1211
|
+
function callersBeforeChange(index, name, def, changes) {
|
|
1212
|
+
return index._isolatedOperation(() => {
|
|
1213
|
+
const saved = changes.map(change => [change.def, DECLARED_SIGNATURE_FIELDS.map(field =>
|
|
1214
|
+
[field, Object.prototype.hasOwnProperty.call(change.def, field), change.def[field]])]);
|
|
1215
|
+
try {
|
|
1216
|
+
for (const change of changes) {
|
|
1217
|
+
for (const field of DECLARED_SIGNATURE_FIELDS) {
|
|
1218
|
+
if (change.before[field] === undefined) delete change.def[field];
|
|
1219
|
+
else change.def[field] = change.before[field];
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const raw = index.findCallers(name, { includeMethods: true, targetDefinitions: [def], collectAccount: true });
|
|
1223
|
+
return [
|
|
1224
|
+
...raw.filter(c => c.tier !== 'unverified').map(c => ({ site: c, boundBefore: 'confirmed' })),
|
|
1225
|
+
...raw.filter(c => c.tier === 'unverified').concat(raw.unverifiedEntries || [])
|
|
1226
|
+
.map(c => ({ site: c, boundBefore: 'unverified' })),
|
|
1227
|
+
];
|
|
1228
|
+
} finally {
|
|
1229
|
+
for (const [definition, fields] of saved) {
|
|
1230
|
+
for (const [field, own, value] of fields) {
|
|
1231
|
+
if (own) definition[field] = value;
|
|
1232
|
+
else delete definition[field];
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Sites the base declaration bound that the current sweep excludes, each
|
|
1241
|
+
* with its verdict against the new signature: `rebind.kind` is
|
|
1242
|
+
* 'no-overload' (fits no parameter list: a compile error), 'other-overload'
|
|
1243
|
+
* (another overload may take it now), 'count' (the argument count decides)
|
|
1244
|
+
* or 'unread' (the call record could not be re-read).
|
|
1245
|
+
*/
|
|
1246
|
+
function sitesReboundByChange(index, name, def, rawCallers, confirmed, unverified, changes) {
|
|
1247
|
+
const excluded = rawCallers.accountRaw?.excludedEntries || [];
|
|
1248
|
+
// Only an exclusion by parameter fit, or a site left unverified, can hide
|
|
1249
|
+
// a call of the old declaration; otherwise the base is not read.
|
|
1250
|
+
const dependent = excluded.filter(entry => SIGNATURE_DEPENDENT_EXCLUSIONS.has(entry.reason));
|
|
1251
|
+
if (dependent.length === 0 && unverified.length === 0) return [];
|
|
1252
|
+
const baseChanges = typeof changes === 'function' ? changes() : changes;
|
|
1253
|
+
if (!baseChanges || baseChanges.length === 0) return [];
|
|
1254
|
+
const siteKey = site => `${site.file}\0${site.line}`;
|
|
1255
|
+
const confirmedKeys = new Set(confirmed.map(siteKey));
|
|
1256
|
+
const unverifiedKeys = new Set(unverified.map(siteKey));
|
|
1257
|
+
const reasonAt = new Map(excluded.map(entry => [siteKey(entry), entry.reason]));
|
|
1258
|
+
const before = callersBeforeChange(index, name, def, baseChanges);
|
|
1259
|
+
const { getCachedCalls, _overloadDiscipline, _overloadApplicable } = require('./callers');
|
|
1260
|
+
const pins = [...new Set([def, ...baseChanges.map(change => change.def)])];
|
|
1261
|
+
const definitions = index.symbols.get(name) || [];
|
|
1262
|
+
const out = [];
|
|
1263
|
+
const seen = new Set();
|
|
1264
|
+
for (const { site, boundBefore } of before) {
|
|
1265
|
+
const key = siteKey(site);
|
|
1266
|
+
// A site still confirmed is arg-checked as usual. A site the old
|
|
1267
|
+
// declaration confirmed and the new one leaves unverified or
|
|
1268
|
+
// excludes is checked against the new declaration here.
|
|
1269
|
+
if (confirmedKeys.has(key) || seen.has(key)) continue;
|
|
1270
|
+
if (unverifiedKeys.has(key) ? boundBefore !== 'confirmed' : !reasonAt.has(key)) continue;
|
|
1271
|
+
seen.add(key);
|
|
1272
|
+
const calls = (getCachedCalls(index, site.file) || []).filter(call =>
|
|
1273
|
+
call.line === site.line && (call.name === name || call.resolvedName === name));
|
|
1274
|
+
const call = calls.find(c => site.column == null || c.column === site.column) || calls[0];
|
|
1275
|
+
const language = index.files.get(site.file)?.language;
|
|
1276
|
+
let rebind = { kind: call ? 'count' : 'unread' };
|
|
1277
|
+
if (call && langTraits(language)?.hasArityOverloads && call.argCount != null && !call.argSpread) {
|
|
1278
|
+
const verdict = _overloadDiscipline(index, call, pins, definitions, site.file);
|
|
1279
|
+
if (verdict === 'other-overload') {
|
|
1280
|
+
const { ownerRefOf } = require('./class-identity');
|
|
1281
|
+
const ownerKey = ownerRefOf(index, def)?.key;
|
|
1282
|
+
const others = definitions.filter(d => !pins.includes(d) && !NON_CALLABLE_TYPES.has(d.type) &&
|
|
1283
|
+
d.className === def.className && ownerKey && ownerRefOf(index, d)?.key === ownerKey &&
|
|
1284
|
+
_overloadApplicable(index, call, d));
|
|
1285
|
+
rebind = { kind: 'other-overload', overloads: others.map(d => formatTypedSignature(d)) };
|
|
1286
|
+
} else if (verdict?.ambiguous) {
|
|
1287
|
+
rebind = verdict.candidates === 0 ? { kind: 'no-overload' }
|
|
1288
|
+
: { kind: 'other-overload', candidates: verdict.candidates };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
let content = site.content;
|
|
1292
|
+
if (content == null) {
|
|
1293
|
+
try { content = index._getFileLines(site.file)[site.line - 1] ?? ''; } catch { content = ''; }
|
|
1294
|
+
}
|
|
1295
|
+
const prior = unverifiedKeys.has(key)
|
|
1296
|
+
? unverified.find(u => siteKey(u) === key && (site.column == null || u.column == null || u.column === site.column))
|
|
1297
|
+
: null;
|
|
1298
|
+
out.push({
|
|
1299
|
+
...site,
|
|
1300
|
+
content,
|
|
1301
|
+
tier: 'unverified',
|
|
1302
|
+
reason: 'bound-before-change',
|
|
1303
|
+
...(prior?.reason && { priorReason: prior.reason }),
|
|
1304
|
+
boundBefore,
|
|
1305
|
+
excludedAs: reasonAt.get(key) || null,
|
|
1306
|
+
...(unverifiedKeys.has(key) && { stillUnverified: true }),
|
|
1307
|
+
rebind,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
out.sort((a, b) => codeUnitCompare(a.relativePath || '', b.relativePath || '') || a.line - b.line);
|
|
1311
|
+
out.baseChanges = baseChanges;
|
|
1312
|
+
return out;
|
|
764
1313
|
}
|
|
765
1314
|
|
|
766
1315
|
/** Map an unverified sweep entry to the public site shape (relative `file`). */
|
|
@@ -782,6 +1331,7 @@ function unverifiedSiteShape(u) {
|
|
|
782
1331
|
// Module-attribute attribution (fix #294): the name binds the module's
|
|
783
1332
|
// export surface — a non-slot surface for rename purposes.
|
|
784
1333
|
...(u.moduleAttribute && { moduleAttribute: true }),
|
|
1334
|
+
...(u.annotationShorthand && { annotationShorthand: u.annotationShorthand }),
|
|
785
1335
|
};
|
|
786
1336
|
}
|
|
787
1337
|
|
|
@@ -794,12 +1344,22 @@ function unverifiedSiteShape(u) {
|
|
|
794
1344
|
* @param {object} def - Resolved definition
|
|
795
1345
|
* @returns {{ sites: Array, unverifiedSites: Array, account: object }}
|
|
796
1346
|
*/
|
|
797
|
-
function computePlanCallSites(index, name, def) {
|
|
798
|
-
|
|
1347
|
+
function computePlanCallSites(index, name, def, options = {}) {
|
|
1348
|
+
// A plan holds one call-site tree per file across its sweeps (fix #395).
|
|
1349
|
+
const ownsTreeCache = !index._treeCache;
|
|
1350
|
+
// A rename edits name tokens only; argument analysis (a parse of every
|
|
1351
|
+
// caller file) serves signature changes (fix #396).
|
|
1352
|
+
const analyzeArgs = options.analyzeArgs !== false;
|
|
1353
|
+
const { confirmed, unverified, account, groundSet, accountParts } =
|
|
1354
|
+
contractedCallerSweep(index, name, def);
|
|
799
1355
|
|
|
800
1356
|
const sites = [];
|
|
801
1357
|
const planLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
802
1358
|
for (const c of confirmed) {
|
|
1359
|
+
// A call a macro template generated is spelled in the macro
|
|
1360
|
+
// definition (fix #374): the template pass edits or lists it there;
|
|
1361
|
+
// the invocation line has no token to rename.
|
|
1362
|
+
if (c.macroExpansion?.origin === 'template') continue;
|
|
803
1363
|
const call = {
|
|
804
1364
|
file: c.file,
|
|
805
1365
|
relativePath: c.relativePath,
|
|
@@ -813,35 +1373,50 @@ function computePlanCallSites(index, name, def) {
|
|
|
813
1373
|
const siteKey = `${c.file}:${c.line}:${c.calledAs || name}`;
|
|
814
1374
|
const occurrence = planLineSeen.get(siteKey) || 0;
|
|
815
1375
|
planLineSeen.set(siteKey, occurrence + 1);
|
|
816
|
-
const analysis = analyzeCallSite(index, call, name, occurrence);
|
|
1376
|
+
const analysis = analyzeArgs ? analyzeCallSite(index, call, name, occurrence) : { args: null, argCount: 0 };
|
|
1377
|
+
// The call's source range (provenance site) lets the rename find
|
|
1378
|
+
// its own name token when the record carries no column (fix #392).
|
|
1379
|
+
const siteFacts = c.provenance?.facts?.site;
|
|
1380
|
+
const siteRange = !Number.isInteger(c.column) && siteFacts &&
|
|
1381
|
+
siteFacts.line === c.line && Number.isInteger(siteFacts.start) &&
|
|
1382
|
+
Number.isInteger(siteFacts.end) ? { start: siteFacts.start, end: siteFacts.end } : null;
|
|
817
1383
|
sites.push({
|
|
818
1384
|
file: call.relativePath,
|
|
819
1385
|
absoluteFile: call.file,
|
|
820
1386
|
line: call.line,
|
|
821
1387
|
...(Number.isInteger(c.column) && { column: c.column }),
|
|
1388
|
+
...(siteRange && { siteRange }),
|
|
822
1389
|
expression: (call.content || '').trim(),
|
|
823
1390
|
args: analysis.args,
|
|
824
1391
|
argCount: analysis.argCount,
|
|
825
1392
|
...(analysis.keywordArgNames && { keywordArgNames: analysis.keywordArgNames }),
|
|
826
1393
|
...(analysis.positionalCount != null && { positionalCount: analysis.positionalCount }),
|
|
827
1394
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
1395
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
1396
|
+
...(c.targetTyped && { targetTyped: true }),
|
|
1397
|
+
...(c.destructured && { destructured: c.destructured }),
|
|
1398
|
+
...(c.importAliasSpelling && { importAliasSpelling: true }),
|
|
828
1399
|
});
|
|
829
1400
|
}
|
|
830
|
-
clearTreeCache(index);
|
|
831
|
-
// Stable ordering
|
|
1401
|
+
if (ownsTreeCache) clearTreeCache(index);
|
|
1402
|
+
// Stable ordering contract: files alphabetical, sites by line ascending.
|
|
832
1403
|
sites.sort((a, b) => {
|
|
833
1404
|
const fc = codeUnitCompare(String(a.file), String(b.file));
|
|
834
1405
|
if (fc !== 0) return fc;
|
|
835
1406
|
return (a.line || 0) - (b.line || 0);
|
|
836
1407
|
});
|
|
1408
|
+
// A target-typed `new(..)` spells no name (fix #400): a rename never
|
|
1409
|
+
// edits it, so an undecided one is no rename candidate either.
|
|
1410
|
+
const renameRelevant = u => u.macroExpansion?.origin !== 'template' && !u.targetTyped;
|
|
837
1411
|
return {
|
|
838
1412
|
sites,
|
|
839
|
-
unverifiedSites: unverified.map(unverifiedSiteShape),
|
|
1413
|
+
unverifiedSites: unverified.filter(renameRelevant).map(unverifiedSiteShape),
|
|
840
1414
|
// Plan-only evidence used to promote calls through a compiler-proven
|
|
841
1415
|
// Go interface slot. Kept out of the public JSON surface.
|
|
842
|
-
rawUnverified: unverified,
|
|
1416
|
+
rawUnverified: unverified.filter(renameRelevant),
|
|
843
1417
|
account,
|
|
844
1418
|
groundSet,
|
|
1419
|
+
accountParts,
|
|
845
1420
|
};
|
|
846
1421
|
}
|
|
847
1422
|
|
|
@@ -877,6 +1452,22 @@ function computePlanScopeWarning(index, name, def, options) {
|
|
|
877
1452
|
* @param {string} funcName - Function name to find
|
|
878
1453
|
* @returns {object} { args, argCount, hasSpread, hasVariable }
|
|
879
1454
|
*/
|
|
1455
|
+
/**
|
|
1456
|
+
* The operation's parsed tree of a file (the one the ground-set
|
|
1457
|
+
* classification already parsed) when the language's query tree for this
|
|
1458
|
+
* content is the plain parse; null otherwise (a C# file with conditional
|
|
1459
|
+
* directives reads a configuration view).
|
|
1460
|
+
*/
|
|
1461
|
+
function sharedParsedTree(index, file, content, language) {
|
|
1462
|
+
if (typeof index._getParsedTree !== 'function') return null;
|
|
1463
|
+
// A language that queries its own view of a file (C# conditional
|
|
1464
|
+
// compilation) shares the tree only where that view is the plain parse
|
|
1465
|
+
// (fix #395).
|
|
1466
|
+
const adapter = getLanguageAdapter(language);
|
|
1467
|
+
if (adapter?.queryTree && !adapter.queryTreeIsPlain?.(content, index.files?.get(file))) return null;
|
|
1468
|
+
return index._getParsedTree(file, content, language);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
880
1471
|
function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
881
1472
|
try {
|
|
882
1473
|
const language = detectLanguage(call.file);
|
|
@@ -897,9 +1488,12 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
897
1488
|
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
898
1489
|
tree = safeParse(jsParser, virtualJS);
|
|
899
1490
|
} else {
|
|
900
|
-
|
|
901
|
-
if (!
|
|
902
|
-
|
|
1491
|
+
tree = sharedParsedTree(index, call.file, content, language);
|
|
1492
|
+
if (!tree) {
|
|
1493
|
+
const parser = getParser(language);
|
|
1494
|
+
if (!parser) return { args: null, argCount: 0 };
|
|
1495
|
+
tree = safeParse(parser, content);
|
|
1496
|
+
}
|
|
903
1497
|
}
|
|
904
1498
|
if (!tree) return { args: null, argCount: 0 };
|
|
905
1499
|
if (!index._treeCache) index._treeCache = new Map();
|
|
@@ -1069,9 +1663,12 @@ function analyzeCallShape(index, filePath, lineNum, funcName) {
|
|
|
1069
1663
|
const virtualJS = htmlModule.buildVirtualJSContent(content, blocks);
|
|
1070
1664
|
tree = safeParse(jsParser, virtualJS);
|
|
1071
1665
|
} else {
|
|
1072
|
-
|
|
1073
|
-
if (!
|
|
1074
|
-
|
|
1666
|
+
tree = sharedParsedTree(index, filePath, content, language);
|
|
1667
|
+
if (!tree) {
|
|
1668
|
+
const parser = getParser(language);
|
|
1669
|
+
if (!parser) return null;
|
|
1670
|
+
tree = safeParse(parser, content);
|
|
1671
|
+
}
|
|
1075
1672
|
}
|
|
1076
1673
|
if (!tree) return null;
|
|
1077
1674
|
if (!index._treeCache) index._treeCache = new Map();
|
|
@@ -1323,7 +1920,11 @@ function verify(index, name, options = {}) {
|
|
|
1323
1920
|
const ctorParamLists = _constructorParamLists(index, def, lang);
|
|
1324
1921
|
const inheritedCtorOnly = !ctorParamLists && def.type === 'class' && !!def.extends;
|
|
1325
1922
|
const selfParams = langTraits(lang)?.selfParam;
|
|
1326
|
-
|
|
1923
|
+
// `&'a mut self` names the receiver too (fix #369): lifetimes are not
|
|
1924
|
+
// part of the receiver spelling.
|
|
1925
|
+
const selfSpelling = name => String(name || '').replace(/'[A-Za-z_]\w*\s*/g, '').trim();
|
|
1926
|
+
const stripSelf = (list) => (selfParams && list.length > 0 && list[0] &&
|
|
1927
|
+
selfParams.includes(selfSpelling(list[0].name)))
|
|
1327
1928
|
? list.slice(1) : list;
|
|
1328
1929
|
let callableIdentityParams = null;
|
|
1329
1930
|
if (!ctorParamLists && ['c', 'cpp'].includes(lang)) {
|
|
@@ -1354,21 +1955,47 @@ function verify(index, name, options = {}) {
|
|
|
1354
1955
|
// dropped). Engine receiver physics replace the pre-v4 className filter
|
|
1355
1956
|
// and the isMethodCall secondary filter — --include-methods and
|
|
1356
1957
|
// --include-uncertain are implied no-ops for verify.
|
|
1357
|
-
|
|
1358
|
-
|
|
1958
|
+
// A declaration changed since the base revision is checked on the call
|
|
1959
|
+
// sites its base declaration bound (fix #394); the base file is read in
|
|
1960
|
+
// the background while the current declaration is swept, and used only
|
|
1961
|
+
// when the sweep excludes a site by parameter fit or leaves one unverified.
|
|
1962
|
+
const baseRef = options.base || 'HEAD';
|
|
1963
|
+
if (!GIT_REF_FORMAT.test(baseRef)) throw new UcnError(`Invalid git ref format: ${baseRef}`);
|
|
1964
|
+
// A base the user names must be a commit: a misspelled ref would read as
|
|
1965
|
+
// a file absent at base and check nothing against the old declaration
|
|
1966
|
+
// (the target-less check already resolved its base through the diff).
|
|
1967
|
+
if (options.base && options.checkBaseRef) assertGitCommit(def.file, baseRef);
|
|
1968
|
+
const baseRead = startBaseRead(def.file, baseRef);
|
|
1969
|
+
let sweep;
|
|
1970
|
+
try {
|
|
1971
|
+
sweep = contractedCallerSweep(index, name, def, {
|
|
1972
|
+
baseChanges: () => baseDeclarationChanges(index, name, def, baseRef, baseRead),
|
|
1973
|
+
});
|
|
1974
|
+
} finally {
|
|
1975
|
+
if (baseRead && !baseRead.done) finishBaseRead(baseRead);
|
|
1976
|
+
}
|
|
1977
|
+
const { confirmed: callerResults, unverified: sweepUnverified, account, rebound, baseChanges } = sweep;
|
|
1359
1978
|
|
|
1360
1979
|
// Convert caller results to usage-like objects for analyzeCallSite.
|
|
1361
1980
|
// Carry callerFile/callerStartLine through so we can compute inTestCase.
|
|
1362
1981
|
const invalidFamilyCalls = sweepUnverified.filter(c =>
|
|
1363
|
-
validateCallMismatch(c.provenance, declarationIdentity(def)));
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1982
|
+
!c.rebind && validateCallMismatch(c.provenance, declarationIdentity(def)));
|
|
1983
|
+
const siteContent = c => {
|
|
1984
|
+
if (c.content != null) return c.content;
|
|
1985
|
+
try { return index._getFileLines(c.file)[c.line - 1] ?? ''; } catch { return ''; }
|
|
1986
|
+
};
|
|
1987
|
+
const pinBaseChange = (baseChanges || []).find(change => change.def === def) || null;
|
|
1988
|
+
const calls = [...callerResults, ...invalidFamilyCalls, ...rebound].map(c => ({
|
|
1989
|
+
invalidOverload: invalidFamilyCalls.includes(c) || c.rebind?.kind === 'no-overload',
|
|
1990
|
+
...(c.rebind && { rebind: c.rebind, excludedAs: c.excludedAs, boundBefore: c.boundBefore,
|
|
1991
|
+
unverifiedReason: c.priorReason }),
|
|
1366
1992
|
file: c.file,
|
|
1367
1993
|
relativePath: c.relativePath,
|
|
1368
1994
|
line: c.line,
|
|
1369
|
-
content: c
|
|
1995
|
+
content: siteContent(c),
|
|
1370
1996
|
usageType: 'call',
|
|
1371
1997
|
receiver: c.receiver,
|
|
1998
|
+
isMethod: c.isMethod,
|
|
1372
1999
|
calledAs: c.calledAs,
|
|
1373
2000
|
callSite: c.provenance?.facts?.site,
|
|
1374
2001
|
// Preserve receiver identity through the usage-shaped adapter. Go
|
|
@@ -1378,6 +2005,7 @@ function verify(index, name, options = {}) {
|
|
|
1378
2005
|
receiverType: c.receiverType,
|
|
1379
2006
|
callerFile: c.callerFile,
|
|
1380
2007
|
callerStartLine: c.callerStartLine,
|
|
2008
|
+
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
1381
2009
|
}));
|
|
1382
2010
|
|
|
1383
2011
|
const valid = [];
|
|
@@ -1424,13 +2052,17 @@ function verify(index, name, options = {}) {
|
|
|
1424
2052
|
callerStartLine: call.callerStartLine,
|
|
1425
2053
|
};
|
|
1426
2054
|
|
|
1427
|
-
if (analysis.args === null) {
|
|
1428
|
-
// Couldn't parse arguments
|
|
2055
|
+
if (analysis.args === null || call.macroExpansion) {
|
|
2056
|
+
// Couldn't parse arguments, or the call exists only in the
|
|
2057
|
+
// expansion of a macro invocation on this line (fix #362): its
|
|
2058
|
+
// arguments are the macro's, not text on the line.
|
|
1429
2059
|
uncertain.push({
|
|
1430
2060
|
file: call.relativePath,
|
|
1431
2061
|
line: call.line,
|
|
1432
2062
|
expression: call.content.trim(),
|
|
1433
|
-
reason:
|
|
2063
|
+
reason: call.macroExpansion
|
|
2064
|
+
? `Call produced by expanding macro ${call.macroExpansion.macro}; check the macro's argument list`
|
|
2065
|
+
: analysis.uncertainReason || 'Could not parse call arguments',
|
|
1434
2066
|
patterns: patternFlagsFrom(analysis),
|
|
1435
2067
|
...carry,
|
|
1436
2068
|
});
|
|
@@ -1453,6 +2085,9 @@ function verify(index, name, options = {}) {
|
|
|
1453
2085
|
}
|
|
1454
2086
|
|
|
1455
2087
|
let argCount = analysis.argCount;
|
|
2088
|
+
// A C# extension method called on its receiver (`b.Length(1, 2)`)
|
|
2089
|
+
// receives that receiver as its `this` parameter (fix #394).
|
|
2090
|
+
if (def.isExtensionMethod && call.isMethod && call.receiver !== def.className) argCount += 1;
|
|
1456
2091
|
// Method-expression / UFCS receiver shift (fix #230): Go
|
|
1457
2092
|
// `M.Add(*m, 2)` and Rust `Engine::run(&e, 1)` pass the receiver as
|
|
1458
2093
|
// the FIRST argument — the same +1 shift the #205 arity discipline
|
|
@@ -1472,11 +2107,32 @@ function verify(index, name, options = {}) {
|
|
|
1472
2107
|
const countOk = hasRest
|
|
1473
2108
|
? argCount >= minArgs
|
|
1474
2109
|
: (argCount >= minArgs && argCount <= expectedParamCount);
|
|
2110
|
+
if (call.rebind && call.rebind.kind !== 'no-overload' && (countOk || call.rebind.kind !== 'count')) {
|
|
2111
|
+
// Bound the base declaration; the new one may still take it, or
|
|
2112
|
+
// another overload may: the compiler decides (fix #394).
|
|
2113
|
+
const target = call.rebind.overloads?.length > 0
|
|
2114
|
+
? `it may now bind ${call.rebind.overloads.join(' or ')}`
|
|
2115
|
+
: call.rebind.candidates > 0 ? `it may now bind one of ${call.rebind.candidates} overloads`
|
|
2116
|
+
: call.rebind.kind === 'unread' ? 'its call could not be re-read'
|
|
2117
|
+
: call.excludedAs ? `the index excludes it now (${call.excludedAs})`
|
|
2118
|
+
: `its binding to the new declaration is unverified (${call.unverifiedReason || 'no evidence'})`;
|
|
2119
|
+
uncertain.push({
|
|
2120
|
+
file: call.relativePath,
|
|
2121
|
+
line: call.line,
|
|
2122
|
+
expression: call.content.trim(),
|
|
2123
|
+
reason: `Called the declaration before the change; ${target}`,
|
|
2124
|
+
boundBeforeChange: true,
|
|
2125
|
+
patterns: patternFlagsFrom(analysis),
|
|
2126
|
+
...carry,
|
|
2127
|
+
});
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
1475
2130
|
if (!countOk || call.invalidOverload) {
|
|
1476
2131
|
mismatches.push({
|
|
1477
2132
|
file: call.relativePath,
|
|
1478
2133
|
line: call.line,
|
|
1479
2134
|
expression: call.content.trim(),
|
|
2135
|
+
...(call.rebind && { boundBeforeChange: true }),
|
|
1480
2136
|
expected: call.invalidOverload && countOk ? 'a compatible overload signature' : hasRest
|
|
1481
2137
|
? `at least ${minArgs} arg(s)`
|
|
1482
2138
|
: (minArgs === expectedParamCount
|
|
@@ -1641,6 +2297,15 @@ function verify(index, name, options = {}) {
|
|
|
1641
2297
|
patterns: patternsAgg,
|
|
1642
2298
|
scopeWarning,
|
|
1643
2299
|
...(warnings.length > 0 && { warnings }),
|
|
2300
|
+
// The base declaration whose call sites were checked (fix #394).
|
|
2301
|
+
...(pinBaseChange && {
|
|
2302
|
+
changedSince: {
|
|
2303
|
+
base: options.base || 'HEAD',
|
|
2304
|
+
signature: formatTypedSignature({ ...def, ...pinBaseChange.before,
|
|
2305
|
+
...(pinBaseChange.before.generics === undefined && { generics: undefined }) }),
|
|
2306
|
+
callSites: rebound.length,
|
|
2307
|
+
},
|
|
2308
|
+
}),
|
|
1644
2309
|
};
|
|
1645
2310
|
} finally { index._endOp(); }
|
|
1646
2311
|
}
|
|
@@ -1690,223 +2355,80 @@ const RESERVED_WORDS_BY_LANGUAGE = {
|
|
|
1690
2355
|
'while').split(' ')),
|
|
1691
2356
|
};
|
|
1692
2357
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
else if (ch === ')') round = Math.max(0, round - 1);
|
|
1706
|
-
else if (ch === '[') square++;
|
|
1707
|
-
else if (ch === ']') square = Math.max(0, square - 1);
|
|
1708
|
-
else if (ch === '{') curly++;
|
|
1709
|
-
else if (ch === '}') curly = Math.max(0, curly - 1);
|
|
1710
|
-
else if (ch === ',' && round === 0 && square === 0 && curly === 0) {
|
|
1711
|
-
out.push(text.slice(start, i).trim());
|
|
1712
|
-
start = i + 1;
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
out.push(text.slice(start).trim());
|
|
1716
|
-
return out.filter(Boolean);
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
const GO_UNNAMED_TYPE_PREFIX = /^(?:\*|\[|map\[|chan(?:<-)?\s|<-chan\s|func\s*\(|interface\s*\{|struct\s*\{|\.\.\.)/;
|
|
1720
|
-
|
|
1721
|
-
/** Turn one raw Go parameter/result declaration into its type spelling. */
|
|
1722
|
-
function goDeclarationType(raw) {
|
|
1723
|
-
let value = String(raw || '').trim();
|
|
1724
|
-
if (!value) return null;
|
|
1725
|
-
// Named slots/results: `value T`, `ctx context.Context`. Unnamed type
|
|
1726
|
-
// forms starting with Go type syntax (`chan T`, `func(...)`, `*T`) keep
|
|
1727
|
-
// their whole text. Structured params normally handle grouped names;
|
|
1728
|
-
// this fallback exists for unnamed interface slots and result tuples.
|
|
1729
|
-
if (!GO_UNNAMED_TYPE_PREFIX.test(value)) {
|
|
1730
|
-
const named = value.match(/^[A-Za-z_][A-Za-z0-9_]*\s+(.+)$/s);
|
|
1731
|
-
if (named) value = named[1].trim();
|
|
1732
|
-
}
|
|
1733
|
-
return value.replace(/\s+/g, '');
|
|
1734
|
-
}
|
|
2358
|
+
const {
|
|
2359
|
+
goContractClosure,
|
|
2360
|
+
externalContractOf,
|
|
2361
|
+
hierarchySlotClosure,
|
|
2362
|
+
anonymousSlotMembers,
|
|
2363
|
+
typedLiteralContract,
|
|
2364
|
+
typedLiteralMembers,
|
|
2365
|
+
rustProjectTraits,
|
|
2366
|
+
rustMacroTemplateSites,
|
|
2367
|
+
splitGoSignatureList,
|
|
2368
|
+
goDeclarationType,
|
|
2369
|
+
} = require('./contract-membership');
|
|
1735
2370
|
|
|
1736
|
-
/**
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2371
|
+
/**
|
|
2372
|
+
* Plan verdict of a C++ qualified reference `Q::name` to the renamed
|
|
2373
|
+
* callable (fix #393): Q resolved from the reference's scope (macro-opened
|
|
2374
|
+
* namespaces, usings, aliases) names the pin's namespace (a namespace-scope
|
|
2375
|
+
* pin) or its class (a member): 'edit'; another namespace or class: null;
|
|
2376
|
+
* unresolvable: 'review'.
|
|
2377
|
+
*/
|
|
2378
|
+
function cppQualifiedReferenceVerdict(index, ref, def) {
|
|
2379
|
+
const { resolveQualifier, effectiveNamespace, macroPrefixAt, includeClosure } = require('./cpp-scope');
|
|
2380
|
+
// Inside an indexed declaration the reference's scope is that
|
|
2381
|
+
// declaration's (as for calls); at namespace scope (an explicit
|
|
2382
|
+
// instantiation is no indexed symbol) it is read from the AST under the
|
|
2383
|
+
// macro-opened prefix.
|
|
2384
|
+
const enclosing = index.findEnclosingFunction(ref.file, ref.line, true);
|
|
2385
|
+
let namespace;
|
|
2386
|
+
if (enclosing) {
|
|
2387
|
+
namespace = effectiveNamespace(index, enclosing) || '';
|
|
1749
2388
|
} else {
|
|
1750
|
-
|
|
1751
|
-
|
|
2389
|
+
let astNamespace = '';
|
|
2390
|
+
try {
|
|
2391
|
+
const content = index._readFile(ref.file);
|
|
2392
|
+
const tree = index._getParsedTree?.(ref.file, content, 'cpp') || safeParse(getParser('cpp'), content);
|
|
2393
|
+
const row = ref.line - 1;
|
|
2394
|
+
const text = (index._getFileLines?.(ref.file) || content.split('\n'))[row] || '';
|
|
2395
|
+
const column = Number.isInteger(ref.column) ? ref.column : Math.max(0, text.search(/\S/));
|
|
2396
|
+
const parts = [];
|
|
2397
|
+
for (let node = tree?.rootNode.descendantForPosition({ row, column }); node; node = node.parent) {
|
|
2398
|
+
if (node.type !== 'namespace_definition') continue;
|
|
2399
|
+
const nameNode = node.childForFieldName('name');
|
|
2400
|
+
if (nameNode?.text) parts.unshift(nameNode.text.replace(/\s+/g, ''));
|
|
2401
|
+
}
|
|
2402
|
+
astNamespace = parts.join('::');
|
|
2403
|
+
} catch { /* unparsed: the prefix alone */ }
|
|
2404
|
+
const prefix = macroPrefixAt(index, ref.file, ref.line) || '';
|
|
2405
|
+
namespace = prefix && astNamespace ? `${prefix}::${astNamespace}` : prefix || astNamespace;
|
|
1752
2406
|
}
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
:
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
function goTypeDefinition(index, name, fromFile) {
|
|
1766
|
-
const pathMod = require('path');
|
|
1767
|
-
const dir = pathMod.dirname(fromFile || '');
|
|
1768
|
-
const kinds = new Set(['struct', 'type', 'class']);
|
|
1769
|
-
const defs = (index.symbols.get(name) || []).filter(candidate =>
|
|
1770
|
-
candidate.file && pathMod.dirname(candidate.file) === dir &&
|
|
1771
|
-
kinds.has(candidate.type));
|
|
1772
|
-
if (defs.length !== 1 || defs[0].generics) return null;
|
|
1773
|
-
return defs[0];
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
function goInterfaceMembers(index, interfaceDef) {
|
|
1777
|
-
const out = [];
|
|
1778
|
-
for (const defs of index.symbols.values()) {
|
|
1779
|
-
for (const candidate of defs) {
|
|
1780
|
-
if (candidate.file === interfaceDef.file &&
|
|
1781
|
-
candidate.className === interfaceDef.name &&
|
|
1782
|
-
candidate.type === 'method' &&
|
|
1783
|
-
candidate.startLine >= interfaceDef.startLine &&
|
|
1784
|
-
candidate.startLine <= interfaceDef.endLine) out.push(candidate);
|
|
1785
|
-
}
|
|
2407
|
+
const context = {
|
|
2408
|
+
file: ref.file,
|
|
2409
|
+
line: ref.line,
|
|
2410
|
+
namespace,
|
|
2411
|
+
className: enclosing?.className || null,
|
|
2412
|
+
};
|
|
2413
|
+
let resolved;
|
|
2414
|
+
try {
|
|
2415
|
+
resolved = resolveQualifier(index, context, String(ref.receiver), includeClosure(index, ref.file));
|
|
2416
|
+
} catch {
|
|
2417
|
+
return 'review';
|
|
1786
2418
|
}
|
|
1787
|
-
return
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
const out = [];
|
|
1793
|
-
for (const definitions of index.symbols.values()) {
|
|
1794
|
-
for (const candidate of definitions) {
|
|
1795
|
-
if (candidate.file === typeDef.file &&
|
|
1796
|
-
candidate.className === typeDef.name &&
|
|
1797
|
-
candidate.type === 'field' && candidate.fieldType &&
|
|
1798
|
-
candidate.startLine >= typeDef.startLine &&
|
|
1799
|
-
candidate.startLine <= typeDef.endLine) {
|
|
1800
|
-
out.push(String(candidate.fieldType).trim());
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
2419
|
+
if (!resolved || resolved.kind === 'unknown') return 'review';
|
|
2420
|
+
const owner = def.className || (def.receiver ? String(def.receiver).replace(/^\*/, '') : null);
|
|
2421
|
+
if (resolved.kind === 'namespace') {
|
|
2422
|
+
if (owner) return null;
|
|
2423
|
+
return (resolved.resolvedNamespace || resolved.namespace) === effectiveNamespace(index, def) ? 'edit' : null;
|
|
1803
2424
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
* unresolved embedded interface makes the set open, so the caller abstains.
|
|
1810
|
-
*/
|
|
1811
|
-
function goInterfaceRequirements(index, interfaceDef, memo, visiting = new Set()) {
|
|
1812
|
-
const key = `${interfaceDef.file}\0${interfaceDef.name}`;
|
|
1813
|
-
if (memo.has(key)) return memo.get(key);
|
|
1814
|
-
if (visiting.has(key) || interfaceDef.generics) return null;
|
|
1815
|
-
visiting.add(key);
|
|
1816
|
-
const requirements = new Map();
|
|
1817
|
-
for (const member of goInterfaceMembers(index, interfaceDef)) {
|
|
1818
|
-
const fingerprint = goMethodFingerprint(member);
|
|
1819
|
-
if (!fingerprint) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1820
|
-
const previous = requirements.get(member.name);
|
|
1821
|
-
if (previous && previous.fingerprint !== fingerprint) {
|
|
1822
|
-
visiting.delete(key); memo.set(key, null); return null;
|
|
1823
|
-
}
|
|
1824
|
-
requirements.set(member.name, { fingerprint, defs: [member] });
|
|
1825
|
-
}
|
|
1826
|
-
for (const rawParent of goEmbeddedTypes(index, interfaceDef)) {
|
|
1827
|
-
if (rawParent.includes('.')) {
|
|
1828
|
-
visiting.delete(key); memo.set(key, null); return null;
|
|
1829
|
-
}
|
|
1830
|
-
const parent = (index.symbols.get(rawParent) || []).find(candidate =>
|
|
1831
|
-
candidate.type === 'interface' &&
|
|
1832
|
-
require('path').dirname(candidate.file || '') ===
|
|
1833
|
-
require('path').dirname(interfaceDef.file || ''));
|
|
1834
|
-
if (!parent) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1835
|
-
const inherited = goInterfaceRequirements(index, parent, memo, visiting);
|
|
1836
|
-
if (!inherited) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1837
|
-
for (const [name, requirement] of inherited) {
|
|
1838
|
-
const previous = requirements.get(name);
|
|
1839
|
-
if (previous && previous.fingerprint !== requirement.fingerprint) {
|
|
1840
|
-
visiting.delete(key); memo.set(key, null); return null;
|
|
1841
|
-
}
|
|
1842
|
-
if (!previous) requirements.set(name, requirement);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
visiting.delete(key);
|
|
1846
|
-
const result = requirements.size > 0 ? requirements : null;
|
|
1847
|
-
memo.set(key, result);
|
|
1848
|
-
return result;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
/**
|
|
1852
|
-
* Method set for a project Go named type, including promoted embedded methods.
|
|
1853
|
-
* Unknown/external embeddings make the set open and force abstention.
|
|
1854
|
-
*/
|
|
1855
|
-
function goConcreteMethodSet(index, typeDef, memo, visiting = new Set()) {
|
|
1856
|
-
const key = goTypeIdentity(typeDef.name, typeDef.file);
|
|
1857
|
-
if (memo.has(key)) return memo.get(key);
|
|
1858
|
-
if (visiting.has(key) || typeDef.generics) return null;
|
|
1859
|
-
visiting.add(key);
|
|
1860
|
-
const methods = new Map();
|
|
1861
|
-
const directNames = new Set();
|
|
1862
|
-
const dir = require('path').dirname(typeDef.file || '');
|
|
1863
|
-
for (const [name, defs] of index.symbols) {
|
|
1864
|
-
const owned = defs.filter(candidate =>
|
|
1865
|
-
!NON_CALLABLE_TYPES.has(candidate.type) &&
|
|
1866
|
-
candidate.className === typeDef.name && candidate.file &&
|
|
1867
|
-
require('path').dirname(candidate.file) === dir &&
|
|
1868
|
-
candidate.type !== 'method');
|
|
1869
|
-
for (const method of owned) {
|
|
1870
|
-
const fingerprint = goMethodFingerprint(method);
|
|
1871
|
-
if (!fingerprint) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1872
|
-
const previous = methods.get(name);
|
|
1873
|
-
if (previous && previous.fingerprint !== fingerprint) {
|
|
1874
|
-
visiting.delete(key); memo.set(key, null); return null;
|
|
1875
|
-
}
|
|
1876
|
-
if (!previous) methods.set(name, { fingerprint, defs: [method] });
|
|
1877
|
-
else previous.defs.push(method);
|
|
1878
|
-
directNames.add(name);
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
for (const rawParent of goEmbeddedTypes(index, typeDef)) {
|
|
1882
|
-
if (rawParent.includes('.')) {
|
|
1883
|
-
visiting.delete(key); memo.set(key, null); return null;
|
|
1884
|
-
}
|
|
1885
|
-
const parent = goTypeDefinition(index, rawParent.replace(/^\*/, ''), typeDef.file);
|
|
1886
|
-
if (!parent) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1887
|
-
const promoted = goConcreteMethodSet(index, parent, memo, visiting);
|
|
1888
|
-
if (!promoted) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1889
|
-
for (const [name, method] of promoted) {
|
|
1890
|
-
// A direct method shadows a promoted one. Two promoted methods of
|
|
1891
|
-
// the same name are ambiguous in Go; abstain instead of coupling.
|
|
1892
|
-
if (methods.has(name)) {
|
|
1893
|
-
if (directNames.has(name)) continue;
|
|
1894
|
-
visiting.delete(key); memo.set(key, null); return null;
|
|
1895
|
-
}
|
|
1896
|
-
methods.set(name, method);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
visiting.delete(key);
|
|
1900
|
-
memo.set(key, methods);
|
|
1901
|
-
return methods;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
function goMethodSetSatisfies(methods, requirements) {
|
|
1905
|
-
if (!methods || !requirements) return false;
|
|
1906
|
-
for (const [name, requirement] of requirements) {
|
|
1907
|
-
if (methods.get(name)?.fingerprint !== requirement.fingerprint) return false;
|
|
2425
|
+
if (resolved.kind === 'type') {
|
|
2426
|
+
if (!owner) return null;
|
|
2427
|
+
const classes = resolved.classes || [];
|
|
2428
|
+
if (classes.length === 0) return 'review';
|
|
2429
|
+
return classes.some(cls => cls.name === String(owner).replace(/<.*$/, '')) ? 'edit' : null;
|
|
1908
2430
|
}
|
|
1909
|
-
return
|
|
2431
|
+
return 'review';
|
|
1910
2432
|
}
|
|
1911
2433
|
|
|
1912
2434
|
function goMethodArityCompatible(def, argCount) {
|
|
@@ -1935,125 +2457,500 @@ function goSlotCoversMethodAmbiguity(index, name, slot, raw) {
|
|
|
1935
2457
|
slot.memberIdentity.has(`${require('path').resolve(definition.file)}\0${definition.startLine}`));
|
|
1936
2458
|
}
|
|
1937
2459
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
* member. Go satisfaction is implicit, so this is a bipartite fixed point:
|
|
1941
|
-
* concrete types join interfaces whose complete method sets they satisfy,
|
|
1942
|
-
* and every satisfier of a joined interface joins the same compiler slot.
|
|
1943
|
-
*/
|
|
1944
|
-
function goInterfaceRenameClosure(index, targetDef) {
|
|
1945
|
-
if (!targetDef.className || goMethodFingerprint(targetDef) == null) return null;
|
|
1946
|
-
const pathMod = require('path');
|
|
1947
|
-
const interfaceMemo = new Map();
|
|
1948
|
-
const methodMemo = new Map();
|
|
1949
|
-
const interfaces = [];
|
|
1950
|
-
for (const defs of index.symbols.values()) {
|
|
1951
|
-
for (const candidate of defs) {
|
|
1952
|
-
if (candidate.type !== 'interface') continue;
|
|
1953
|
-
const requirements = goInterfaceRequirements(index, candidate, interfaceMemo);
|
|
1954
|
-
const directTarget = goInterfaceMembers(index, candidate).find(member =>
|
|
1955
|
-
member.name === targetDef.name &&
|
|
1956
|
-
goMethodFingerprint(member) === goMethodFingerprint(targetDef));
|
|
1957
|
-
if (requirements && directTarget) {
|
|
1958
|
-
interfaces.push({
|
|
1959
|
-
key: `${candidate.file}\0${candidate.name}`,
|
|
1960
|
-
def: candidate,
|
|
1961
|
-
requirements,
|
|
1962
|
-
targetDefs: [directTarget],
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
if (interfaces.length === 0) return null;
|
|
2460
|
+
const IMPORT_STATEMENT_TYPES = new Set(['import_statement', 'import_from_statement',
|
|
2461
|
+
'future_import_statement', 'use_declaration', 'import_declaration', 'using_directive', 'export_statement']);
|
|
1968
2462
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
concreteByKey.set(key, node);
|
|
2463
|
+
/** The first line of the import statement holding the token at
|
|
2464
|
+
* line/column (1-based line, 0-based column), or `line` when none does. */
|
|
2465
|
+
function importStatementLine(index, filePath, line, column) {
|
|
2466
|
+
const entry = index.files.get(filePath);
|
|
2467
|
+
if (!entry || !Number.isInteger(column)) return line;
|
|
2468
|
+
let tree;
|
|
2469
|
+
try {
|
|
2470
|
+
const content = index._readFile(filePath);
|
|
2471
|
+
tree = index._getParsedTree?.(filePath, content, entry.language) ||
|
|
2472
|
+
safeParse(getParser(entry.language), content);
|
|
2473
|
+
} catch {
|
|
2474
|
+
tree = null;
|
|
1982
2475
|
}
|
|
2476
|
+
let node = tree?.rootNode?.descendantForPosition({ row: line - 1, column });
|
|
2477
|
+
while (node && !IMPORT_STATEMENT_TYPES.has(node.type)) node = node.parent;
|
|
2478
|
+
return node ? node.startPosition.row + 1 : line;
|
|
2479
|
+
}
|
|
1983
2480
|
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2481
|
+
/** Is a Go site file compiled under another build constraint than the
|
|
2482
|
+
* pin's file (fix #400)? A file every build compiles is in every
|
|
2483
|
+
* configuration. */
|
|
2484
|
+
function goOtherConfiguration(index, pinFile, siteFile) {
|
|
2485
|
+
const { _goFileConstraint } = require('./callers');
|
|
2486
|
+
const site = siteFile ? _goFileConstraint(index, siteFile) : '';
|
|
2487
|
+
return !!site && site !== _goFileConstraint(index, pinFile);
|
|
2488
|
+
}
|
|
1988
2489
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2490
|
+
/**
|
|
2491
|
+
* fix #396: other external-linkage definitions of a C/C++ free function
|
|
2492
|
+
* `def` in other files (same C++ namespace and parameter types, same
|
|
2493
|
+
* language branch). `defines`: a declaration in the pin's identity group is
|
|
2494
|
+
* in the definition's include closure (both define the declared function);
|
|
2495
|
+
* `unrelated`: none is.
|
|
2496
|
+
*/
|
|
2497
|
+
function cLinkAlternatives(index, def, definitions) {
|
|
2498
|
+
const none = { defines: [], unrelated: [] };
|
|
2499
|
+
const language = index.files.get(def.file)?.language;
|
|
2500
|
+
if (!langTraits(language)?.textualIncludes || def.className || def.receiver || def.isSignature ||
|
|
2501
|
+
def.type !== 'function' || def.modifiers?.includes('static') || def.friendOf) return none;
|
|
2502
|
+
const { _closeCallableIdentityGroup, _cFamilySignatureKey, _textualIncludeClosures } = require('./callers');
|
|
2503
|
+
const { effectiveNamespace } = require('./cpp-scope');
|
|
2504
|
+
const group = new Set(_closeCallableIdentityGroup(index, [def], definitions));
|
|
2505
|
+
const prototypes = [...group].filter(member => member.isSignature);
|
|
2506
|
+
const overloads = langTraits(language)?.hasArityOverloads;
|
|
2507
|
+
const key = d => `${overloads ? effectiveNamespace(index, d) : ''}\0${overloads ? _cFamilySignatureKey(d) : ''}\0${d.languageBranch || ''}`;
|
|
2508
|
+
const pinKey = key(def);
|
|
2509
|
+
const out = { defines: [], unrelated: [] };
|
|
2510
|
+
for (const other of definitions) {
|
|
2511
|
+
if (other === def || group.has(other) || other.file === def.file || other.type !== 'function' ||
|
|
2512
|
+
other.isSignature || other.className || other.receiver || other.friendOf ||
|
|
2513
|
+
other.modifiers?.includes('static') || other.generatedByMacro ||
|
|
2514
|
+
!langTraits(index.files.get(other.file)?.language)?.textualIncludes || key(other) !== pinKey) continue;
|
|
2515
|
+
const closure = _textualIncludeClosures(index, other.file).all;
|
|
2516
|
+
const declared = prototypes.some(prototype => prototype.file === other.file || closure.has(prototype.file));
|
|
2517
|
+
(declared ? out.defines : out.unrelated).push(other);
|
|
2005
2518
|
}
|
|
2006
|
-
|
|
2519
|
+
return out;
|
|
2520
|
+
}
|
|
2007
2521
|
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
memberDefs.push(...node.targetDefs);
|
|
2014
|
-
}
|
|
2015
|
-
for (const node of concreteByKey.values()) {
|
|
2016
|
-
if (joinedConcrete.has(node.key)) memberDefs.push(...node.targetDefs);
|
|
2017
|
-
}
|
|
2018
|
-
const seen = new Set();
|
|
2019
|
-
const uniqueMembers = memberDefs.filter(member => {
|
|
2020
|
-
const key = `${pathMod.resolve(member.file)}\0${member.startLine}`;
|
|
2021
|
-
if (seen.has(key)) return false;
|
|
2022
|
-
seen.add(key);
|
|
2023
|
-
return true;
|
|
2024
|
-
});
|
|
2025
|
-
return {
|
|
2026
|
-
interfaceNames,
|
|
2027
|
-
memberDefs: uniqueMembers,
|
|
2028
|
-
memberIdentity: new Set(uniqueMembers.map(member =>
|
|
2029
|
-
`${pathMod.resolve(member.file)}\0${member.startLine}`)),
|
|
2030
|
-
};
|
|
2522
|
+
// C: plain identifiers of one spelling at file scope name one entity
|
|
2523
|
+
// (no overloading, no namespaces).
|
|
2524
|
+
function cExternalReferenceLanguage(index, file) {
|
|
2525
|
+
const traits = langTraits(index.files.get(file)?.language);
|
|
2526
|
+
return !!traits?.textualIncludes && !traits.hasArityOverloads;
|
|
2031
2527
|
}
|
|
2032
2528
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2529
|
+
/**
|
|
2530
|
+
* fix #396: where a file-scope C reference to NAME in another file than the
|
|
2531
|
+
* pin resolves. The file's own definitions or declarations of the name that
|
|
2532
|
+
* are not the pin decide first (a static function or object of its own);
|
|
2533
|
+
* otherwise the external-linkage function the unit links is the pin when the
|
|
2534
|
+
* link verdict keeps it and a declaration of it is visible ('edit'), several
|
|
2535
|
+
* link candidates or no visible declaration review, another definition is
|
|
2536
|
+
* not the pin.
|
|
2537
|
+
*/
|
|
2538
|
+
function cExternalReferenceVerdict(index, ref, def, pinIdentity, definitions) {
|
|
2539
|
+
if (def.className || def.receiver || def.modifiers?.includes('static')) return null;
|
|
2540
|
+
const own = (index.files.get(ref.file)?.symbols || []).filter(symbol => symbol.name === def.name);
|
|
2541
|
+
if (own.some(symbol => !pinIdentity.has(symbol))) return null;
|
|
2542
|
+
const { _textualLinkVerdict, _textualIncludeClosures } = require('./callers');
|
|
2543
|
+
const verdict = _textualLinkVerdict(index, ref.file, [...pinIdentity], definitions, ref.line);
|
|
2544
|
+
if (verdict === 'other') return null;
|
|
2545
|
+
if (verdict && verdict.ambiguous) return 'review';
|
|
2546
|
+
const { strong } = _textualIncludeClosures(index, ref.file);
|
|
2547
|
+
const declared = own.length > 0 || [...pinIdentity].some(member => strong.has(member.file));
|
|
2548
|
+
return declared ? 'edit' : 'review';
|
|
2549
|
+
}
|
|
2040
2550
|
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2551
|
+
/**
|
|
2552
|
+
* Type-reference completion of a type rename (fix #386). Every name token on
|
|
2553
|
+
* the ground lines is decided by core/type-references.js; this merges the
|
|
2554
|
+
* decisions into the plan's changes:
|
|
2555
|
+
* - edits join an existing token edit on the line, replace a manual
|
|
2556
|
+
* review item another pass left on a line whose tokens are all decided,
|
|
2557
|
+
* or become new reference edits;
|
|
2558
|
+
* - tokens the lookup cannot settle make the line a review item with
|
|
2559
|
+
* their reason (a mixed line keeps its certain edits and is flagged);
|
|
2560
|
+
* - a line holding the name outside every parsed token and outside
|
|
2561
|
+
* comments/strings (macro bodies, unparsed regions) is a review item;
|
|
2562
|
+
* - reference-review items of other passes on lines whose tokens provably
|
|
2563
|
+
* name something else are dropped, and unverified call candidates on
|
|
2564
|
+
* decided lines leave the band (claimed by the account instead).
|
|
2565
|
+
*/
|
|
2566
|
+
function applyTypeReferencePass(index, ctx) {
|
|
2567
|
+
const { def, name, renameTo, changes, reviewItems, planUnverified } = ctx;
|
|
2568
|
+
const { typeReferenceSites } = require('./type-references');
|
|
2569
|
+
const { files } = typeReferenceSites(index, def, name, ctx.groundSet);
|
|
2570
|
+
const decided = new Map(); // rel:line -> { abs, edited }
|
|
2571
|
+
const reviewed = new Map(); // rel:line -> { abs, reason }
|
|
2572
|
+
const stringEdited = new Set();
|
|
2573
|
+
const target = `${def.relativePath || def.file}:${def.nameLine || def.startLine}`;
|
|
2574
|
+
const reasonText = reason => String(reason || 'unresolved').replace(/-/g, ' ');
|
|
2575
|
+
const special = change => change.contractDependency || change.templateDependency ||
|
|
2576
|
+
change.editKind === 'example' || change.editKind === 'macro-expansion';
|
|
2577
|
+
// Rebuild a line edit from code-unit token columns.
|
|
2578
|
+
const lineEdit = (raw, columns) => {
|
|
2579
|
+
const sorted = [...columns].sort((a, b) => b - a);
|
|
2580
|
+
let renamed = raw;
|
|
2581
|
+
for (const column of sorted) {
|
|
2582
|
+
if (renamed.slice(column, column + name.length) !== name) continue;
|
|
2583
|
+
renamed = renamed.slice(0, column) + renameTo + renamed.slice(column + name.length);
|
|
2584
|
+
}
|
|
2585
|
+
return {
|
|
2586
|
+
source: raw.trim(), renamed: renamed.trim(), count: sorted.length,
|
|
2587
|
+
columns: sorted, rawSource: raw, oldName: name, newName: renameTo,
|
|
2588
|
+
};
|
|
2589
|
+
};
|
|
2590
|
+
for (const [abs, info] of files) {
|
|
2591
|
+
const rel = info.relativePath;
|
|
2592
|
+
const fileLines = index._getFileLines(abs);
|
|
2593
|
+
for (const [line, slot] of [...info.lines].sort((a, b) => a[0] - b[0])) {
|
|
2594
|
+
const key = `${rel}:${line}`;
|
|
2595
|
+
const at = changes.findIndex(change => change.file === rel && change.line === line);
|
|
2596
|
+
const existing = at >= 0 ? changes[at] : null;
|
|
2597
|
+
const raw = fileLines[line - 1] || '';
|
|
2598
|
+
const source = raw.trim();
|
|
2599
|
+
if (slot.tokens === 0 && slot.reviews.length === 0) {
|
|
2600
|
+
if (slot.text && !slot.macroBody) continue;
|
|
2601
|
+
if (existing) continue;
|
|
2602
|
+
const reason = slot.unparsedReason || (slot.macroBody ? 'macro-body' : 'unparsed-occurrence');
|
|
2603
|
+
reviewed.set(key, { abs, reason });
|
|
2604
|
+
changes.push({
|
|
2605
|
+
file: rel,
|
|
2606
|
+
line,
|
|
2607
|
+
expression: source,
|
|
2608
|
+
suggestion: `"${name}" appears here outside the parsed code (${reasonText(reason)}); ` +
|
|
2609
|
+
`rename it if it names ${name} at ${target}`,
|
|
2610
|
+
needsReview: true,
|
|
2611
|
+
reviewReason: reason,
|
|
2612
|
+
editKind: 'reference',
|
|
2613
|
+
});
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
const units = columns => columns.map(column => tokenColumn(raw, column, name))
|
|
2617
|
+
.filter(Number.isInteger);
|
|
2618
|
+
const editUnits = units(slot.edits);
|
|
2619
|
+
const skipUnits = new Set(units(slot.skipColumns || []));
|
|
2620
|
+
const reviewList = slot.reviews.map(review => ({
|
|
2621
|
+
...review,
|
|
2622
|
+
unit: Number.isInteger(review.column) ? tokenColumn(raw, review.column, name) : null,
|
|
2623
|
+
}));
|
|
2624
|
+
// An existing edit of this line: the type lookup decides every
|
|
2625
|
+
// token spelling the name. A token the other pass edits and the
|
|
2626
|
+
// lookup cannot settle stays edited and is flagged; one the
|
|
2627
|
+
// lookup proves to name another declaration (or a macro argument
|
|
2628
|
+
// its replacement list uses in another role) leaves the edit,
|
|
2629
|
+
// unless the edit is the pin's own declaration or one a
|
|
2630
|
+
// dedicated pass proved.
|
|
2631
|
+
if (existing && existing.newExpression !== undefined && !special(existing)) {
|
|
2632
|
+
const prior = existing._edit && existing._edit.rawSource === raw ? existing._edit.columns
|
|
2633
|
+
: renameIdentifierTokens(index, abs, line, name, renameTo).columns || [];
|
|
2634
|
+
const trusted = (existing.isDefinition || existing._evidence) &&
|
|
2635
|
+
!reviewList.some(review => review.reason === 'macro-argument-other-use');
|
|
2636
|
+
const hardUnits = new Set(reviewList.filter(review => review.reason === 'macro-argument-other-use')
|
|
2637
|
+
.map(review => review.unit).filter(Number.isInteger));
|
|
2638
|
+
const keep = trusted ? prior
|
|
2639
|
+
: prior.filter(column => !skipUnits.has(column) && !hardUnits.has(column));
|
|
2640
|
+
const final = new Set([...keep, ...editUnits]);
|
|
2641
|
+
const open = reviewList.filter(review => review.unit == null || !(trusted && final.has(review.unit)));
|
|
2642
|
+
if (final.size === 0) {
|
|
2643
|
+
if (open.length === 0) {
|
|
2644
|
+
changes.splice(at, 1);
|
|
2645
|
+
decided.set(key, { abs, edited: false });
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
delete existing.newExpression;
|
|
2649
|
+
existing.needsReview = true;
|
|
2650
|
+
existing.reviewReason = open[0].reason;
|
|
2651
|
+
existing.suggestion = `Verify whether "${name}" here names ${name} at ${target} ` +
|
|
2652
|
+
`(${reasonText(open[0].reason)}) and rename it if it does`;
|
|
2653
|
+
reviewed.set(key, { abs, reason: open[0].reason });
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
const edit = lineEdit(raw, final);
|
|
2657
|
+
const previous = existing.newExpression;
|
|
2658
|
+
existing.expression = edit.source;
|
|
2659
|
+
existing.newExpression = edit.renamed;
|
|
2660
|
+
if (typeof existing.suggestion === 'string' && previous && existing.suggestion.includes(previous)) {
|
|
2661
|
+
existing.suggestion = existing.suggestion.replace(previous, edit.renamed);
|
|
2662
|
+
}
|
|
2663
|
+
tagTokenEdit(existing, edit);
|
|
2664
|
+
if (open.length > 0) {
|
|
2665
|
+
existing.needsReview = true;
|
|
2666
|
+
existing.reviewReason = existing.reviewReason || open[0].reason;
|
|
2667
|
+
existing.suggestion += `; verify the "${name}" token(s) UCN could not resolve ` +
|
|
2668
|
+
`(${reasonText(open[0].reason)})`;
|
|
2669
|
+
reviewed.set(key, { abs, reason: open[0].reason });
|
|
2670
|
+
} else {
|
|
2671
|
+
// A manual flag another pass left because it could not
|
|
2672
|
+
// place a token is settled once every token is decided.
|
|
2673
|
+
if (existing.needsReview && !existing.reviewReason &&
|
|
2674
|
+
final.size + skipUnits.size >= slot.tokens) delete existing.needsReview;
|
|
2675
|
+
decided.set(key, { abs, edited: true });
|
|
2676
|
+
if (slot.stringAnnotation) stringEdited.add(key);
|
|
2677
|
+
}
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const certain = reviewList.length === 0;
|
|
2681
|
+
const edit = editUnits.length > 0 ? lineEdit(raw, editUnits) : null;
|
|
2682
|
+
const hasEdit = !!edit && edit.renamed !== edit.source;
|
|
2683
|
+
if (certain) {
|
|
2684
|
+
decided.set(key, { abs, edited: hasEdit });
|
|
2685
|
+
if (slot.stringAnnotation && hasEdit) stringEdited.add(key);
|
|
2686
|
+
} else {
|
|
2687
|
+
reviewed.set(key, { abs, reason: reviewList[0].reason });
|
|
2688
|
+
}
|
|
2689
|
+
const reviewNote = certain ? '' : `; verify the "${name}" token(s) UCN could not resolve ` +
|
|
2690
|
+
`(${reasonText(reviewList[0].reason)})`;
|
|
2691
|
+
if (existing) {
|
|
2692
|
+
// A manual review item another pass left on this line.
|
|
2693
|
+
if (existing.newExpression === undefined && existing.needsReview && !special(existing)) {
|
|
2694
|
+
if (certain && hasEdit) {
|
|
2695
|
+
const kind = existing.editKind === 'call' ? 'call' : 'reference';
|
|
2696
|
+
for (const field of Object.keys(existing)) delete existing[field];
|
|
2697
|
+
Object.assign(existing, {
|
|
2698
|
+
file: rel,
|
|
2699
|
+
line,
|
|
2700
|
+
expression: edit.source,
|
|
2701
|
+
suggestion: `${kind === 'call' ? 'Rename to' : 'Update type reference'}: ${edit.renamed}`,
|
|
2702
|
+
newExpression: edit.renamed,
|
|
2703
|
+
editKind: kind,
|
|
2704
|
+
});
|
|
2705
|
+
tagTokenEdit(existing, edit);
|
|
2706
|
+
} else if (certain) {
|
|
2707
|
+
// Every token here provably names another declaration.
|
|
2708
|
+
changes.splice(at, 1);
|
|
2709
|
+
} else if (typeof existing.suggestion === 'string' && !existing.suggestion.includes(reviewNote)) {
|
|
2710
|
+
existing.reviewReason = existing.reviewReason || reviewList[0].reason;
|
|
2711
|
+
existing.suggestion += reviewNote;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
if (hasEdit) {
|
|
2717
|
+
changes.push(tagTokenEdit({
|
|
2718
|
+
file: rel,
|
|
2719
|
+
line,
|
|
2720
|
+
expression: edit.source,
|
|
2721
|
+
suggestion: `Update type reference: ${edit.renamed}${reviewNote}`,
|
|
2722
|
+
newExpression: edit.renamed,
|
|
2723
|
+
editKind: 'reference',
|
|
2724
|
+
...(!certain && { needsReview: true, reviewReason: reviewList[0].reason }),
|
|
2725
|
+
}, edit));
|
|
2726
|
+
} else if (!certain) {
|
|
2727
|
+
changes.push({
|
|
2728
|
+
file: rel,
|
|
2729
|
+
line,
|
|
2730
|
+
expression: source,
|
|
2731
|
+
suggestion: `Verify whether "${name}" here names ${name} at ${target} ` +
|
|
2732
|
+
`(${reasonText(reviewList[0].reason)}) and rename it if it does`,
|
|
2733
|
+
needsReview: true,
|
|
2734
|
+
reviewReason: reviewList[0].reason,
|
|
2735
|
+
editKind: 'reference',
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
// C# applies `XAttribute` as `[X]` (fix #386).
|
|
2741
|
+
const { csharpAttributeShortSites } = require('./type-references');
|
|
2742
|
+
const suffix = 'Attribute';
|
|
2743
|
+
for (const site of csharpAttributeShortSites(index, def, name)) {
|
|
2744
|
+
if (site.verdict === 'no') continue;
|
|
2745
|
+
const short = name.slice(0, -suffix.length);
|
|
2746
|
+
const newShort = renameTo.endsWith(suffix) && renameTo.length > suffix.length
|
|
2747
|
+
? renameTo.slice(0, -suffix.length) : renameTo;
|
|
2748
|
+
const raw = index._getFileLines(site.file)[site.line - 1] || '';
|
|
2749
|
+
const column = tokenColumn(raw, site.column, short);
|
|
2750
|
+
const existing = changes.find(change => change.file === site.relativePath && change.line === site.line);
|
|
2751
|
+
if (site.verdict === 'yes' && Number.isInteger(column) && !existing) {
|
|
2752
|
+
const renamed = raw.slice(0, column) + newShort + raw.slice(column + short.length);
|
|
2753
|
+
changes.push({
|
|
2754
|
+
file: site.relativePath,
|
|
2755
|
+
line: site.line,
|
|
2756
|
+
expression: raw.trim(),
|
|
2757
|
+
suggestion: `Update attribute usage: ${renamed.trim()}`,
|
|
2758
|
+
newExpression: renamed.trim(),
|
|
2759
|
+
editKind: 'reference',
|
|
2760
|
+
});
|
|
2761
|
+
continue;
|
|
2762
|
+
}
|
|
2763
|
+
const reason = site.verdict === 'yes' ? 'attribute-short-name' : site.reason || 'attribute-short-name';
|
|
2764
|
+
if (existing) {
|
|
2765
|
+
existing.needsReview = true;
|
|
2766
|
+
existing.reviewReason = existing.reviewReason || reason;
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
changes.push({
|
|
2770
|
+
file: site.relativePath,
|
|
2771
|
+
line: site.line,
|
|
2772
|
+
expression: raw.trim(),
|
|
2773
|
+
suggestion: `Verify whether [${short}] applies ${name} at ${target} and rename it to [${newShort}] if it does`,
|
|
2774
|
+
needsReview: true,
|
|
2775
|
+
reviewReason: reason,
|
|
2776
|
+
editKind: 'reference',
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
// String annotations the pass edited are no longer text-only review.
|
|
2780
|
+
if (stringEdited.size > 0) {
|
|
2781
|
+
for (let i = reviewItems.length - 1; i >= 0; i--) {
|
|
2782
|
+
const item = reviewItems[i];
|
|
2783
|
+
if (item.textDependency && stringEdited.has(`${item.file}:${item.line}`)) reviewItems.splice(i, 1);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
// Unverified call candidates on decided lines are settled.
|
|
2787
|
+
const removed = [];
|
|
2788
|
+
const kept = [];
|
|
2789
|
+
for (const site of planUnverified) {
|
|
2790
|
+
const key = `${site.file}:${site.line}`;
|
|
2791
|
+
if (decided.has(key)) removed.push(site);
|
|
2792
|
+
else kept.push(site);
|
|
2793
|
+
}
|
|
2794
|
+
if (removed.length > 0) {
|
|
2795
|
+
planUnverified.length = 0;
|
|
2796
|
+
planUnverified.push(...kept);
|
|
2797
|
+
}
|
|
2798
|
+
return { decided, reviewed, removed };
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* Object identity of a one-hop member-assignment definition (fix #359, ky-
|
|
2803
|
+
* measured): `response.json = async () => {...}` patches a property of the
|
|
2804
|
+
* object `response` names. It is a project declaration only when that object
|
|
2805
|
+
* is itself a project binding: a module-scope declaration or import of the
|
|
2806
|
+
* file (`const api = {}; api.get = ...`, `exports.h = ...`). A receiver bound
|
|
2807
|
+
* inside an enclosing function (parameter or local) or not declared at all
|
|
2808
|
+
* (a runtime global) names an object whose property contract belongs to its
|
|
2809
|
+
* own type, typically external (a Fetch `Response`); renaming it is not a
|
|
2810
|
+
* mechanical project rename. Returns 'module' | 'local' | 'external', or null
|
|
2811
|
+
* when the definition is not a receiver-named member assignment.
|
|
2812
|
+
*/
|
|
2813
|
+
const FUNCTION_SCOPE_TYPES = new Set([
|
|
2814
|
+
'function_declaration', 'function_expression', 'function', 'arrow_function',
|
|
2815
|
+
'method_definition', 'generator_function', 'generator_function_declaration',
|
|
2816
|
+
]);
|
|
2817
|
+
function memberAssignmentOwnerScope(index, def) {
|
|
2818
|
+
if (!def || !def.memberAssigned || def.className || !def.assignedReceiver) return null;
|
|
2819
|
+
const receiver = def.assignedReceiver;
|
|
2820
|
+
if (receiver === 'exports' || receiver === 'module') return 'module';
|
|
2821
|
+
const language = index.files.get(def.file)?.language;
|
|
2822
|
+
const parser = language && getParser(language);
|
|
2823
|
+
if (!parser) return null;
|
|
2824
|
+
const content = index._readFile(def.file);
|
|
2825
|
+
const tree = index._getParsedTree?.(def.file, content, language) || safeParse(parser, content);
|
|
2826
|
+
if (!tree) return null;
|
|
2827
|
+
const row = def.startLine - 1;
|
|
2828
|
+
// The assignment node at the definition line.
|
|
2829
|
+
let assignment = null;
|
|
2830
|
+
const stack = [tree.rootNode];
|
|
2831
|
+
while (stack.length > 0 && !assignment) {
|
|
2832
|
+
const node = stack.pop();
|
|
2833
|
+
if (node.endPosition.row < row || node.startPosition.row > row) continue;
|
|
2834
|
+
if (node.type === 'assignment_expression' && node.startPosition.row === row) {
|
|
2835
|
+
const left = node.childForFieldName('left');
|
|
2836
|
+
const obj = left && left.type === 'member_expression'
|
|
2837
|
+
? left.childForFieldName('object') : null;
|
|
2838
|
+
const prop = left && left.childForFieldName('property');
|
|
2839
|
+
if (obj && obj.type === 'identifier' && obj.text === receiver &&
|
|
2840
|
+
prop && prop.text === def.name) {
|
|
2841
|
+
assignment = node;
|
|
2842
|
+
break;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
stack.push(...(node.namedChildren || []));
|
|
2846
|
+
}
|
|
2847
|
+
if (!assignment) return null;
|
|
2848
|
+
const declaresName = (node) => {
|
|
2849
|
+
// Binding identifiers introduced by a pattern / declarator subtree.
|
|
2850
|
+
const out = [];
|
|
2851
|
+
const walk = (n) => {
|
|
2852
|
+
if (!n) return;
|
|
2853
|
+
if ((n.type === 'identifier' || n.type === 'shorthand_property_identifier_pattern') &&
|
|
2854
|
+
n.text === receiver) { out.push(n); return; }
|
|
2855
|
+
if (n.type === 'default_value' || n.type === 'assignment_pattern') {
|
|
2856
|
+
walk(n.childForFieldName('left'));
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
for (const c of n.namedChildren || []) {
|
|
2860
|
+
if (c.type === 'type_annotation') continue;
|
|
2861
|
+
walk(c);
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
walk(node);
|
|
2865
|
+
return out.length > 0;
|
|
2866
|
+
};
|
|
2867
|
+
const scopeDeclares = (scope) => {
|
|
2868
|
+
if (FUNCTION_SCOPE_TYPES.has(scope.type)) {
|
|
2869
|
+
const params = scope.childForFieldName('parameters') ||
|
|
2870
|
+
scope.childForFieldName('parameter');
|
|
2871
|
+
if (params && declaresName(params)) return true;
|
|
2872
|
+
}
|
|
2873
|
+
const body = FUNCTION_SCOPE_TYPES.has(scope.type)
|
|
2874
|
+
? scope.childForFieldName('body') : scope;
|
|
2875
|
+
if (!body) return false;
|
|
2876
|
+
const stmts = [body];
|
|
2877
|
+
while (stmts.length > 0) {
|
|
2878
|
+
const n = stmts.pop();
|
|
2879
|
+
for (const c of n.namedChildren || []) {
|
|
2880
|
+
if (FUNCTION_SCOPE_TYPES.has(c.type) || c.type === 'class_declaration' ||
|
|
2881
|
+
c.type === 'class') {
|
|
2882
|
+
const nm = c.childForFieldName('name');
|
|
2883
|
+
if (nm && nm.text === receiver && c.type !== 'function_expression' &&
|
|
2884
|
+
c.type !== 'arrow_function' && c.type !== 'class') return true;
|
|
2885
|
+
continue;
|
|
2886
|
+
}
|
|
2887
|
+
if (c.type === 'variable_declarator') {
|
|
2888
|
+
if (declaresName(c.childForFieldName('name'))) return true;
|
|
2889
|
+
continue;
|
|
2890
|
+
}
|
|
2891
|
+
if (c.type === 'import_statement') {
|
|
2892
|
+
if (declaresName(c.namedChildren.find(x => x.type === 'import_clause'))) return true;
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
if (c.type === 'catch_clause') {
|
|
2896
|
+
if (declaresName(c.childForFieldName('parameter'))) return true;
|
|
2897
|
+
}
|
|
2898
|
+
if (c.type === 'for_in_statement') {
|
|
2899
|
+
if (declaresName(c.childForFieldName('left'))) return true;
|
|
2900
|
+
}
|
|
2901
|
+
stmts.push(c);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return false;
|
|
2905
|
+
};
|
|
2906
|
+
for (let scope = assignment.parent; scope; scope = scope.parent) {
|
|
2907
|
+
if (FUNCTION_SCOPE_TYPES.has(scope.type)) {
|
|
2908
|
+
if (scopeDeclares(scope)) return 'local';
|
|
2909
|
+
} else if (scope.type === 'program') {
|
|
2910
|
+
return scopeDeclares(scope) ? 'module' : 'external';
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
return 'external';
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
function plan(index, name, options = {}) {
|
|
2917
|
+
index._beginOp();
|
|
2918
|
+
// Every slot member of a rename is swept over the same files: their call
|
|
2919
|
+
// sites are analyzed on one tree per file for the whole plan (fix #395).
|
|
2920
|
+
const ownsTreeCache = !index._treeCache;
|
|
2921
|
+
if (ownsTreeCache) index._treeCache = new Map();
|
|
2922
|
+
try {
|
|
2923
|
+
const definitions = index.symbols.get(name);
|
|
2924
|
+
if (!definitions || definitions.length === 0) {
|
|
2925
|
+
return { found: false, function: name };
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
const resolved = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
2929
|
+
let def = resolved.def || definitions[0];
|
|
2930
|
+
// A constructor or destructor carries its class's name (Java, C#, C++):
|
|
2931
|
+
// renaming it is renaming the class (fix #386).
|
|
2932
|
+
// (A C# member can never carry its type's name except the finalizer.)
|
|
2933
|
+
if (options.renameTo && def.className === def.name && !isTypeRenamePin(def) &&
|
|
2934
|
+
(def.type === 'constructor' || def.memberType === 'constructor' ||
|
|
2935
|
+
(def.type === 'method' && index.files.get(def.file)?.language === 'csharp'))) {
|
|
2936
|
+
const owner = definitions.find(d => isTypeRenamePin(d) && d.file === def.file &&
|
|
2937
|
+
d.startLine <= def.startLine && d.endLine >= def.endLine);
|
|
2938
|
+
if (owner) def = owner;
|
|
2939
|
+
}
|
|
2940
|
+
// BUG-BY: enrich types for typed-arrow-fn declarations.
|
|
2941
|
+
const arrowTypes = extractArrowTypesFromVarDecl(index, def);
|
|
2942
|
+
// Class target: the signature being planned is the CONSTRUCTOR's
|
|
2943
|
+
// (fix #230, same rule as verify) — single-ctor classes only; with
|
|
2944
|
+
// overloads the class def's own (empty) list stays, since plan cannot
|
|
2945
|
+
// know which overload the user means.
|
|
2946
|
+
const planLang = index.files.get(def.file)?.language;
|
|
2947
|
+
const planCtorLists = _constructorParamLists(index, def, planLang);
|
|
2948
|
+
const currentParams = (planCtorLists && planCtorLists.length === 1 && planCtorLists[0]) ||
|
|
2949
|
+
(arrowTypes?.paramsStructured) || def.paramsStructured || [];
|
|
2950
|
+
// BUG-BV: render with TS-correct param formatting (`opt?: number`).
|
|
2951
|
+
const currentSignature = formatTypedSignature(def,
|
|
2952
|
+
(planCtorLists && planCtorLists.length === 1)
|
|
2953
|
+
? { paramsStructured: currentParams }
|
|
2057
2954
|
: arrowTypes ? {
|
|
2058
2955
|
paramsStructured: arrowTypes.paramsStructured,
|
|
2059
2956
|
returnType: arrowTypes.returnType
|
|
@@ -2063,8 +2960,13 @@ function plan(index, name, options = {}) {
|
|
|
2063
2960
|
// run contractedCallerSweep (v4 tiered contract), so plan and verify stay
|
|
2064
2961
|
// in lock-step by construction. Unverified candidates are NOT planned
|
|
2065
2962
|
// (they may target another symbol) but stay visible with reasons.
|
|
2066
|
-
const { sites: planCallSites, unverifiedSites: planUnverified, account:
|
|
2067
|
-
groundSet: planGroundSet } = computePlanCallSites(index, name, def
|
|
2963
|
+
const { sites: planCallSites, unverifiedSites: planUnverified, account: pinAccount,
|
|
2964
|
+
groundSet: planGroundSet, accountParts: planAccountParts } = computePlanCallSites(index, name, def,
|
|
2965
|
+
{ analyzeArgs: !options.renameTo });
|
|
2966
|
+
let planAccount = pinAccount;
|
|
2967
|
+
// Call sites a slot member's sweep contributed to this plan (fix #376):
|
|
2968
|
+
// the plan's ACCOUNT is rebuilt over the same sites it lists.
|
|
2969
|
+
const slotAccountSites = [];
|
|
2068
2970
|
const impactScopeWarning = computePlanScopeWarning(index, name, def, options);
|
|
2069
2971
|
|
|
2070
2972
|
// Reject ambiguous multi-op invocations rather than silently coalescing.
|
|
@@ -2110,6 +3012,12 @@ function plan(index, name, options = {}) {
|
|
|
2110
3012
|
let operation = null;
|
|
2111
3013
|
let changes = [];
|
|
2112
3014
|
const reviewItems = [];
|
|
3015
|
+
// Contract membership (fix #360): project slot roots the rename closed
|
|
3016
|
+
// over, and Go satisfaction sites the closure could not complete.
|
|
3017
|
+
const contractRootDefs = [];
|
|
3018
|
+
const goContractReviews = [];
|
|
3019
|
+
// Hierarchy slot closure of a rename (fix #376), for the contract check.
|
|
3020
|
+
let hierarchySlot = null;
|
|
2113
3021
|
let unchangedSites = 0;
|
|
2114
3022
|
|
|
2115
3023
|
if (options.addParam) {
|
|
@@ -2229,7 +3137,8 @@ function plan(index, name, options = {}) {
|
|
|
2229
3137
|
const lang = fileEntry?.language;
|
|
2230
3138
|
let selfOffset = 0;
|
|
2231
3139
|
const planSelfParams = langTraits(lang)?.selfParam;
|
|
2232
|
-
if (planSelfParams && currentParams.length > 0 && planSelfParams.includes(
|
|
3140
|
+
if (planSelfParams && currentParams.length > 0 && planSelfParams.includes(
|
|
3141
|
+
String(currentParams[0].name || '').replace(/'[A-Za-z_]\w*\s*/g, '').trim())) {
|
|
2233
3142
|
selfOffset = 1;
|
|
2234
3143
|
}
|
|
2235
3144
|
const callerArgIndex = paramIndex - selfOffset;
|
|
@@ -2266,6 +3175,8 @@ function plan(index, name, options = {}) {
|
|
|
2266
3175
|
}
|
|
2267
3176
|
}
|
|
2268
3177
|
|
|
3178
|
+
// The rename's usage records, shared with the method-group pass below.
|
|
3179
|
+
let planUsages = null;
|
|
2269
3180
|
if (options.renameTo) {
|
|
2270
3181
|
operation = 'rename';
|
|
2271
3182
|
newSignature = currentSignature.replace(new RegExp('\\b' + escapeRegExp(name) + '\\b'), options.renameTo);
|
|
@@ -2280,9 +3191,95 @@ function plan(index, name, options = {}) {
|
|
|
2280
3191
|
// exact token on the SAME line (`a.Run() || b.Run() || other.Run()`)
|
|
2281
3192
|
// and update one edit instead of losing it to line-level deduplication.
|
|
2282
3193
|
const emittedRenameCalls = new Map();
|
|
3194
|
+
// A definition edit on a line another change already edits joins
|
|
3195
|
+
// that change's token edit instead of being dropped (fix #376).
|
|
3196
|
+
const lineTaken = (member, rel, line) => {
|
|
3197
|
+
const existing = changes.find(change => change.file === rel && change.line === line);
|
|
3198
|
+
if (!existing) return false;
|
|
3199
|
+
if (existing.newExpression !== undefined && existing._edit) {
|
|
3200
|
+
const edit = renameIdentifierTokens(index, member.file, line, name,
|
|
3201
|
+
options.renameTo, null, null, { definitionNameOnly: true, definition: member });
|
|
3202
|
+
if (edit.renamed !== edit.source && mergeTokenEdit(existing, edit)) {
|
|
3203
|
+
existing.isDefinition = true;
|
|
3204
|
+
existing.editKind = 'definition';
|
|
3205
|
+
existing.suggestion = `Update definition: ${existing.newExpression}`;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
return true;
|
|
3209
|
+
};
|
|
3210
|
+
const emittedPatternKeys = new Set();
|
|
3211
|
+
const emitPatternKeyEdit = (absoluteFile, relativeFile, pattern) => {
|
|
3212
|
+
const key = `${relativeFile}:${pattern.line}:${pattern.column}`;
|
|
3213
|
+
if (emittedPatternKeys.has(key)) return;
|
|
3214
|
+
emittedPatternKeys.add(key);
|
|
3215
|
+
const replacement = pattern.shorthand
|
|
3216
|
+
? `${options.renameTo}: ${pattern.local}` : options.renameTo;
|
|
3217
|
+
const edit = renameIdentifierTokens(index, absoluteFile, pattern.line, name,
|
|
3218
|
+
replacement, [pattern.column], 1);
|
|
3219
|
+
if (edit.renamed === edit.source) {
|
|
3220
|
+
if (!reviewItems.some(item => item.file === relativeFile && item.line === pattern.line)) {
|
|
3221
|
+
reviewItems.push({
|
|
3222
|
+
file: relativeFile,
|
|
3223
|
+
line: pattern.line,
|
|
3224
|
+
expression: edit.source,
|
|
3225
|
+
suggestion: `Rename the destructured property "${name}" to "${options.renameTo}" ` +
|
|
3226
|
+
'manually, keeping the local binding',
|
|
3227
|
+
needsReview: true,
|
|
3228
|
+
editKind: 'reference',
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
const concrete = tagTokenEdit({
|
|
3234
|
+
file: relativeFile,
|
|
3235
|
+
line: pattern.line,
|
|
3236
|
+
expression: edit.source,
|
|
3237
|
+
suggestion: `Rename the destructured property: ${edit.renamed}`,
|
|
3238
|
+
newExpression: edit.renamed,
|
|
3239
|
+
editKind: 'reference',
|
|
3240
|
+
}, edit);
|
|
3241
|
+
const lineOwner = changes.find(change => change.file === relativeFile &&
|
|
3242
|
+
change.line === pattern.line && change.newExpression !== undefined && change._edit);
|
|
3243
|
+
if (!lineOwner || !mergeTokenEdit(lineOwner, edit)) changes.push(concrete);
|
|
3244
|
+
};
|
|
2283
3245
|
const emitRenameCallSites = (siteList) => {
|
|
2284
3246
|
const touched = new Set();
|
|
2285
3247
|
for (const incoming of siteList) {
|
|
3248
|
+
// A target-typed `new(..)` spells no name: the type written at
|
|
3249
|
+
// its declaration site is what the rename edits (fix #393).
|
|
3250
|
+
if (incoming.targetTyped) continue;
|
|
3251
|
+
// A call of a name destructured from the member (`const { run }
|
|
3252
|
+
// = make(); run()`) spells the member at the pattern key (fix
|
|
3253
|
+
// #397): the key is renamed and the local binding kept
|
|
3254
|
+
// (`{ run }` -> `{ runZ: run }`), so the call line is unchanged.
|
|
3255
|
+
if (incoming.destructured) {
|
|
3256
|
+
emitPatternKeyEdit(incoming.absoluteFile || incoming.file, incoming.file, incoming.destructured);
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
// A call spelled by an aliased import's local name keeps it; the
|
|
3260
|
+
// import's source side and the export chain carry the rename.
|
|
3261
|
+
if (incoming.importAliasSpelling) continue;
|
|
3262
|
+
// A pasted call target (`fs__##lc` with `XX(STAT, stat)`) is
|
|
3263
|
+
// not spelled on the invocation line: no token edit exists here,
|
|
3264
|
+
// and the rename is incomplete until the macro's arguments or
|
|
3265
|
+
// replacement list change with it (fix #362).
|
|
3266
|
+
if (incoming.macroExpansion?.origin === 'paste') {
|
|
3267
|
+
if (!reviewItems.some(item => item.file === incoming.file && item.line === incoming.line)) {
|
|
3268
|
+
reviewItems.push({
|
|
3269
|
+
file: incoming.file,
|
|
3270
|
+
line: incoming.line,
|
|
3271
|
+
expression: incoming.expression,
|
|
3272
|
+
suggestion: `Macro ${incoming.macroExpansion.macro} builds "${name}" by token pasting at this ` +
|
|
3273
|
+
'invocation; change the macro arguments or definition so the expansion spells ' +
|
|
3274
|
+
`"${options.renameTo}"`,
|
|
3275
|
+
needsReview: true,
|
|
3276
|
+
templateDependency: true,
|
|
3277
|
+
reviewReason: 'macro-expansion',
|
|
3278
|
+
editKind: 'macro-expansion',
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
2286
3283
|
const lineKey = `${incoming.file}:${incoming.line}`;
|
|
2287
3284
|
const site = emittedRenameCalls.get(lineKey) || {
|
|
2288
3285
|
file: incoming.file,
|
|
@@ -2297,7 +3294,10 @@ function plan(index, name, options = {}) {
|
|
|
2297
3294
|
};
|
|
2298
3295
|
if (site.calledAs !== incoming.calledAs) site.calledAs = null;
|
|
2299
3296
|
site.callCount++;
|
|
2300
|
-
|
|
3297
|
+
const column = Number.isInteger(incoming.column) ? incoming.column
|
|
3298
|
+
: callTokenColumn(index, incoming.absoluteFile || incoming.file, incoming.siteRange,
|
|
3299
|
+
name, incoming.line);
|
|
3300
|
+
if (Number.isInteger(column)) site.columns.push(column);
|
|
2301
3301
|
else site.missingColumn = true;
|
|
2302
3302
|
emittedRenameCalls.set(lineKey, site);
|
|
2303
3303
|
touched.add(lineKey);
|
|
@@ -2308,7 +3308,9 @@ function plan(index, name, options = {}) {
|
|
|
2308
3308
|
// `{ old: local }`). The caller engine carries the authored name
|
|
2309
3309
|
// in calledAs; that token must remain unchanged while the import's
|
|
2310
3310
|
// source-side identifier is edited below.
|
|
2311
|
-
|
|
3311
|
+
// `calledAs: 'bound'` labels a bind/call/apply site
|
|
3312
|
+
// (`obj.m.bind(obj)`), which spells the name (fix #397).
|
|
3313
|
+
if (site.calledAs && site.calledAs !== name && site.calledAs !== 'bound') continue;
|
|
2312
3314
|
let edit = renameIdentifierTokens(index,
|
|
2313
3315
|
site.absoluteFile || site.file, site.line, name,
|
|
2314
3316
|
options.renameTo, site.missingColumn ? [] : site.columns,
|
|
@@ -2334,7 +3336,7 @@ function plan(index, name, options = {}) {
|
|
|
2334
3336
|
// Missing parser columns mean UCN can prove the edit is
|
|
2335
3337
|
// required but cannot safely synthesize it. Never fall back
|
|
2336
3338
|
// to whole-line regex replacement.
|
|
2337
|
-
if (site.missingColumn) {
|
|
3339
|
+
if (site.missingColumn && site.calledAs !== 'bound') {
|
|
2338
3340
|
const manual = {
|
|
2339
3341
|
file: site.file,
|
|
2340
3342
|
line: site.line,
|
|
@@ -2353,17 +3355,25 @@ function plan(index, name, options = {}) {
|
|
|
2353
3355
|
}
|
|
2354
3356
|
continue;
|
|
2355
3357
|
}
|
|
2356
|
-
const concrete = {
|
|
3358
|
+
const concrete = tagTokenEdit({
|
|
2357
3359
|
file: site.file,
|
|
2358
3360
|
line: site.line,
|
|
2359
3361
|
expression: edit.source,
|
|
2360
3362
|
suggestion: `Rename to: ${newExpression}`,
|
|
2361
3363
|
newExpression,
|
|
2362
3364
|
editKind: 'call',
|
|
2363
|
-
};
|
|
2364
|
-
|
|
3365
|
+
}, edit);
|
|
3366
|
+
// Another pass already edits this line (a slot member's
|
|
3367
|
+
// one-line declaration): add the call tokens to that edit.
|
|
3368
|
+
const lineOwner = site.mergedInto || (!site.change && changes.find(change =>
|
|
3369
|
+
change.file === site.file && change.line === site.line &&
|
|
3370
|
+
change.newExpression !== undefined && change._edit));
|
|
3371
|
+
if (lineOwner && mergeTokenEdit(lineOwner, edit)) {
|
|
3372
|
+
site.mergedInto = lineOwner;
|
|
3373
|
+
} else if (site.change) {
|
|
2365
3374
|
for (const key of Object.keys(site.change)) delete site.change[key];
|
|
2366
3375
|
Object.assign(site.change, concrete);
|
|
3376
|
+
tagTokenEdit(site.change, edit);
|
|
2367
3377
|
} else {
|
|
2368
3378
|
site.change = concrete;
|
|
2369
3379
|
changes.push(concrete);
|
|
@@ -2394,22 +3404,95 @@ function plan(index, name, options = {}) {
|
|
|
2394
3404
|
includeTests: true,
|
|
2395
3405
|
internalEvidence: true,
|
|
2396
3406
|
});
|
|
3407
|
+
planUsages = usages;
|
|
2397
3408
|
const importUsages = usages.filter(u => u.usageType === 'import' && !u.isDefinition);
|
|
3409
|
+
// Import identity (fix #359, starlette-measured): an import binds a
|
|
3410
|
+
// module-level name. A MEMBER pin (method/field of a class, struct,
|
|
3411
|
+
// trait or impl) is never what `import json` binds, so renaming
|
|
3412
|
+
// Request.json must not rewrite the stdlib `import json` in its own
|
|
3413
|
+
// file. Languages whose bare names can denote members through a
|
|
3414
|
+
// member import (Java `import static a.D.head`) keep the edit only
|
|
3415
|
+
// when the binding provably reaches the pinned file.
|
|
3416
|
+
const pinIsMember = !!def.className && !!(def.isMethod || def.memberType);
|
|
3417
|
+
const memberImportable = pinIsMember &&
|
|
3418
|
+
!!langTraits(planLang)?.bareCallReachesMethods;
|
|
3419
|
+
const importMayBindPin = (file) => {
|
|
3420
|
+
// A source-side import token need not be exposed by its importing
|
|
3421
|
+
// file: `from core import transform as xf` binds only `xf` there.
|
|
3422
|
+
// The statement's source module decides ownership below.
|
|
3423
|
+
if (!pinIsMember) return true;
|
|
3424
|
+
return memberImportable &&
|
|
3425
|
+
_nameBindingReaches(index, file, name, renameTargetFiles) === 'yes';
|
|
3426
|
+
};
|
|
3427
|
+
// Import SOURCE identity (fix #376): an import statement's source-side
|
|
3428
|
+
// name is edited only when the module it imports from binds the
|
|
3429
|
+
// renamed definition. `import { flag as baseFlag } from './utils'`
|
|
3430
|
+
// inside the pin's own file imports ANOTHER module's flag; a module
|
|
3431
|
+
// never imports its own definition, so in the pin's file an import of
|
|
3432
|
+
// the name from any other (or unresolvable) module is never this
|
|
3433
|
+
// rename's import.
|
|
3434
|
+
const importSourceBindsPin = (filePath, line, column = null) => {
|
|
3435
|
+
const fileEntry = index.files.get(filePath);
|
|
3436
|
+
// Bindings carry their statement's first line: a token on a later
|
|
3437
|
+
// line of a multi-line import (`import {\n a,\n} from './m'`,
|
|
3438
|
+
// `from m import (\n a,\n)`) belongs to that statement (fix #400).
|
|
3439
|
+
const onLine = (fileEntry?.importBindings || []).some(binding => binding.line === line &&
|
|
3440
|
+
(binding.name === name || binding.alias === name));
|
|
3441
|
+
const statementLine = onLine ? line : importStatementLine(index, filePath, line, column);
|
|
3442
|
+
const bindings = (fileEntry?.importBindings || []).filter(binding =>
|
|
3443
|
+
(binding.line === line || binding.line === statementLine) &&
|
|
3444
|
+
binding.name === name && binding.module);
|
|
3445
|
+
// The name is only the LOCAL alias of another imported name on
|
|
3446
|
+
// this line (`import { g as f }`, `const f = require('m').g`,
|
|
3447
|
+
// fix #397): an import alias keeps its local name, and the
|
|
3448
|
+
// source-side name is not the renamed definition's.
|
|
3449
|
+
if (bindings.length === 0 && (fileEntry?.importBindings || []).some(binding =>
|
|
3450
|
+
(binding.line === line || binding.line === statementLine) &&
|
|
3451
|
+
binding.alias === name && binding.name !== name)) return false;
|
|
3452
|
+
if (bindings.length === 0) return true;
|
|
3453
|
+
return bindings.some(binding => {
|
|
3454
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
3455
|
+
const abs = rel ? (require('path').isAbsolute(rel) ? rel
|
|
3456
|
+
: require('path').join(index.root, rel)) : null;
|
|
3457
|
+
// An import from a module outside the project (`const parse =
|
|
3458
|
+
// require('parseurl')`, fix #397) binds that module's value,
|
|
3459
|
+
// never the renamed definition; a resolver gap keeps the edit.
|
|
3460
|
+
if (!abs && !require('./callers')._unresolvedModuleIsGap(index, binding.module, binding)) return false;
|
|
3461
|
+
if (!abs) return filePath !== def.file;
|
|
3462
|
+
if (abs === filePath) return false;
|
|
3463
|
+
// A module binding the name through one alternative of a
|
|
3464
|
+
// configuration item (fix #398F) keeps its item name: the
|
|
3465
|
+
// importer's token names the item, not the renamed pin.
|
|
3466
|
+
const verdict = _nameBindingReaches(index, abs, name, renameTargetFiles, 4,
|
|
3467
|
+
{ configurationVerdict: true });
|
|
3468
|
+
return verdict !== 'no' && verdict !== 'configuration';
|
|
3469
|
+
});
|
|
3470
|
+
};
|
|
3471
|
+
// An unaliased import that is one alternative of a configuration
|
|
3472
|
+
// item in its own scope (fix #398F: `try: from ._speedups import f` /
|
|
3473
|
+
// `except ImportError: def f`) keeps the item's local name.
|
|
3474
|
+
const importKeepsItemName = (filePath, line) => {
|
|
3475
|
+
const { itemsNamed } = require('./configuration-items');
|
|
3476
|
+
return itemsNamed(index, filePath, name).some(item =>
|
|
3477
|
+
item.sites.some(site => site.kind === 'import' && site.line === line));
|
|
3478
|
+
};
|
|
2398
3479
|
for (const imp of importUsages) {
|
|
2399
3480
|
// Skip if already covered by a call site change in the same file:line
|
|
2400
3481
|
const alreadyCovered = changes.some(c =>
|
|
2401
3482
|
c.file === (imp.relativePath || imp.file) && c.line === imp.line
|
|
2402
3483
|
);
|
|
2403
3484
|
if (alreadyCovered) continue;
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
3485
|
+
// The token is the importer's own local alias (fix #397): an
|
|
3486
|
+
// import alias keeps its local name.
|
|
3487
|
+
if (imp.importAlias) continue;
|
|
3488
|
+
if (imp.file && def.file && !importMayBindPin(imp.file)) continue;
|
|
3489
|
+
if (imp.file && !importSourceBindsPin(imp.file, imp.line, imp.column)) continue;
|
|
2408
3490
|
const edit = renameIdentifierTokens(index, imp.file,
|
|
2409
|
-
imp.line, name,
|
|
3491
|
+
imp.line, name, imp.file && importKeepsItemName(imp.file, imp.line)
|
|
3492
|
+
? `${options.renameTo} as ${name}` : options.renameTo);
|
|
2410
3493
|
const newImport = edit.renamed;
|
|
2411
3494
|
if (newImport === edit.source) continue;
|
|
2412
|
-
changes.push({
|
|
3495
|
+
changes.push(tagTokenEdit({
|
|
2413
3496
|
file: imp.relativePath || imp.file,
|
|
2414
3497
|
line: imp.line,
|
|
2415
3498
|
expression: edit.source,
|
|
@@ -2417,7 +3500,7 @@ function plan(index, name, options = {}) {
|
|
|
2417
3500
|
newExpression: newImport,
|
|
2418
3501
|
isImport: true,
|
|
2419
3502
|
editKind: 'import',
|
|
2420
|
-
});
|
|
3503
|
+
}, edit));
|
|
2421
3504
|
}
|
|
2422
3505
|
|
|
2423
3506
|
// Renamed CJS/Python imports are intentionally surfaced as reference
|
|
@@ -2429,8 +3512,8 @@ function plan(index, name, options = {}) {
|
|
|
2429
3512
|
.find(alias => alias.original === binding.name)?.local;
|
|
2430
3513
|
if (binding.name !== name || !localAlias ||
|
|
2431
3514
|
localAlias === name || !binding.line) continue;
|
|
2432
|
-
if (
|
|
2433
|
-
|
|
3515
|
+
if (!importMayBindPin(filePath)) continue;
|
|
3516
|
+
if (!importSourceBindsPin(filePath, binding.line)) continue;
|
|
2434
3517
|
const rel = fileEntry.relativePath || filePath;
|
|
2435
3518
|
if (changes.some(change =>
|
|
2436
3519
|
change.file === rel && change.line === binding.line)) continue;
|
|
@@ -2485,18 +3568,27 @@ function plan(index, name, options = {}) {
|
|
|
2485
3568
|
const pinnedAtExportLine = defsAtExportLine.some(d =>
|
|
2486
3569
|
d.file === def.file && d.startLine === def.startLine);
|
|
2487
3570
|
if (defsAtExportLine.length > 0 && !pinnedAtExportLine) continue;
|
|
3571
|
+
// A member pin's export surface is its own declaration line
|
|
3572
|
+
// only; a same-named module-level export belongs to another
|
|
3573
|
+
// binding (fix #359).
|
|
3574
|
+
if (pinIsMember && !pinnedAtExportLine) continue;
|
|
2488
3575
|
const exportFile = targetEntry.relativePath || exportPath;
|
|
2489
3576
|
if (changes.some(change =>
|
|
2490
3577
|
change.file === exportFile && change.line === exported.line)) {
|
|
2491
3578
|
continue;
|
|
2492
3579
|
}
|
|
3580
|
+
// The pinned definition's own line is an export surface of
|
|
3581
|
+
// THAT declaration (`export function head(head: string)`):
|
|
3582
|
+
// rename only the declaration's name token (fix #359).
|
|
2493
3583
|
const edit = renameIdentifierTokens(index, exportPath,
|
|
2494
|
-
exported.line, name, options.renameTo
|
|
3584
|
+
exported.line, name, options.renameTo, null, null,
|
|
3585
|
+
pinnedAtExportLine
|
|
3586
|
+
? { definitionNameOnly: true, definition: def } : {});
|
|
2495
3587
|
const sourceLine = edit.source;
|
|
2496
3588
|
const occurrences = edit.count;
|
|
2497
3589
|
const newExpression = edit.renamed;
|
|
2498
3590
|
if (newExpression === sourceLine) continue;
|
|
2499
|
-
changes.push({
|
|
3591
|
+
changes.push(tagTokenEdit({
|
|
2500
3592
|
file: exportFile,
|
|
2501
3593
|
line: exported.line,
|
|
2502
3594
|
expression: sourceLine,
|
|
@@ -2505,7 +3597,7 @@ function plan(index, name, options = {}) {
|
|
|
2505
3597
|
isExport: true,
|
|
2506
3598
|
editKind: 'export',
|
|
2507
3599
|
...(occurrences > 1 && { needsReview: true }),
|
|
2508
|
-
});
|
|
3600
|
+
}, edit));
|
|
2509
3601
|
}
|
|
2510
3602
|
}
|
|
2511
3603
|
|
|
@@ -2594,82 +3686,290 @@ function plan(index, name, options = {}) {
|
|
|
2594
3686
|
if (member === def) continue;
|
|
2595
3687
|
const line = member.nameLine || member.startLine;
|
|
2596
3688
|
const rel = member.relativePath || member.file;
|
|
2597
|
-
if (
|
|
2598
|
-
change.file === rel && change.line === line)) continue;
|
|
3689
|
+
if (lineTaken(member, rel, line)) continue;
|
|
2599
3690
|
const edit = renameIdentifierTokens(index, member.file,
|
|
2600
3691
|
line, name, options.renameTo,
|
|
2601
|
-
null, null, { definitionNameOnly: true });
|
|
3692
|
+
null, null, { definitionNameOnly: true, definition: member });
|
|
2602
3693
|
if (edit.renamed === edit.source) continue;
|
|
2603
|
-
changes.push({
|
|
3694
|
+
changes.push(tagTokenEdit({
|
|
2604
3695
|
file: rel,
|
|
2605
3696
|
line,
|
|
2606
3697
|
expression: edit.source,
|
|
2607
|
-
suggestion:
|
|
3698
|
+
suggestion: langTraits(index.files.get(member.file)?.language)?.textualIncludes
|
|
3699
|
+
? `Update declaration: ${edit.renamed}`
|
|
3700
|
+
: isAccessorDefinition(member) && isAccessorDefinition(def)
|
|
3701
|
+
? `Update accessor: ${edit.renamed}`
|
|
3702
|
+
: `Update overload signature: ${edit.renamed}`,
|
|
2608
3703
|
newExpression: edit.renamed,
|
|
2609
3704
|
isDefinition: true,
|
|
2610
3705
|
editKind: 'definition',
|
|
2611
|
-
});
|
|
3706
|
+
}, edit));
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// Configuration alternatives (fix #376): in a language where a
|
|
3711
|
+
// definition cannot be repeated in one scope except under
|
|
3712
|
+
// conditional compilation (traits.conditionalDefinitions: Rust
|
|
3713
|
+
// `#[cfg]`, C/C++/C# `#if`), a same-file definition of the same name,
|
|
3714
|
+
// owner and signature is the SAME item in another build
|
|
3715
|
+
// configuration. Only one is compiled at a time, so the caller engine
|
|
3716
|
+
// binds calls to one of them; the rename must change every
|
|
3717
|
+
// alternative and every alternative's call sites.
|
|
3718
|
+
const conditionalStyle = langTraits(planLang)?.conditionalDefinitions;
|
|
3719
|
+
// Other external definitions of a C/C++ free function in other files
|
|
3720
|
+
// (fix #396: platform variants `net_open` in net.c and net_win.c):
|
|
3721
|
+
// each defines the function its shared declaration declares, so the
|
|
3722
|
+
// rename changes them all; one no shared declaration ties to the pin
|
|
3723
|
+
// (a separate program's own definition) is a review item.
|
|
3724
|
+
const linkAlternatives = cLinkAlternatives(index, def, definitions);
|
|
3725
|
+
for (const other of linkAlternatives.unrelated) {
|
|
3726
|
+
reviewItems.push({
|
|
3727
|
+
file: other.relativePath || path.relative(index.root, other.file),
|
|
3728
|
+
line: other.nameLine || other.startLine,
|
|
3729
|
+
expression: (index.getLineContent(other.file, other.nameLine || other.startLine) || '').trim(),
|
|
3730
|
+
suggestion: `Another external definition of "${name}" that no declaration of the renamed ` +
|
|
3731
|
+
'function reaches (a separate program or platform build); rename it too if it is the same function',
|
|
3732
|
+
needsReview: true,
|
|
3733
|
+
editKind: 'definition',
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
if (conditionalStyle && !def.isSignature) {
|
|
3737
|
+
const { _cFamilySignatureKey, _isCrossFileConfigurationMember, _sameOwnerPath,
|
|
3738
|
+
_isConfigurationAlternative } = require('./callers');
|
|
3739
|
+
const hasCfg = d => (d.attributesWithArgs || []).some(a => a.name === 'cfg') ||
|
|
3740
|
+
(d.modifiers || []).includes('cfg');
|
|
3741
|
+
const alternatives = definitions.filter(other => other !== def && ((
|
|
3742
|
+
// Runtime configuration items (fix #398F, Python): the
|
|
3743
|
+
// parser-recorded alternatives of the item, whatever their
|
|
3744
|
+
// parameters.
|
|
3745
|
+
conditionalStyle === 'branch' ? _isConfigurationAlternative(index, def, other) :
|
|
3746
|
+
other.file === def.file && !other.isSignature &&
|
|
3747
|
+
!NON_CALLABLE_TYPES.has(other.type) &&
|
|
3748
|
+
(other.className || null) === (def.className || null) &&
|
|
3749
|
+
(other.traitName || null) === (def.traitName || null) &&
|
|
3750
|
+
(other.namespace || null) === (def.namespace || null) &&
|
|
3751
|
+
(other.explicitInterface || null) === (def.explicitInterface || null) &&
|
|
3752
|
+
!(other.startLine === def.startLine && other.endLine === def.endLine) &&
|
|
3753
|
+
// Members of two same-name classes declared in different
|
|
3754
|
+
// scopes (a function-local class) are two items (fix #389).
|
|
3755
|
+
!distinctOwnerDefinitions(index, other, def) &&
|
|
3756
|
+
// One full owner path (fix #395): same-name nested types of
|
|
3757
|
+
// two outer classes are two items.
|
|
3758
|
+
_sameOwnerPath(index, other, def) &&
|
|
3759
|
+
_cFamilySignatureKey(other) === _cFamilySignatureKey(def) &&
|
|
3760
|
+
(conditionalStyle !== 'attribute' || hasCfg(def) || hasCfg(other))) ||
|
|
3761
|
+
// The same class defined again in a file no translation unit
|
|
3762
|
+
// includes beside this one (fix #385: `#ifdef _WIN32
|
|
3763
|
+
// #include "client-windows.h" #else #include "client.h"`).
|
|
3764
|
+
(!other.isSignature && _isCrossFileConfigurationMember(index, def, other)) ||
|
|
3765
|
+
linkAlternatives.defines.includes(other)));
|
|
3766
|
+
const alternativeSites = [];
|
|
3767
|
+
for (const other of alternatives) {
|
|
3768
|
+
const line = other.nameLine || other.startLine;
|
|
3769
|
+
const rel = other.relativePath || other.file;
|
|
3770
|
+
if (!lineTaken(other, rel, line)) {
|
|
3771
|
+
const edit = renameIdentifierTokens(index, other.file, line, name,
|
|
3772
|
+
options.renameTo, null, null, { definitionNameOnly: true, definition: other });
|
|
3773
|
+
if (edit.renamed !== edit.source) {
|
|
3774
|
+
changes.push(tagTokenEdit({
|
|
3775
|
+
file: rel,
|
|
3776
|
+
line,
|
|
3777
|
+
expression: edit.source,
|
|
3778
|
+
suggestion: linkAlternatives.defines.includes(other)
|
|
3779
|
+
? `Update another definition of the declared function: ${edit.renamed}`
|
|
3780
|
+
: `Update configuration alternative: ${edit.renamed}`,
|
|
3781
|
+
newExpression: edit.renamed,
|
|
3782
|
+
isDefinition: true,
|
|
3783
|
+
editKind: 'definition',
|
|
3784
|
+
}, edit));
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
const sweep = computePlanCallSites(index, name, other, { analyzeArgs: !options.renameTo });
|
|
3788
|
+
const seen = new Set([...planCallSites, ...alternativeSites].map(site =>
|
|
3789
|
+
`${site.file}:${site.line}:${site.column ?? '*'}`));
|
|
3790
|
+
const lines = new Set([...planCallSites, ...alternativeSites].map(site =>
|
|
3791
|
+
`${site.file}:${site.line}`));
|
|
3792
|
+
for (const site of sweep.sites) {
|
|
3793
|
+
const key = `${site.file}:${site.line}:${site.column ?? '*'}`;
|
|
3794
|
+
if (seen.has(key)) continue;
|
|
3795
|
+
// One call reached through two alternatives' sweeps, once
|
|
3796
|
+
// without its column (fix #385: a macro alternative's
|
|
3797
|
+
// invocation beside the function's own call record).
|
|
3798
|
+
if (site.column == null && lines.has(`${site.file}:${site.line}`)) continue;
|
|
3799
|
+
const lineless = alternativeSites.findIndex(existing => existing.column == null &&
|
|
3800
|
+
existing.file === site.file && existing.line === site.line);
|
|
3801
|
+
if (lineless >= 0) alternativeSites.splice(lineless, 1);
|
|
3802
|
+
seen.add(key);
|
|
3803
|
+
lines.add(`${site.file}:${site.line}`);
|
|
3804
|
+
alternativeSites.push(site);
|
|
3805
|
+
}
|
|
3806
|
+
for (const site of sweep.unverifiedSites) {
|
|
3807
|
+
if (planUnverified.some(existing => existing.file === site.file &&
|
|
3808
|
+
existing.line === site.line)) continue;
|
|
3809
|
+
// A call another alternative's sweep already edits is that
|
|
3810
|
+
// edit, not a second (column-less) site of review.
|
|
3811
|
+
if (lines.has(`${site.file}:${site.line}`)) continue;
|
|
3812
|
+
planUnverified.push(site);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
// A call the engine left ambiguous only between configuration
|
|
3816
|
+
// alternatives binds one of them whichever configuration builds:
|
|
3817
|
+
// when the rename covers every project body of the name, it is
|
|
3818
|
+
// an edit.
|
|
3819
|
+
if (alternatives.length > 0) {
|
|
3820
|
+
const renamed = new Set([def, ...alternatives]);
|
|
3821
|
+
const coversAll = definitions.every(d => renamed.has(d) || d.isSignature ||
|
|
3822
|
+
NON_CALLABLE_TYPES.has(d.type) ||
|
|
3823
|
+
(d.className || null) !== (def.className || null));
|
|
3824
|
+
if (coversAll) {
|
|
3825
|
+
const promoted = planUnverified.filter(site =>
|
|
3826
|
+
site.reason === 'link-ambiguous' || site.reason === 'macro-definition-ambiguous' ||
|
|
3827
|
+
site.reason === 'overload-ambiguous' || site.reason === 'configuration-alternative');
|
|
3828
|
+
const editedLines = new Set([...planCallSites, ...alternativeSites]
|
|
3829
|
+
.map(site => `${site.file}:${site.line}`));
|
|
3830
|
+
for (const site of promoted) {
|
|
3831
|
+
// Already an edit through an alternative's own sweep.
|
|
3832
|
+
if (editedLines.has(`${site.file}:${site.line}`)) continue;
|
|
3833
|
+
alternativeSites.push({
|
|
3834
|
+
file: site.file,
|
|
3835
|
+
absoluteFile: require('path').join(index.root, site.file),
|
|
3836
|
+
line: site.line,
|
|
3837
|
+
expression: site.expression,
|
|
3838
|
+
});
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
if (alternativeSites.length > 0) {
|
|
3843
|
+
emitRenameCallSites(alternativeSites);
|
|
3844
|
+
slotAccountSites.push(...alternativeSites);
|
|
3845
|
+
const changedLines = new Set(changes.map(change => `${change.file}:${change.line}`));
|
|
3846
|
+
const kept = planUnverified.filter(site => !changedLines.has(`${site.file}:${site.line}`));
|
|
3847
|
+
planUnverified.length = 0;
|
|
3848
|
+
planUnverified.push(...kept);
|
|
3849
|
+
planUnverified.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// Run-time alternative bindings of the same scope variable (fix
|
|
3854
|
+
// #376): a language whose adapter reports them (Python) binds a name
|
|
3855
|
+
// per executed statement, so `if ...: f = lib.f` / `else: def f()`,
|
|
3856
|
+
// try/except import fallbacks and a class body's `close = release`
|
|
3857
|
+
// are one variable with the pin. Definitions and assignment targets
|
|
3858
|
+
// are renamed with it, an unaliased import alternative gains an
|
|
3859
|
+
// alias, and class-body reads of a member are references.
|
|
3860
|
+
{
|
|
3861
|
+
let adapter = null;
|
|
3862
|
+
try { adapter = getLanguageAdapter(planLang); } catch { adapter = null; }
|
|
3863
|
+
if (typeof adapter?.findScopeAlternativeBindings === 'function') {
|
|
3864
|
+
let tree = null;
|
|
3865
|
+
try {
|
|
3866
|
+
const content = index._readFile(def.file);
|
|
3867
|
+
const parser = getParser(planLang);
|
|
3868
|
+
tree = parser && (index._getParsedTree?.(def.file, content, planLang) ||
|
|
3869
|
+
safeParse(parser, content));
|
|
3870
|
+
} catch { tree = null; }
|
|
3871
|
+
const bindings = tree
|
|
3872
|
+
? adapter.findScopeAlternativeBindings(tree, def.nameLine || def.startLine, name)
|
|
3873
|
+
: [];
|
|
3874
|
+
const rel = def.relativePath || def.file;
|
|
3875
|
+
for (const binding of bindings) {
|
|
3876
|
+
if (changes.some(change => change.file === rel && change.line === binding.line)) continue;
|
|
3877
|
+
const replacement = binding.kind === 'import-name'
|
|
3878
|
+
? `${name} as ${options.renameTo}` : options.renameTo;
|
|
3879
|
+
const edit = renameIdentifierTokens(index, def.file, binding.line, name,
|
|
3880
|
+
replacement, [binding.column]);
|
|
3881
|
+
if (edit.renamed === edit.source) continue;
|
|
3882
|
+
const isReference = binding.kind === 'reference';
|
|
3883
|
+
changes.push(tagTokenEdit({
|
|
3884
|
+
file: rel,
|
|
3885
|
+
line: binding.line,
|
|
3886
|
+
expression: edit.source,
|
|
3887
|
+
suggestion: isReference
|
|
3888
|
+
? `Update class-scope reference: ${edit.renamed}`
|
|
3889
|
+
: `Update alternative binding: ${edit.renamed}`,
|
|
3890
|
+
newExpression: edit.renamed,
|
|
3891
|
+
...(!isReference && { isDefinition: true }),
|
|
3892
|
+
editKind: isReference ? 'reference' : 'definition',
|
|
3893
|
+
}, edit));
|
|
3894
|
+
}
|
|
2612
3895
|
}
|
|
2613
3896
|
}
|
|
2614
3897
|
|
|
2615
3898
|
// Renaming a virtual/overridden member is one hierarchy-wide change.
|
|
2616
3899
|
// Leaving descendant declarations behind either fails compilation
|
|
2617
3900
|
// (Java/C#/TS override) or silently changes dispatch (Python/JS).
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
//
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
//
|
|
2626
|
-
//
|
|
2627
|
-
//
|
|
2628
|
-
//
|
|
2629
|
-
//
|
|
2630
|
-
//
|
|
2631
|
-
//
|
|
2632
|
-
//
|
|
2633
|
-
//
|
|
2634
|
-
let
|
|
3901
|
+
// A typed object-literal member (`const h: Handler = { handle() {} }`)
|
|
3902
|
+
// fills the slot of its declared interface the same way (fix #360).
|
|
3903
|
+
const literalContract = typedLiteralContract(index, def);
|
|
3904
|
+
if (def.className || literalContract?.projectInterface) {
|
|
3905
|
+
// Slot closure by class DEFINITION identity (fix #376): the
|
|
3906
|
+
// renamed member's slot spans every transitive ancestor that
|
|
3907
|
+
// declares it - including through intermediate classes that do
|
|
3908
|
+
// not redeclare it (jackson WriterBasedJsonGenerator.writeName
|
|
3909
|
+
// -> JsonGeneratorBase -> GeneratorBase -> JsonGenerator) - and
|
|
3910
|
+
// every transitive descendant of those declaring classes that
|
|
3911
|
+
// overrides it. Parents resolve from the declaring file
|
|
3912
|
+
// (qualified and nested spellings included), children only when
|
|
3913
|
+
// their own base resolves back to the same definition; languages
|
|
3914
|
+
// with overloads by parameter list keep only the same-signature
|
|
3915
|
+
// slot. Declared `implements` clauses are slot edges for members
|
|
3916
|
+
// declared in the class body.
|
|
3917
|
+
let slotStartRef = null;
|
|
3918
|
+
if (!def.className && literalContract?.projectInterface) {
|
|
3919
|
+
const iface = literalContract.projectInterface;
|
|
3920
|
+
const entry = classDefsNamed(index, iface.name).entries
|
|
3921
|
+
.find(e => e.def.file === iface.file);
|
|
3922
|
+
if (entry) slotStartRef = { name: iface.name, key: entry.key, def: entry.def };
|
|
3923
|
+
}
|
|
3924
|
+
const hierarchy = (def.className || slotStartRef)
|
|
3925
|
+
? hierarchySlotClosure(index, def, slotStartRef) : null;
|
|
2635
3926
|
const slotAncestors = [];
|
|
2636
|
-
const
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
for (const parentName of parents) {
|
|
2642
|
-
const parentFile = index._resolveClassFile(parentName, slotRoot.file);
|
|
2643
|
-
if (!parentFile) continue;
|
|
2644
|
-
const parentDef = (index.symbols.get(name) || []).find(symbol =>
|
|
2645
|
-
symbol.className === parentName && symbol.file === parentFile &&
|
|
2646
|
-
!NON_CALLABLE_TYPES.has(symbol.type));
|
|
2647
|
-
if (!parentDef) continue;
|
|
2648
|
-
if (langTraits(planLang)?.hasArityOverloads &&
|
|
2649
|
-
(parentDef.paramsStructured || []).length !==
|
|
2650
|
-
(def.paramsStructured || []).length) continue;
|
|
2651
|
-
const key = identityKey(parentName, parentFile);
|
|
2652
|
-
if (climbed.has(key)) break;
|
|
2653
|
-
climbed.add(key);
|
|
2654
|
-
slotRoot = { name: parentName, file: parentFile };
|
|
2655
|
-
slotAncestors.push({ className: parentName, file: parentFile, def: parentDef });
|
|
2656
|
-
moved = true;
|
|
2657
|
-
break;
|
|
2658
|
-
}
|
|
2659
|
-
if (!moved) break;
|
|
3927
|
+
for (const ancestor of hierarchy?.ancestors || []) {
|
|
3928
|
+
for (const member of ancestor.members) {
|
|
3929
|
+
if (member.file === def.file && member.startLine === def.startLine) continue;
|
|
3930
|
+
slotAncestors.push({ className: ancestor.ref.name, file: member.file, def: member });
|
|
3931
|
+
}
|
|
2660
3932
|
}
|
|
3933
|
+
hierarchySlot = hierarchy;
|
|
2661
3934
|
const slotMemberDefs = [];
|
|
3935
|
+
// fix #379: a C/C++ slot member declared in its class and
|
|
3936
|
+
// defined out of line (`void Derived::f() {}` in another file)
|
|
3937
|
+
// is one callable; renaming the declaration alone leaves a
|
|
3938
|
+
// definition that matches nothing.
|
|
3939
|
+
const { _closeCallableIdentityGroup: closeIdentity } = require('./callers');
|
|
3940
|
+
const renameSlotPartners = (member, suggestion) => {
|
|
3941
|
+
for (const partner of closeIdentity(index, [member], definitions)) {
|
|
3942
|
+
if (partner === member ||
|
|
3943
|
+
(partner.file === def.file && partner.startLine === def.startLine)) continue;
|
|
3944
|
+
const line = partner.nameLine || partner.startLine;
|
|
3945
|
+
const rel = partner.relativePath || partner.file;
|
|
3946
|
+
if (lineTaken(partner, rel, line)) continue;
|
|
3947
|
+
const edit = renameIdentifierTokens(index, partner.file,
|
|
3948
|
+
line, name, options.renameTo,
|
|
3949
|
+
null, null, { definitionNameOnly: true, definition: partner });
|
|
3950
|
+
if (edit.renamed === edit.source) continue;
|
|
3951
|
+
changes.push(tagTokenEdit({
|
|
3952
|
+
file: rel,
|
|
3953
|
+
line,
|
|
3954
|
+
expression: edit.source,
|
|
3955
|
+
suggestion: `${suggestion}: ${edit.renamed}`,
|
|
3956
|
+
newExpression: edit.renamed,
|
|
3957
|
+
isDefinition: true,
|
|
3958
|
+
editKind: 'definition',
|
|
3959
|
+
}, edit));
|
|
3960
|
+
slotMemberDefs.push(partner);
|
|
3961
|
+
}
|
|
3962
|
+
};
|
|
2662
3963
|
for (const ancestor of slotAncestors) {
|
|
2663
3964
|
slotMemberDefs.push(ancestor.def);
|
|
2664
3965
|
const line = ancestor.def.nameLine || ancestor.def.startLine;
|
|
2665
3966
|
const rel = ancestor.def.relativePath || ancestor.def.file;
|
|
2666
|
-
if (
|
|
2667
|
-
change.file === rel && change.line === line)) continue;
|
|
3967
|
+
if (lineTaken(ancestor.def, rel, line)) continue;
|
|
2668
3968
|
const edit = renameIdentifierTokens(index, ancestor.def.file,
|
|
2669
3969
|
line, name, options.renameTo,
|
|
2670
|
-
null, null, { definitionNameOnly: true });
|
|
3970
|
+
null, null, { definitionNameOnly: true, definition: ancestor.def });
|
|
2671
3971
|
if (edit.renamed === edit.source) continue;
|
|
2672
|
-
changes.push({
|
|
3972
|
+
changes.push(tagTokenEdit({
|
|
2673
3973
|
file: rel,
|
|
2674
3974
|
line,
|
|
2675
3975
|
expression: edit.source,
|
|
@@ -2677,59 +3977,112 @@ function plan(index, name, options = {}) {
|
|
|
2677
3977
|
newExpression: edit.renamed,
|
|
2678
3978
|
isDefinition: true,
|
|
2679
3979
|
editKind: 'definition',
|
|
2680
|
-
});
|
|
3980
|
+
}, edit));
|
|
2681
3981
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
const
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3982
|
+
for (const ancestor of slotAncestors) renameSlotPartners(ancestor.def, 'Update base definition');
|
|
3983
|
+
|
|
3984
|
+
const pinOwnerKey = hierarchy?.pinRef?.key || null;
|
|
3985
|
+
for (const descendant of hierarchy?.descendants || []) {
|
|
3986
|
+
for (const override of descendant.members) {
|
|
3987
|
+
// The pin handles itself; same-class siblings (Java
|
|
3988
|
+
// arity overloads, TS/Python signature stubs) belong to
|
|
3989
|
+
// the identity-group pass, never the hierarchy walk.
|
|
3990
|
+
if (pinOwnerKey && descendant.ref.key === pinOwnerKey) continue;
|
|
3991
|
+
if (override.file === def.file && override.startLine === def.startLine) continue;
|
|
3992
|
+
const line = override.nameLine || override.startLine;
|
|
3993
|
+
const rel = override.relativePath || override.file;
|
|
3994
|
+
if (lineTaken(override, rel, line)) continue;
|
|
3995
|
+
const edit = renameIdentifierTokens(index, override.file,
|
|
3996
|
+
line, name, options.renameTo,
|
|
3997
|
+
null, null, { definitionNameOnly: true, definition: override });
|
|
3998
|
+
const sourceLine = edit.source;
|
|
3999
|
+
const newExpression = edit.renamed;
|
|
4000
|
+
if (newExpression === sourceLine) continue;
|
|
4001
|
+
changes.push(tagTokenEdit({
|
|
4002
|
+
file: rel,
|
|
4003
|
+
line,
|
|
4004
|
+
expression: sourceLine,
|
|
4005
|
+
suggestion: `Update overriding definition: ${newExpression}`,
|
|
4006
|
+
newExpression,
|
|
4007
|
+
isDefinition: true,
|
|
4008
|
+
editKind: 'definition',
|
|
4009
|
+
}, edit));
|
|
4010
|
+
slotMemberDefs.push(override);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
for (const descendant of hierarchy?.descendants || []) {
|
|
4014
|
+
if (pinOwnerKey && descendant.ref.key === pinOwnerKey) continue;
|
|
4015
|
+
for (const override of descendant.members) {
|
|
4016
|
+
renameSlotPartners(override, 'Update overriding definition');
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
const slotOwners = [
|
|
4020
|
+
...(hierarchy ? [hierarchy.pinRef, ...hierarchy.ancestors.map(a => a.ref),
|
|
4021
|
+
...hierarchy.descendants.map(d => d.ref)] : []),
|
|
4022
|
+
].filter(ref => ref?.def?.file).map(ref => ({ name: ref.name, file: ref.def.file }));
|
|
4023
|
+
// Typed object literals declared against the root or any
|
|
4024
|
+
// descendant interface fill the same slot.
|
|
4025
|
+
for (const owner of slotOwners) {
|
|
4026
|
+
for (const member of typedLiteralMembers(index, name, owner.name, owner.file)) {
|
|
4027
|
+
if (member.file === def.file && member.startLine === def.startLine) continue;
|
|
4028
|
+
const line = member.nameLine || member.startLine;
|
|
4029
|
+
const rel = member.relativePath || member.file;
|
|
4030
|
+
if (lineTaken(member, rel, line)) continue;
|
|
4031
|
+
const edit = renameIdentifierTokens(index, member.file,
|
|
4032
|
+
line, name, options.renameTo,
|
|
4033
|
+
null, null, { definitionNameOnly: true, definition: member });
|
|
4034
|
+
if (edit.renamed === edit.source) continue;
|
|
4035
|
+
changes.push(tagTokenEdit({
|
|
4036
|
+
file: rel,
|
|
4037
|
+
line,
|
|
4038
|
+
expression: edit.source,
|
|
4039
|
+
suggestion: `Update typed object-literal member: ${edit.renamed}`,
|
|
4040
|
+
newExpression: edit.renamed,
|
|
4041
|
+
isDefinition: true,
|
|
4042
|
+
editKind: 'definition',
|
|
4043
|
+
}, edit));
|
|
4044
|
+
slotMemberDefs.push(member);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
// Anonymous subclasses (fix #390): a Java `new Listener() {
|
|
4048
|
+
// @Override void run() {} }` or a JS/TS class expression that
|
|
4049
|
+
// extends a slot owner overrides the member without being an
|
|
4050
|
+
// indexed class.
|
|
4051
|
+
for (const site of anonymousSlotMembers(index, name, planGroundSet?.perFile, hierarchy, def)) {
|
|
4052
|
+
const rel = require('path').relative(index.root, site.file);
|
|
4053
|
+
if (site.verdict === 'review') {
|
|
4054
|
+
if (!changes.some(change => change.file === rel && change.line === site.line)) {
|
|
4055
|
+
changes.push({
|
|
4056
|
+
file: rel,
|
|
4057
|
+
line: site.line,
|
|
4058
|
+
expression: (index._getFileLines(site.file)[site.line - 1] || '').trim(),
|
|
4059
|
+
suggestion: `Verify whether this anonymous class member overrides ${name} and rename it to ${options.renameTo} if it does`,
|
|
4060
|
+
needsReview: true,
|
|
4061
|
+
reviewReason: site.reason,
|
|
4062
|
+
editKind: 'definition',
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
const existing = changes.find(change => change.file === rel && change.line === site.line);
|
|
4068
|
+
const edit = renameIdentifierTokens(index, site.file, site.line, name, options.renameTo,
|
|
4069
|
+
[site.column]);
|
|
4070
|
+
if (edit.renamed === edit.source) continue;
|
|
4071
|
+
if (existing) {
|
|
4072
|
+
if (existing.newExpression !== undefined && existing._edit) mergeTokenEdit(existing, edit);
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
changes.push(tagTokenEdit({
|
|
2723
4076
|
file: rel,
|
|
2724
|
-
line,
|
|
2725
|
-
expression:
|
|
2726
|
-
suggestion: `Update
|
|
2727
|
-
newExpression,
|
|
4077
|
+
line: site.line,
|
|
4078
|
+
expression: edit.source,
|
|
4079
|
+
suggestion: `Update anonymous class override: ${edit.renamed}`,
|
|
4080
|
+
newExpression: edit.renamed,
|
|
2728
4081
|
isDefinition: true,
|
|
2729
4082
|
editKind: 'definition',
|
|
2730
|
-
});
|
|
2731
|
-
slotMemberDefs.push(override);
|
|
4083
|
+
}, edit));
|
|
2732
4084
|
}
|
|
4085
|
+
contractRootDefs.push(...slotAncestors.map(ancestor => ancestor.def));
|
|
2733
4086
|
|
|
2734
4087
|
// Go interface slots are implicit (fix #302, mux Match): there is
|
|
2735
4088
|
// no implements edge for the hierarchy walk above. Compute the
|
|
@@ -2739,20 +4092,20 @@ function plan(index, name, options = {}) {
|
|
|
2739
4092
|
// signatures) abstain in goInterfaceRenameClosure — absence is
|
|
2740
4093
|
// never treated as proof.
|
|
2741
4094
|
const goInterfaceSlot = planLang === 'go'
|
|
2742
|
-
?
|
|
4095
|
+
? goContractClosure(index, def)
|
|
2743
4096
|
: null;
|
|
4097
|
+
if (goInterfaceSlot) goContractReviews.push(...goInterfaceSlot.reviews);
|
|
2744
4098
|
if (goInterfaceSlot) {
|
|
2745
4099
|
for (const member of goInterfaceSlot.memberDefs) {
|
|
2746
4100
|
if (member.file === def.file && member.startLine === def.startLine) continue;
|
|
2747
4101
|
const line = member.nameLine || member.startLine;
|
|
2748
4102
|
const rel = member.relativePath || member.file;
|
|
2749
|
-
if (!
|
|
2750
|
-
change.file === rel && change.line === line)) {
|
|
4103
|
+
if (!lineTaken(member, rel, line)) {
|
|
2751
4104
|
const edit = renameIdentifierTokens(index, member.file,
|
|
2752
4105
|
line, name, options.renameTo,
|
|
2753
|
-
null, null, { definitionNameOnly: true });
|
|
4106
|
+
null, null, { definitionNameOnly: true, definition: member });
|
|
2754
4107
|
if (edit.renamed !== edit.source) {
|
|
2755
|
-
changes.push({
|
|
4108
|
+
changes.push(tagTokenEdit({
|
|
2756
4109
|
file: rel,
|
|
2757
4110
|
line,
|
|
2758
4111
|
expression: edit.source,
|
|
@@ -2760,7 +4113,7 @@ function plan(index, name, options = {}) {
|
|
|
2760
4113
|
newExpression: edit.renamed,
|
|
2761
4114
|
isDefinition: true,
|
|
2762
4115
|
editKind: 'definition',
|
|
2763
|
-
});
|
|
4116
|
+
}, edit));
|
|
2764
4117
|
}
|
|
2765
4118
|
}
|
|
2766
4119
|
slotMemberDefs.push(member);
|
|
@@ -2776,38 +4129,44 @@ function plan(index, name, options = {}) {
|
|
|
2776
4129
|
// it, with file-identity discipline (two crates may define
|
|
2777
4130
|
// same-named traits — a member joins only when its trait
|
|
2778
4131
|
// resolves to the pin's trait FILE).
|
|
2779
|
-
|
|
2780
|
-
|
|
4132
|
+
// Trait identity is the trait DEFINITION (fix #376): impl
|
|
4133
|
+
// headers spell generic traits with arguments (`impl<T> Ctx<T>
|
|
4134
|
+
// for Option<T>`) and paths (`crate::Ctx`), so a member joins
|
|
4135
|
+
// when its impl's trait resolves to the pin's trait definition.
|
|
4136
|
+
const traitKey = d => `${d.file}\0${d.startLine}`;
|
|
4137
|
+
let pinTraitDefs = [];
|
|
2781
4138
|
if (def.traitName) {
|
|
2782
|
-
|
|
2783
|
-
pinTraitFile = index._resolveClassFile(def.traitName, def.file) || null;
|
|
4139
|
+
pinTraitDefs = rustProjectTraits(index, def.traitName, def.file, def.startLine);
|
|
2784
4140
|
} else if ((index.symbols.get(def.className) || []).some(d =>
|
|
2785
4141
|
d.type === 'trait' && d.file === def.file)) {
|
|
2786
|
-
|
|
2787
|
-
|
|
4142
|
+
pinTraitDefs = (index.symbols.get(def.className) || []).filter(d =>
|
|
4143
|
+
d.type === 'trait' && d.file === def.file &&
|
|
4144
|
+
d.startLine <= def.startLine && (d.endLine || d.startLine) >= def.startLine);
|
|
2788
4145
|
}
|
|
2789
|
-
if (
|
|
2790
|
-
const
|
|
4146
|
+
if (pinTraitDefs.length === 1) {
|
|
4147
|
+
const pinTrait = pinTraitDefs[0];
|
|
2791
4148
|
for (const member of index.symbols.get(name) || []) {
|
|
2792
4149
|
if (member.file === def.file && member.startLine === def.startLine) continue;
|
|
2793
4150
|
if (NON_CALLABLE_TYPES.has(member.type)) continue;
|
|
2794
4151
|
let inSlot = false;
|
|
2795
|
-
if (member.traitName
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
4152
|
+
if (member.traitName) {
|
|
4153
|
+
const traits = rustProjectTraits(index, member.traitName, member.file,
|
|
4154
|
+
member.startLine);
|
|
4155
|
+
inSlot = traits.length === 1 && traitKey(traits[0]) === traitKey(pinTrait);
|
|
4156
|
+
} else if (member.className === pinTrait.name && member.file === pinTrait.file &&
|
|
4157
|
+
member.startLine >= pinTrait.startLine &&
|
|
4158
|
+
member.startLine <= (pinTrait.endLine || pinTrait.startLine)) {
|
|
2800
4159
|
inSlot = true; // the trait's own declaration member
|
|
2801
4160
|
}
|
|
2802
4161
|
if (!inSlot) continue;
|
|
2803
4162
|
const line = member.nameLine || member.startLine;
|
|
2804
4163
|
const rel = member.relativePath || member.file;
|
|
2805
|
-
if (!
|
|
4164
|
+
if (!lineTaken(member, rel, line)) {
|
|
2806
4165
|
const edit = renameIdentifierTokens(index, member.file,
|
|
2807
4166
|
line, name, options.renameTo,
|
|
2808
|
-
null, null, { definitionNameOnly: true });
|
|
4167
|
+
null, null, { definitionNameOnly: true, definition: member });
|
|
2809
4168
|
if (edit.renamed !== edit.source) {
|
|
2810
|
-
changes.push({
|
|
4169
|
+
changes.push(tagTokenEdit({
|
|
2811
4170
|
file: rel,
|
|
2812
4171
|
line,
|
|
2813
4172
|
expression: edit.source,
|
|
@@ -2815,80 +4174,11 @@ function plan(index, name, options = {}) {
|
|
|
2815
4174
|
newExpression: edit.renamed,
|
|
2816
4175
|
isDefinition: true,
|
|
2817
4176
|
editKind: 'definition',
|
|
2818
|
-
});
|
|
4177
|
+
}, edit));
|
|
2819
4178
|
}
|
|
2820
4179
|
}
|
|
2821
4180
|
slotMemberDefs.push(member);
|
|
2822
4181
|
}
|
|
2823
|
-
// Macro-generated impls are invisible to the index (the
|
|
2824
|
-
// as_cast_impl! family: the `fn` lives in a macro_rules body,
|
|
2825
|
-
// no impl_item node exists). A definition-shaped ground line
|
|
2826
|
-
// no indexed def claims, sitting inside a macro_rules body
|
|
2827
|
-
// whose token tree names `impl … <Trait>`, implements the
|
|
2828
|
-
// renamed slot — compiler-connected via the impl header, so
|
|
2829
|
-
// the edit is synthesized mechanically.
|
|
2830
|
-
// The sweep already computed the ground set for this exact
|
|
2831
|
-
// name (contractedCallerSweep) — reuse it instead of a second
|
|
2832
|
-
// full-repo text scan (read-only by contract).
|
|
2833
|
-
const macroGround = planGroundSet;
|
|
2834
|
-
const fnRe = new RegExp(`\\bfn\\s+${escapeRegExp(name)}\\b`);
|
|
2835
|
-
const implRe = new RegExp(
|
|
2836
|
-
`\\bimpl\\b[^;{}]{0,160}\\b${escapeRegExp(traitName)}\\b`);
|
|
2837
|
-
for (const [macroFile, lineNos] of macroGround.perFile) {
|
|
2838
|
-
const macroEntry = index.files.get(macroFile);
|
|
2839
|
-
if (!macroEntry || macroEntry.language !== 'rust') continue;
|
|
2840
|
-
const claimed = new Set((index.symbols.get(name) || [])
|
|
2841
|
-
.filter(d => d.file === macroFile)
|
|
2842
|
-
.map(d => d.nameLine || d.startLine));
|
|
2843
|
-
let macroTree = null;
|
|
2844
|
-
let macroNodes = null;
|
|
2845
|
-
for (const lineNo of lineNos) {
|
|
2846
|
-
if (claimed.has(lineNo)) continue;
|
|
2847
|
-
const rel = macroEntry.relativePath || macroFile;
|
|
2848
|
-
if (changes.some(change =>
|
|
2849
|
-
change.file === rel && change.line === lineNo)) continue;
|
|
2850
|
-
const content = index._readFile(macroFile);
|
|
2851
|
-
if (!fnRe.test(content.split('\n')[lineNo - 1] || '')) continue;
|
|
2852
|
-
if (macroTree === null) {
|
|
2853
|
-
const parser = getParser('rust');
|
|
2854
|
-
macroTree = (parser &&
|
|
2855
|
-
(index._getParsedTree?.(macroFile, content, 'rust') ||
|
|
2856
|
-
safeParse(parser, content))) || false;
|
|
2857
|
-
macroNodes = [];
|
|
2858
|
-
if (macroTree) {
|
|
2859
|
-
const stack = [macroTree.rootNode];
|
|
2860
|
-
while (stack.length > 0) {
|
|
2861
|
-
const node = stack.pop();
|
|
2862
|
-
if (node.type === 'macro_definition') {
|
|
2863
|
-
macroNodes.push(node);
|
|
2864
|
-
continue;
|
|
2865
|
-
}
|
|
2866
|
-
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2867
|
-
stack.push(node.namedChild(i));
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
if (!macroTree || !macroNodes) continue;
|
|
2873
|
-
const mac = macroNodes.find(m =>
|
|
2874
|
-
m.startPosition.row + 1 <= lineNo &&
|
|
2875
|
-
m.endPosition.row + 1 >= lineNo);
|
|
2876
|
-
if (!mac || !implRe.test(mac.text)) continue;
|
|
2877
|
-
const edit = renameIdentifierTokens(index, macroFile,
|
|
2878
|
-
lineNo, name, options.renameTo,
|
|
2879
|
-
null, null, { definitionNameOnly: true });
|
|
2880
|
-
if (edit.renamed === edit.source) continue;
|
|
2881
|
-
changes.push({
|
|
2882
|
-
file: rel,
|
|
2883
|
-
line: lineNo,
|
|
2884
|
-
expression: edit.source,
|
|
2885
|
-
suggestion: `Update macro-generated trait implementation: ${edit.renamed}`,
|
|
2886
|
-
newExpression: edit.renamed,
|
|
2887
|
-
isDefinition: true,
|
|
2888
|
-
editKind: 'definition',
|
|
2889
|
-
});
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
4182
|
}
|
|
2893
4183
|
|
|
2894
4184
|
// A slot rename must also carry every member's CALL sites — the
|
|
@@ -2903,8 +4193,15 @@ function plan(index, name, options = {}) {
|
|
|
2903
4193
|
`${site.file}:${site.line}:${Number.isInteger(site.column) ? site.column : '*'}`;
|
|
2904
4194
|
const seenSiteTokens = new Set(planCallSites.map(site => siteTokenKey(site)));
|
|
2905
4195
|
const slotMemberSites = [];
|
|
2906
|
-
|
|
2907
|
-
|
|
4196
|
+
// Members a macro generated (fix #374) are swept after, and
|
|
4197
|
+
// capped apart from, the written ones: a macro instantiated for
|
|
4198
|
+
// many types must not crowd written members out of the sweep.
|
|
4199
|
+
const writtenMembers = slotMemberDefs.filter(member => !member.macroExpansion);
|
|
4200
|
+
const generatedMembers = slotMemberDefs.filter(member => member.macroExpansion);
|
|
4201
|
+
const sweptMembers = [...writtenMembers.slice(0, SLOT_SWEEP_CAP),
|
|
4202
|
+
...generatedMembers.slice(0, SLOT_SWEEP_CAP)];
|
|
4203
|
+
for (const memberDef of sweptMembers) {
|
|
4204
|
+
const memberSweep = computePlanCallSites(index, name, memberDef, { analyzeArgs: !options.renameTo });
|
|
2908
4205
|
for (const site of memberSweep.sites) {
|
|
2909
4206
|
const key = siteTokenKey(site);
|
|
2910
4207
|
if (seenSiteTokens.has(key)) continue;
|
|
@@ -2918,9 +4215,16 @@ function plan(index, name, options = {}) {
|
|
|
2918
4215
|
for (const raw of memberSweep.rawUnverified || []) {
|
|
2919
4216
|
const exactInterfaceReceiver = goInterfaceSlot &&
|
|
2920
4217
|
raw.reason === 'possible-dispatch' &&
|
|
2921
|
-
goInterfaceSlot.interfaceNames.has(
|
|
4218
|
+
goInterfaceSlot.interfaceNames.has(
|
|
4219
|
+
String(raw.dispatchVia || '').split('.').pop());
|
|
4220
|
+
// A closed ambiguity holds where its receiver is a
|
|
4221
|
+
// project value: in a file of another build
|
|
4222
|
+
// configuration than the pin's, names may resolve to
|
|
4223
|
+
// other declarations (an import there, a variable here),
|
|
4224
|
+
// so the site stays a review item (fix #400).
|
|
2922
4225
|
const closedAmbiguity = goSlotCoversMethodAmbiguity(
|
|
2923
|
-
index, name, goInterfaceSlot, raw)
|
|
4226
|
+
index, name, goInterfaceSlot, raw) &&
|
|
4227
|
+
!goOtherConfiguration(index, def.file, raw.file);
|
|
2924
4228
|
if ((!exactInterfaceReceiver && !closedAmbiguity) ||
|
|
2925
4229
|
raw.externalContract) continue;
|
|
2926
4230
|
const relativePath = raw.relativePath ||
|
|
@@ -2964,14 +4268,16 @@ function plan(index, name, options = {}) {
|
|
|
2964
4268
|
});
|
|
2965
4269
|
}
|
|
2966
4270
|
}
|
|
2967
|
-
if (
|
|
4271
|
+
if (writtenMembers.length > SLOT_SWEEP_CAP || generatedMembers.length > SLOT_SWEEP_CAP) {
|
|
2968
4272
|
resolved.warnings.push({
|
|
2969
4273
|
message: `Dispatch slot has ${slotMemberDefs.length} member ` +
|
|
2970
4274
|
`definitions; call sites were swept for the first ` +
|
|
2971
|
-
`${SLOT_SWEEP_CAP}
|
|
4275
|
+
`${SLOT_SWEEP_CAP}${generatedMembers.length > 0
|
|
4276
|
+
? ' written and first ' + SLOT_SWEEP_CAP + ' macro-generated' : ''} — review the rest manually.`,
|
|
2972
4277
|
});
|
|
2973
4278
|
}
|
|
2974
4279
|
emitRenameCallSites(slotMemberSites);
|
|
4280
|
+
slotAccountSites.push(...slotMemberSites);
|
|
2975
4281
|
// A site a slot member's sweep CONFIRMED is a planned edit now —
|
|
2976
4282
|
// it no longer belongs in the pin's "may need this change" band.
|
|
2977
4283
|
const changedLines = new Set(changes.map(change =>
|
|
@@ -3004,12 +4310,12 @@ function plan(index, name, options = {}) {
|
|
|
3004
4310
|
!new Set(['c', 'cpp']).has(siblingLang)) continue;
|
|
3005
4311
|
const line = sibling.nameLine || sibling.startLine;
|
|
3006
4312
|
const rel = sibling.relativePath || sibling.file;
|
|
3007
|
-
if (
|
|
4313
|
+
if (lineTaken(sibling, rel, line)) continue;
|
|
3008
4314
|
const edit = renameIdentifierTokens(index, sibling.file,
|
|
3009
4315
|
line, name, options.renameTo,
|
|
3010
|
-
null, null, { definitionNameOnly: true });
|
|
4316
|
+
null, null, { definitionNameOnly: true, definition: sibling });
|
|
3011
4317
|
if (edit.renamed === edit.source) continue;
|
|
3012
|
-
changes.push({
|
|
4318
|
+
changes.push(tagTokenEdit({
|
|
3013
4319
|
file: rel,
|
|
3014
4320
|
line,
|
|
3015
4321
|
expression: edit.source,
|
|
@@ -3017,23 +4323,151 @@ function plan(index, name, options = {}) {
|
|
|
3017
4323
|
newExpression: edit.renamed,
|
|
3018
4324
|
isDefinition: true,
|
|
3019
4325
|
editKind: 'definition',
|
|
3020
|
-
});
|
|
4326
|
+
}, edit));
|
|
3021
4327
|
}
|
|
3022
4328
|
}
|
|
3023
4329
|
|
|
3024
|
-
//
|
|
3025
|
-
//
|
|
3026
|
-
//
|
|
3027
|
-
//
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
3030
|
-
//
|
|
3031
|
-
//
|
|
3032
|
-
//
|
|
3033
|
-
//
|
|
3034
|
-
//
|
|
3035
|
-
//
|
|
3036
|
-
//
|
|
4330
|
+
// Macro templates (fix #360, bytes-measured): identifiers inside a
|
|
4331
|
+
// macro_rules! transcriber bind at every expansion site, so the
|
|
4332
|
+
// AST never shows them as definitions or calls of the pin - and a
|
|
4333
|
+
// rename that leaves them behind breaks every expansion. Each token
|
|
4334
|
+
// spelling the name is classified by its token shape; a site is
|
|
4335
|
+
// edited only when its identity is proven from the pin:
|
|
4336
|
+
// `fn NAME` whose every owner (an `impl Trait for ..` header inside
|
|
4337
|
+
// the template, or the impl blocks that invoke the macro) is the
|
|
4338
|
+
// pin's own trait slot;
|
|
4339
|
+
// `.NAME(` / `Self::NAME(` forwarding inside such a template fn;
|
|
4340
|
+
// `NAME(` for a free-function pin whose binding reaches the pin in
|
|
4341
|
+
// the macro's file.
|
|
4342
|
+
// Every other token that can denote the pin is listed for review,
|
|
4343
|
+
// never omitted; tokens that provably denote another item (a `fn`
|
|
4344
|
+
// under a different trait, a dot-call against a free function) are
|
|
4345
|
+
// skipped.
|
|
4346
|
+
// A renamed TYPE's template tokens are decided by the type-reference
|
|
4347
|
+
// pass (fix #386).
|
|
4348
|
+
if (langTraits(planLang)?.macroTemplateBodies && planGroundSet?.perFile && !isTypeRenamePin(def)) {
|
|
4349
|
+
const pinTraitDefs = def.traitName
|
|
4350
|
+
? rustProjectTraits(index, def.traitName, def.file, def.startLine)
|
|
4351
|
+
: (def.className ? (index.symbols.get(def.className) || []).filter(d =>
|
|
4352
|
+
d.type === 'trait' && d.file === def.file) : []);
|
|
4353
|
+
const pinTraitKeys = new Set(pinTraitDefs.map(d => `${d.file}\0${d.startLine}`));
|
|
4354
|
+
const ownerIsPinTrait = (owner, macroFile) => owner.kind === 'impl' && owner.trait &&
|
|
4355
|
+
rustProjectTraits(index, owner.trait, macroFile)
|
|
4356
|
+
.some(d => pinTraitKeys.has(`${d.file}\0${d.startLine}`));
|
|
4357
|
+
// Files that can hold template tokens of this name: a ground
|
|
4358
|
+
// line inside an indexed macro definition's range. The AST pass
|
|
4359
|
+
// decides every token; this only avoids parsing other files.
|
|
4360
|
+
const templateFiles = [...planGroundSet.perFile.entries()].filter(([file, lineNos]) => {
|
|
4361
|
+
const entry = index.files.get(file);
|
|
4362
|
+
if (entry?.language !== planLang) return false;
|
|
4363
|
+
const macros = (entry.symbols || []).filter(symbol => symbol.type === 'macro');
|
|
4364
|
+
return macros.length > 0 && [...lineNos].some(lineNo => macros.some(macro =>
|
|
4365
|
+
macro.startLine <= lineNo && (macro.endLine || macro.startLine) >= lineNo));
|
|
4366
|
+
}).map(([file]) => file);
|
|
4367
|
+
const sites = rustMacroTemplateSites(index, name, templateFiles);
|
|
4368
|
+
const decisions = new Map(); // site -> 'edit' | 'review' | 'skip'
|
|
4369
|
+
const editedTemplateFns = new Set(); // file\0macro
|
|
4370
|
+
for (const site of sites.filter(item => item.shape === 'definition')) {
|
|
4371
|
+
let decision;
|
|
4372
|
+
if (pinTraitKeys.size > 0) {
|
|
4373
|
+
const matches = site.owners.filter(owner => ownerIsPinTrait(owner, site.file));
|
|
4374
|
+
if (site.owners.length > 0 && matches.length === site.owners.length) decision = 'edit';
|
|
4375
|
+
else if (matches.length === 0 && site.owners.length > 0 &&
|
|
4376
|
+
site.owners.every(owner => owner.kind === 'impl' && owner.trait)) decision = 'skip';
|
|
4377
|
+
else decision = 'review';
|
|
4378
|
+
} else if (def.className) {
|
|
4379
|
+
decision = site.owners.length > 0 && site.owners.every(owner =>
|
|
4380
|
+
owner.kind === 'impl' && owner.trait) ? 'skip' : 'review';
|
|
4381
|
+
} else {
|
|
4382
|
+
decision = site.owners.length > 0 && site.owners.every(owner =>
|
|
4383
|
+
owner.kind === 'impl') ? 'skip' : 'review';
|
|
4384
|
+
}
|
|
4385
|
+
decisions.set(site, decision);
|
|
4386
|
+
if (decision === 'edit') editedTemplateFns.add(`${site.file}\0${site.macroName}`);
|
|
4387
|
+
}
|
|
4388
|
+
for (const site of sites.filter(item => item.shape !== 'definition')) {
|
|
4389
|
+
let decision = 'review';
|
|
4390
|
+
const forwarding = site.enclosingFn === name &&
|
|
4391
|
+
editedTemplateFns.has(`${site.file}\0${site.macroName}`);
|
|
4392
|
+
if (site.shape === 'method-call') {
|
|
4393
|
+
decision = !def.className ? 'skip' : forwarding ? 'edit' : 'review';
|
|
4394
|
+
} else if (site.shape === 'path-call') {
|
|
4395
|
+
decision = forwarding && site.pathQualifier === 'Self' ? 'edit' : 'review';
|
|
4396
|
+
} else if (site.shape === 'call') {
|
|
4397
|
+
if (def.className) decision = 'skip'; // bare calls never denote methods
|
|
4398
|
+
else {
|
|
4399
|
+
decision = _nameBindingReaches(index, site.file, name, renameTargetFiles) === 'yes'
|
|
4400
|
+
? 'edit' : 'review';
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
decisions.set(site, decision);
|
|
4404
|
+
}
|
|
4405
|
+
const editsByLine = new Map();
|
|
4406
|
+
const unverifiedTemplateLines = new Set(planUnverified.map(site =>
|
|
4407
|
+
`${site.file}:${site.line}`));
|
|
4408
|
+
for (const [site, decision] of decisions) {
|
|
4409
|
+
if (decision === 'skip') continue;
|
|
4410
|
+
const key = `${site.file}\0${site.line}`;
|
|
4411
|
+
if (decision === 'edit') {
|
|
4412
|
+
if (!editsByLine.has(key)) editsByLine.set(key, { site, columns: [], definition: false });
|
|
4413
|
+
const entry = editsByLine.get(key);
|
|
4414
|
+
entry.columns.push(site.column);
|
|
4415
|
+
if (site.shape === 'definition') entry.definition = true;
|
|
4416
|
+
continue;
|
|
4417
|
+
}
|
|
4418
|
+
if (unverifiedTemplateLines.has(`${site.relativePath}:${site.line}`)) continue;
|
|
4419
|
+
if (reviewItems.some(item => item.file === site.relativePath &&
|
|
4420
|
+
item.line === site.line && item.templateDependency)) continue;
|
|
4421
|
+
reviewItems.push({
|
|
4422
|
+
file: site.relativePath,
|
|
4423
|
+
line: site.line,
|
|
4424
|
+
expression: (index.getLineContent(site.file, site.line) || '').trim(),
|
|
4425
|
+
suggestion: `Macro ${site.macroName || 'template'} spells "${name}" as a ` +
|
|
4426
|
+
`${site.shape.replace('-', ' ')} token; it binds at each expansion - ` +
|
|
4427
|
+
'rename it with the pin if the expansions reach it',
|
|
4428
|
+
needsReview: true,
|
|
4429
|
+
templateDependency: true,
|
|
4430
|
+
reviewReason: 'macro-template',
|
|
4431
|
+
editKind: 'macro-template',
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
for (const { site, columns, definition } of editsByLine.values()) {
|
|
4435
|
+
if (changes.some(change => change.file === site.relativePath &&
|
|
4436
|
+
change.line === site.line)) continue;
|
|
4437
|
+
const edit = renameIdentifierTokens(index, site.file, site.line, name,
|
|
4438
|
+
options.renameTo, columns.sort((a, b) => a - b));
|
|
4439
|
+
if (edit.renamed === edit.source) continue;
|
|
4440
|
+
changes.push(tagTokenEdit({
|
|
4441
|
+
file: site.relativePath,
|
|
4442
|
+
line: site.line,
|
|
4443
|
+
expression: edit.source,
|
|
4444
|
+
suggestion: `Update macro template ${site.macroName || ''}: ${edit.renamed}`.replace(' ', ' '),
|
|
4445
|
+
newExpression: edit.renamed,
|
|
4446
|
+
...(definition && { isDefinition: true }),
|
|
4447
|
+
editKind: definition ? 'definition' : 'call',
|
|
4448
|
+
}, edit));
|
|
4449
|
+
}
|
|
4450
|
+
if (editsByLine.size > 0) {
|
|
4451
|
+
const changedLines = new Set(changes.map(change => `${change.file}:${change.line}`));
|
|
4452
|
+
const kept = planUnverified.filter(site => !changedLines.has(`${site.file}:${site.line}`));
|
|
4453
|
+
planUnverified.length = 0;
|
|
4454
|
+
planUnverified.push(...kept);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
// Reference-position usages are rename edits too (outcome-eval
|
|
4459
|
+
// flask, 2026-08-18): `return decorator`, `callback=handler`,
|
|
4460
|
+
// `cls.method` values. Call syntax flows through the tiered sweep;
|
|
4461
|
+
// references never did — a plan-following rename left them on the
|
|
4462
|
+
// old name and the toolchain rejected the result. Evidence
|
|
4463
|
+
// discipline mirrors the caller engine:
|
|
4464
|
+
// - same-file, non-method pin: nearest-binder containment — the
|
|
4465
|
+
// innermost same-name def whose scope container holds the line
|
|
4466
|
+
// must be the pin (a sibling nested `decorator` keeps its own
|
|
4467
|
+
// references; ties bind nothing);
|
|
4468
|
+
// - same-file, method pin: self/cls/this-received inside the
|
|
4469
|
+
// pin's class range (receiver evidence read from the line);
|
|
4470
|
+
// - cross-file, non-method pin: the #217 import-ownership chase —
|
|
3037
4471
|
// 'yes' edits, 'unknown' surfaces needsReview (no synthesized
|
|
3038
4472
|
// edit), 'no' skips;
|
|
3039
4473
|
// - cross-file method references are receiver-blind here:
|
|
@@ -3071,10 +4505,18 @@ function plan(index, name, options = {}) {
|
|
|
3071
4505
|
}
|
|
3072
4506
|
return best; // null = module scope
|
|
3073
4507
|
};
|
|
4508
|
+
// A prototype and its definition (C/C++ forward declarations,
|
|
4509
|
+
// overload signatures) are one callable (fix #396): each binds
|
|
4510
|
+
// the name as the pin.
|
|
4511
|
+
const pinIdentity = new Set(require('./callers')._closeCallableIdentityGroup(
|
|
4512
|
+
index, [def], definitions));
|
|
3074
4513
|
const binders = definitions
|
|
3075
4514
|
.filter(candidate => candidate.file === def.file &&
|
|
3076
4515
|
!NON_CALLABLE_TYPES.has(candidate.type))
|
|
3077
|
-
.map(candidate => ({
|
|
4516
|
+
.map(candidate => ({
|
|
4517
|
+
def: pinIdentity.has(candidate) ? def : candidate,
|
|
4518
|
+
container: containerOf(candidate),
|
|
4519
|
+
}));
|
|
3078
4520
|
const classKinds = new Set(['class', 'struct', 'interface', 'trait',
|
|
3079
4521
|
'record', 'enum', 'namespace']);
|
|
3080
4522
|
const pinClassRange = def.className
|
|
@@ -3095,92 +4537,236 @@ function plan(index, name, options = {}) {
|
|
|
3095
4537
|
(symbol.endLine || symbol.startLine) >= line);
|
|
3096
4538
|
};
|
|
3097
4539
|
const pinContainer = containerOf(def);
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
4540
|
+
const methodGroupPin = !!langTraits(planLang)?.methodGroupReferences &&
|
|
4541
|
+
(def.type === 'method' || def.memberType === 'method' || !!def.isMethod);
|
|
4542
|
+
// Where each bare reference resolves (fix #392): the parser
|
|
4543
|
+
// records the language's own lexical scoping on the usage
|
|
4544
|
+
// (`scopeBinding`), so a reference inside a function body or on a
|
|
4545
|
+
// decorator line is decided instead of reviewed. The pin must be
|
|
4546
|
+
// declared at file scope for a file-scope resolution to reach it
|
|
4547
|
+
// (a Rust inline module is its own scope).
|
|
4548
|
+
const pinNameLine = def.nameLine || def.startLine;
|
|
4549
|
+
const pinAtFileScope = !pinContainer && !fileSymbols.some(symbol =>
|
|
4550
|
+
symbol.type === 'module' && symbol !== def &&
|
|
4551
|
+
symbol.startLine <= pinNameLine &&
|
|
4552
|
+
(symbol.endLine || symbol.startLine) >= pinNameLine &&
|
|
4553
|
+
!(symbol.startLine === def.startLine && symbol.name === def.name));
|
|
4554
|
+
const fileScopeBinders = binders.filter(binder => !binder.container);
|
|
4555
|
+
const pinIsFileBinding = pinAtFileScope && fileScopeBinders.length >= 1 &&
|
|
4556
|
+
fileScopeBinders.every(binder => binder.def === def);
|
|
4557
|
+
// One record's verdict: 'edit', 'review' or null (not the pin).
|
|
4558
|
+
const verdictFor = (ref) => {
|
|
4559
|
+
// A C++ qualified reference (`template auto ns::f(float)
|
|
4560
|
+
// -> R;`, `&Cls::m`, fix #393) names what its qualifier
|
|
4561
|
+
// resolves to from the reference's own scope.
|
|
4562
|
+
if (ref.receiver && index.files.get(ref.file)?.language === 'cpp') {
|
|
4563
|
+
return cppQualifiedReferenceVerdict(index, ref, def);
|
|
4564
|
+
}
|
|
4565
|
+
// A function declared in a function body (a C# local
|
|
4566
|
+
// function, a nested def) is in scope only in its block:
|
|
4567
|
+
// never from another file, and never as the member of an
|
|
4568
|
+
// object (`profile.Items`, fix #395).
|
|
4569
|
+
if (pinContainer && !def.className && (ref.file !== def.file || ref.receiver)) return null;
|
|
4570
|
+
// A property of an object (an object-literal member, `obj.f =
|
|
4571
|
+
// fn`) is reached through a receiver, never as a bare
|
|
4572
|
+
// binding: member accesses are decided by the call-site and
|
|
4573
|
+
// accessor passes (fix #397).
|
|
4574
|
+
if (ref.receiver && def.memberAssigned && !def.className) return null;
|
|
3105
4575
|
if (ref.file === def.file) {
|
|
4576
|
+
// C# method groups are decided by the language's lookup
|
|
4577
|
+
// in their own pass (fix #395).
|
|
4578
|
+
if (def.className && methodGroupPin) return null;
|
|
3106
4579
|
if (def.className) {
|
|
3107
4580
|
const lineText = ref.content ||
|
|
3108
4581
|
index.getLineContent(def.file, ref.line) || '';
|
|
3109
|
-
|
|
3110
|
-
|
|
4582
|
+
// `this` names the innermost enclosing class (fix
|
|
4583
|
+
// #390: `this.size = size` in a nested class's
|
|
4584
|
+
// constructor is its own field), and where fields
|
|
4585
|
+
// and methods are separate namespaces a paren-less
|
|
4586
|
+
// `this.m` is never the method (Java, Rust).
|
|
4587
|
+
const innermostClass = fileSymbols.filter(symbol =>
|
|
4588
|
+
classKinds.has(symbol.type) && symbol.startLine <= ref.line &&
|
|
4589
|
+
(symbol.endLine || symbol.startLine) >= ref.line)
|
|
4590
|
+
.sort((a, b) => ((a.endLine || a.startLine) - a.startLine) -
|
|
4591
|
+
((b.endLine || b.startLine) - b.startLine))[0] || null;
|
|
4592
|
+
if (pinClassRange && innermostClass === pinClassRange &&
|
|
4593
|
+
!langTraits(planLang)?.fieldsAndMethodsSeparate &&
|
|
3111
4594
|
selfReceived.test(lineText)) {
|
|
3112
|
-
|
|
3113
|
-
}
|
|
3114
|
-
} else {
|
|
3115
|
-
let winner = null;
|
|
3116
|
-
let ambiguous = false;
|
|
3117
|
-
for (const binder of binders) {
|
|
3118
|
-
const contains = !binder.container ||
|
|
3119
|
-
(binder.container.start <= ref.line &&
|
|
3120
|
-
binder.container.end >= ref.line);
|
|
3121
|
-
if (!contains) continue;
|
|
3122
|
-
const size = binder.container
|
|
3123
|
-
? binder.container.end - binder.container.start
|
|
3124
|
-
: Infinity;
|
|
3125
|
-
if (!winner || size < winner.size) {
|
|
3126
|
-
winner = { def: binder.def, size };
|
|
3127
|
-
ambiguous = false;
|
|
3128
|
-
} else if (size === winner.size &&
|
|
3129
|
-
binder.def !== winner.def) {
|
|
3130
|
-
ambiguous = true;
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
if (winner && !ambiguous && winner.def === def) {
|
|
3134
|
-
if (pinContainer) {
|
|
3135
|
-
verdict = 'edit'; // nested pin binds its container
|
|
3136
|
-
} else if (!insideFunctionLike(def.file, ref.line)) {
|
|
3137
|
-
verdict = 'edit'; // module-scope row, module pin
|
|
3138
|
-
} else {
|
|
3139
|
-
verdict = 'review'; // possible local shadow
|
|
3140
|
-
}
|
|
4595
|
+
return 'edit';
|
|
3141
4596
|
}
|
|
4597
|
+
return null;
|
|
4598
|
+
}
|
|
4599
|
+
if (!ref.receiver && ref.scopeBinding === 'module') {
|
|
4600
|
+
// Resolves at file scope: the pin when it is the
|
|
4601
|
+
// file's only binding of the name there (a nested pin
|
|
4602
|
+
// is not visible there).
|
|
4603
|
+
if (pinIsFileBinding) return 'edit';
|
|
4604
|
+
if (!pinAtFileScope) return null;
|
|
4605
|
+
} else if (!ref.receiver && ref.scopeBinding === 'local') {
|
|
4606
|
+
// An enclosing function or block binds the name: the
|
|
4607
|
+
// pin only when that binding is the pin's own def.
|
|
4608
|
+
return Array.isArray(ref.scopeDefLines) &&
|
|
4609
|
+
ref.scopeDefLines.includes(pinNameLine) ? 'edit' : null;
|
|
3142
4610
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
} else if (!ref.receiver) {
|
|
3160
|
-
const ownership = _nameBindingReaches(
|
|
3161
|
-
index, ref.file, name, renameTargetFiles);
|
|
3162
|
-
if (ownership === 'yes' &&
|
|
3163
|
-
!insideFunctionLike(ref.file, ref.line)) {
|
|
3164
|
-
verdict = 'edit';
|
|
3165
|
-
} else if (ownership !== 'no') {
|
|
3166
|
-
verdict = 'review';
|
|
4611
|
+
let winner = null;
|
|
4612
|
+
let ambiguous = false;
|
|
4613
|
+
for (const binder of binders) {
|
|
4614
|
+
const contains = !binder.container ||
|
|
4615
|
+
(binder.container.start <= ref.line &&
|
|
4616
|
+
binder.container.end >= ref.line);
|
|
4617
|
+
if (!contains) continue;
|
|
4618
|
+
const size = binder.container
|
|
4619
|
+
? binder.container.end - binder.container.start
|
|
4620
|
+
: Infinity;
|
|
4621
|
+
if (!winner || size < winner.size) {
|
|
4622
|
+
winner = { def: binder.def, size };
|
|
4623
|
+
ambiguous = false;
|
|
4624
|
+
} else if (size === winner.size &&
|
|
4625
|
+
binder.def !== winner.def) {
|
|
4626
|
+
ambiguous = true;
|
|
3167
4627
|
}
|
|
3168
4628
|
}
|
|
4629
|
+
if (winner && !ambiguous && winner.def === def) {
|
|
4630
|
+
if (pinContainer) return 'edit'; // nested pin binds its container
|
|
4631
|
+
if (!insideFunctionLike(def.file, ref.line)) return 'edit'; // module-scope row
|
|
4632
|
+
return 'review'; // possible local shadow
|
|
4633
|
+
}
|
|
4634
|
+
return null;
|
|
4635
|
+
}
|
|
4636
|
+
if (def.className) return null;
|
|
4637
|
+
// Module-attribute references carry stronger evidence
|
|
4638
|
+
// than a bare name: `import requests; requests.put` binds
|
|
4639
|
+
// through requests' export chain even inside a function.
|
|
4640
|
+
// Parser-side local-shadow evidence is a hard guard — a
|
|
4641
|
+
// parameter/assignment named requests defeats the import.
|
|
4642
|
+
const moduleOwnership = ref.receiver &&
|
|
4643
|
+
!ref.receiverLocalBinding
|
|
4644
|
+
? _moduleAttributeBindingReaches(
|
|
4645
|
+
index, ref.file, ref.receiver, name,
|
|
4646
|
+
renameTargetFiles)
|
|
4647
|
+
: null;
|
|
4648
|
+
if (moduleOwnership === 'yes') return 'edit';
|
|
4649
|
+
if (moduleOwnership === 'unknown') return 'review';
|
|
4650
|
+
if (ref.receiver) return null;
|
|
4651
|
+
// A local binding of the name is never the pin (fix #392); a
|
|
4652
|
+
// file-scope resolution follows the file's binding of the name.
|
|
4653
|
+
if (ref.scopeBinding === 'local') return null;
|
|
4654
|
+
// C (fix #396): a file-scope reference names the file's own
|
|
4655
|
+
// function of that name, else the external-linkage function
|
|
4656
|
+
// its translation unit links, as a call does.
|
|
4657
|
+
if (ref.scopeBinding === 'module' && cExternalReferenceLanguage(index, ref.file)) {
|
|
4658
|
+
return cExternalReferenceVerdict(index, ref, def, pinIdentity, definitions);
|
|
4659
|
+
}
|
|
4660
|
+
const ownership = _nameBindingReaches(
|
|
4661
|
+
index, ref.file, name, renameTargetFiles);
|
|
4662
|
+
if (ownership === 'yes' &&
|
|
4663
|
+
(ref.scopeBinding === 'module' ||
|
|
4664
|
+
!insideFunctionLike(ref.file, ref.line))) {
|
|
4665
|
+
return 'edit';
|
|
3169
4666
|
}
|
|
4667
|
+
return ownership !== 'no' ? 'review' : null;
|
|
4668
|
+
};
|
|
4669
|
+
const eligible = (ref) => {
|
|
4670
|
+
if (ref.usageType !== 'reference' || ref.isDefinition) return false;
|
|
4671
|
+
// Keyword-argument names, object keys and struct field keys
|
|
4672
|
+
// are not references to a non-member binding (a member pin
|
|
4673
|
+
// reads its receiver tokens from the line below).
|
|
4674
|
+
if (!def.className && !ref.receiver && ref.scopeBinding === 'none') return false;
|
|
4675
|
+
// An attribute path or derive entry names a macro (fix #377):
|
|
4676
|
+
// never a reference to a same-named fn or value.
|
|
4677
|
+
if (ref.namespace === 'macro' && !isMacroNamespaceDefinition(def)) return false;
|
|
4678
|
+
return true;
|
|
4679
|
+
};
|
|
4680
|
+
for (const first of usages) {
|
|
4681
|
+
if (first.usageType !== 'reference' || first.isDefinition) continue;
|
|
4682
|
+
// Every record of the line is decided (fix #392): two
|
|
4683
|
+
// references on one line are two tokens.
|
|
4684
|
+
const records = [first, ...(first.sameLine || [])].filter(eligible);
|
|
4685
|
+
if (records.length === 0) continue;
|
|
4686
|
+
const ref = records[0];
|
|
4687
|
+
const rel = ref.relativePath || ref.file;
|
|
4688
|
+
if (unverifiedLines.has(`${rel}:${ref.line}`)) continue;
|
|
4689
|
+
const existing = changes.find(change =>
|
|
4690
|
+
change.file === rel && change.line === ref.line);
|
|
4691
|
+
const verdicts = records.map(verdictFor);
|
|
4692
|
+
let verdict = verdicts.includes('review') ? 'review'
|
|
4693
|
+
: verdicts.includes('edit') ? 'edit' : null;
|
|
3170
4694
|
if (!verdict) continue;
|
|
4695
|
+
// A same-file member pin is referenced through its receiver
|
|
4696
|
+
// (fix #376): edit exactly the `this.NAME` / `self.NAME`
|
|
4697
|
+
// member tokens on the line (both in `this.m =
|
|
4698
|
+
// this.m.bind(this)`), never an object-literal key or other
|
|
4699
|
+
// same-spelled property (`{ m: this.m }` keeps its key).
|
|
4700
|
+
let refColumns = records.filter((record, i) => verdicts[i] === 'edit')
|
|
4701
|
+
.map(record => record.column);
|
|
4702
|
+
if (!refColumns.every(Number.isInteger)) refColumns = null;
|
|
4703
|
+
if (verdict === 'edit' && ref.file === def.file && def.className) {
|
|
4704
|
+
const selfColumns = selfMemberTokenColumns(index, def.file, ref.line, name);
|
|
4705
|
+
if (selfColumns.length === 0) verdict = 'review';
|
|
4706
|
+
else refColumns = selfColumns;
|
|
4707
|
+
}
|
|
4708
|
+
// An object-literal shorthand `{ name }` keeps its key (fix
|
|
4709
|
+
// #397, immer-measured: `{ applyPatches_ }` passed where a
|
|
4710
|
+
// Plugin object is expected): the value is spelled out,
|
|
4711
|
+
// `{ name: renamed }`.
|
|
4712
|
+
const shorthandEdits = verdict === 'edit' ? records.filter((record, i) =>
|
|
4713
|
+
verdicts[i] === 'edit' && record.shorthandProperty) : [];
|
|
4714
|
+
if (shorthandEdits.length > 0) {
|
|
4715
|
+
const mixed = shorthandEdits.length !== records.filter((record, i) =>
|
|
4716
|
+
verdicts[i] === 'edit').length;
|
|
4717
|
+
if (existing || mixed || !refColumns) {
|
|
4718
|
+
if (!existing?.needsReview) {
|
|
4719
|
+
reviewItems.push({
|
|
4720
|
+
file: rel,
|
|
4721
|
+
line: ref.line,
|
|
4722
|
+
expression: (ref.content || '').trim(),
|
|
4723
|
+
suggestion: `Shorthand property "${name}" names the renamed binding: ` +
|
|
4724
|
+
`write it as "${name}: ${options.renameTo}" (the key stays)`,
|
|
4725
|
+
needsReview: true,
|
|
4726
|
+
editKind: 'reference',
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
4729
|
+
continue;
|
|
4730
|
+
}
|
|
4731
|
+
const edit = renameIdentifierTokens(index, ref.file, ref.line, name,
|
|
4732
|
+
`${name}: ${options.renameTo}`, refColumns);
|
|
4733
|
+
if (edit.renamed === edit.source) continue;
|
|
4734
|
+
changes.push(tagTokenEdit({
|
|
4735
|
+
file: rel,
|
|
4736
|
+
line: ref.line,
|
|
4737
|
+
expression: edit.source,
|
|
4738
|
+
suggestion: `Update reference: ${edit.renamed}`,
|
|
4739
|
+
newExpression: edit.renamed,
|
|
4740
|
+
editKind: 'reference',
|
|
4741
|
+
}, edit));
|
|
4742
|
+
continue;
|
|
4743
|
+
}
|
|
4744
|
+
if (existing) {
|
|
4745
|
+
// A line another pass already edits (a call beside the
|
|
4746
|
+
// reference) gains the reference tokens.
|
|
4747
|
+
// A member pin's own `this.m` tokens join a bound call's
|
|
4748
|
+
// edit on the line (`this.m = this.m.bind(this)`, fix #397).
|
|
4749
|
+
if (verdict === 'edit' && (!def.className || ref.file === def.file) &&
|
|
4750
|
+
existing.newExpression !== undefined &&
|
|
4751
|
+
!existing.needsReview && refColumns) {
|
|
4752
|
+
const edit = renameIdentifierTokens(index, ref.file,
|
|
4753
|
+
ref.line, name, options.renameTo, refColumns);
|
|
4754
|
+
mergeTokenEdit(existing, edit);
|
|
4755
|
+
}
|
|
4756
|
+
continue;
|
|
4757
|
+
}
|
|
3171
4758
|
if (verdict === 'edit') {
|
|
3172
4759
|
const edit = renameIdentifierTokens(index, ref.file,
|
|
3173
|
-
ref.line, name, options.renameTo,
|
|
3174
|
-
Number.isInteger(ref.column) ? [ref.column] : null);
|
|
4760
|
+
ref.line, name, options.renameTo, refColumns);
|
|
3175
4761
|
if (edit.renamed === edit.source) continue;
|
|
3176
|
-
changes.push({
|
|
4762
|
+
changes.push(tagTokenEdit({
|
|
3177
4763
|
file: rel,
|
|
3178
4764
|
line: ref.line,
|
|
3179
4765
|
expression: edit.source,
|
|
3180
4766
|
suggestion: `Update reference: ${edit.renamed}`,
|
|
3181
4767
|
newExpression: edit.renamed,
|
|
3182
4768
|
editKind: 'reference',
|
|
3183
|
-
});
|
|
4769
|
+
}, edit));
|
|
3184
4770
|
} else {
|
|
3185
4771
|
changes.push({
|
|
3186
4772
|
file: rel,
|
|
@@ -3214,8 +4800,26 @@ function plan(index, name, options = {}) {
|
|
|
3214
4800
|
const ref = refs[0];
|
|
3215
4801
|
const columns = refs.map(item => item.column)
|
|
3216
4802
|
.filter(Number.isInteger);
|
|
4803
|
+
// A shorthand destructuring key (`const { body } = this`)
|
|
4804
|
+
// reads the property into a local of the same name: the key
|
|
4805
|
+
// is renamed and the local kept (`{ bodyZ: body }`, fix #397).
|
|
4806
|
+
const shorthandKeys = refs.filter(item => item.patternKey?.shorthand);
|
|
4807
|
+
if (shorthandKeys.length > 0 && (shorthandKeys.length !== refs.length ||
|
|
4808
|
+
columns.length !== refs.length)) {
|
|
4809
|
+
changes.push({
|
|
4810
|
+
file: ref.file,
|
|
4811
|
+
line: ref.line,
|
|
4812
|
+
expression: ref.expression,
|
|
4813
|
+
suggestion: `Rename the destructured property "${name}" to "${options.renameTo}" ` +
|
|
4814
|
+
'manually, keeping the local binding',
|
|
4815
|
+
needsReview: true,
|
|
4816
|
+
editKind: 'reference',
|
|
4817
|
+
});
|
|
4818
|
+
continue;
|
|
4819
|
+
}
|
|
3217
4820
|
const edit = renameIdentifierTokens(index, ref.absoluteFile,
|
|
3218
|
-
ref.line, name,
|
|
4821
|
+
ref.line, name, shorthandKeys.length > 0
|
|
4822
|
+
? `${options.renameTo}: ${name}` : options.renameTo,
|
|
3219
4823
|
columns.length === refs.length ? columns : null);
|
|
3220
4824
|
if (edit.renamed === edit.source) continue;
|
|
3221
4825
|
const concrete = {
|
|
@@ -3232,7 +4836,11 @@ function plan(index, name, options = {}) {
|
|
|
3232
4836
|
if (existing) Object.assign(existing, concrete);
|
|
3233
4837
|
else changes.push(concrete);
|
|
3234
4838
|
}
|
|
4839
|
+
const listedAccess = new Set();
|
|
3235
4840
|
for (const ref of accessorReferences.unverified) {
|
|
4841
|
+
const lineKey = `${ref.file}:${ref.line}`;
|
|
4842
|
+
if (listedAccess.has(lineKey)) continue;
|
|
4843
|
+
listedAccess.add(lineKey);
|
|
3236
4844
|
changes.push({
|
|
3237
4845
|
file: ref.file,
|
|
3238
4846
|
line: ref.line,
|
|
@@ -3276,15 +4884,36 @@ function plan(index, name, options = {}) {
|
|
|
3276
4884
|
const definitionSource = index.getLineContent(def.file, definitionLine).trim();
|
|
3277
4885
|
const existingDefinitionLine = changes.find(change =>
|
|
3278
4886
|
change.file === definitionFile && change.line === definitionLine);
|
|
3279
|
-
|
|
4887
|
+
// A definition a macro_rules! transcriber generated (fix #374) is
|
|
4888
|
+
// spelled in the macro's template, never at the invocation: the
|
|
4889
|
+
// template pass below edits or lists it; the invocation line is only
|
|
4890
|
+
// pointed at for review.
|
|
4891
|
+
if (def.macroExpansion?.origin === 'template') {
|
|
4892
|
+
reviewItems.push({
|
|
4893
|
+
file: definitionFile,
|
|
4894
|
+
line: definitionLine,
|
|
4895
|
+
expression: definitionSource,
|
|
4896
|
+
suggestion: `Generated by macro ${def.macroExpansion.macro} (${def.macroExpansion.definition}); ` +
|
|
4897
|
+
'the declaration is written in the macro template',
|
|
4898
|
+
needsReview: true,
|
|
4899
|
+
templateDependency: true,
|
|
4900
|
+
editKind: 'macro-generated-definition',
|
|
4901
|
+
});
|
|
4902
|
+
} else if (existingDefinitionLine) {
|
|
3280
4903
|
existingDefinitionLine.isDefinition = true;
|
|
3281
4904
|
existingDefinitionLine.editKind = 'definition';
|
|
3282
4905
|
if (options.renameTo) {
|
|
3283
|
-
const
|
|
4906
|
+
const defEdit = renameIdentifierTokens(index, def.file,
|
|
3284
4907
|
definitionLine, name, options.renameTo,
|
|
3285
|
-
null, null, { definitionNameOnly: true })
|
|
3286
|
-
|
|
3287
|
-
|
|
4908
|
+
null, null, { definitionNameOnly: true, definition: def });
|
|
4909
|
+
// Keep the tokens the line's other edit renames (a recursive or
|
|
4910
|
+
// delegating call on the declaration line) (fix #376).
|
|
4911
|
+
if (!(existingDefinitionLine.newExpression !== undefined &&
|
|
4912
|
+
mergeTokenEdit(existingDefinitionLine, defEdit))) {
|
|
4913
|
+
existingDefinitionLine.newExpression = defEdit.renamed;
|
|
4914
|
+
tagTokenEdit(existingDefinitionLine, defEdit);
|
|
4915
|
+
}
|
|
4916
|
+
existingDefinitionLine.suggestion = `Update definition: ${existingDefinitionLine.newExpression}`;
|
|
3288
4917
|
}
|
|
3289
4918
|
} else {
|
|
3290
4919
|
const definitionChange = {
|
|
@@ -3295,11 +4924,13 @@ function plan(index, name, options = {}) {
|
|
|
3295
4924
|
editKind: 'definition',
|
|
3296
4925
|
};
|
|
3297
4926
|
if (options.renameTo) {
|
|
3298
|
-
const
|
|
4927
|
+
const defEdit = renameIdentifierTokens(index, def.file,
|
|
3299
4928
|
definitionLine, name, options.renameTo,
|
|
3300
|
-
null, null, { definitionNameOnly: true })
|
|
4929
|
+
null, null, { definitionNameOnly: true, definition: def });
|
|
4930
|
+
const renamed = defEdit.renamed;
|
|
3301
4931
|
definitionChange.newExpression = renamed;
|
|
3302
4932
|
definitionChange.suggestion = `Update definition: ${renamed}`;
|
|
4933
|
+
tagTokenEdit(definitionChange, defEdit);
|
|
3303
4934
|
} else {
|
|
3304
4935
|
definitionChange.suggestion =
|
|
3305
4936
|
`Update declaration signature to: ${newSignature}`;
|
|
@@ -3311,6 +4942,674 @@ function plan(index, name, options = {}) {
|
|
|
3311
4942
|
changes.unshift(definitionChange);
|
|
3312
4943
|
}
|
|
3313
4944
|
|
|
4945
|
+
// Members a macro invocation declares (fix #385: `ERROR_DEF(Base, Name)`
|
|
4946
|
+
// in the class body expands to Name's constructors) are spelled by the
|
|
4947
|
+
// invocation's argument: renaming the class renames that token too, or
|
|
4948
|
+
// the expansion declares members of the old name.
|
|
4949
|
+
if (options.renameTo && ['class', 'struct', 'union'].includes(def.type) &&
|
|
4950
|
+
langTraits(planLang)?.textualIncludes) {
|
|
4951
|
+
const invocationLines = new Set();
|
|
4952
|
+
for (const member of index.files.get(def.file)?.symbols || []) {
|
|
4953
|
+
if (!member.generatedByMacro || member.className !== def.name || member.name !== name ||
|
|
4954
|
+
member.startLine < def.startLine || member.endLine > def.endLine) continue;
|
|
4955
|
+
invocationLines.add(member.startLine);
|
|
4956
|
+
}
|
|
4957
|
+
for (const line of [...invocationLines].sort((a, b) => a - b)) {
|
|
4958
|
+
if (changes.some(change => change.file === definitionFile && change.line === line)) continue;
|
|
4959
|
+
const edit = renameIdentifierTokens(index, def.file, line, name, options.renameTo);
|
|
4960
|
+
if (edit.renamed === edit.source) continue;
|
|
4961
|
+
changes.push(markEvidence(tagTokenEdit({
|
|
4962
|
+
file: definitionFile,
|
|
4963
|
+
line,
|
|
4964
|
+
expression: edit.source,
|
|
4965
|
+
suggestion: `Rename the macro argument that declares members of ${name}: ${edit.renamed}`,
|
|
4966
|
+
newExpression: edit.renamed,
|
|
4967
|
+
editKind: 'reference',
|
|
4968
|
+
}, edit)));
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
// C++ using-declarations that name the renamed entity (fix #385, the
|
|
4973
|
+
// spdlog rename harness's silent breaks: `using details::os::
|
|
4974
|
+
// filename_to_str;` before bare calls the plan already edits). The
|
|
4975
|
+
// written qualifier resolves from the declaration's enclosing
|
|
4976
|
+
// namespaces (macro-opened ones included); one that names the pin's
|
|
4977
|
+
// namespace (or its class, for `using Base::m;`) is edited, one that
|
|
4978
|
+
// only ends like it is listed for review.
|
|
4979
|
+
if (options.renameTo && langTraits(planLang)?.textualIncludes) {
|
|
4980
|
+
const { effectiveNamespace } = require('./cpp-scope');
|
|
4981
|
+
const pinNamespace = def.className ? null : effectiveNamespace(index, def);
|
|
4982
|
+
for (const [file, entry] of index.files) {
|
|
4983
|
+
for (const fact of entry.cppUsings || []) {
|
|
4984
|
+
if (fact.kind !== 'declaration' || fact.name !== name) continue;
|
|
4985
|
+
const rel = entry.relativePath;
|
|
4986
|
+
if (changes.some(change => change.file === rel && change.line === fact.line)) continue;
|
|
4987
|
+
const absolute = fact.target.startsWith('::');
|
|
4988
|
+
const qualifier = fact.target.replace(/^::/, '').split('::').slice(0, -1).join('::');
|
|
4989
|
+
let exact = false;
|
|
4990
|
+
let near = false;
|
|
4991
|
+
if (def.className) {
|
|
4992
|
+
const owner = qualifier.split('::').pop();
|
|
4993
|
+
exact = owner === def.className;
|
|
4994
|
+
} else {
|
|
4995
|
+
const site = effectiveNamespace(index, { file, startLine: fact.line, namespace: fact.namespace });
|
|
4996
|
+
const scopes = [];
|
|
4997
|
+
if (!absolute) {
|
|
4998
|
+
const parts = site ? site.split('::') : [];
|
|
4999
|
+
for (let i = parts.length; i > 0; i--) scopes.push(parts.slice(0, i).join('::'));
|
|
5000
|
+
}
|
|
5001
|
+
scopes.push('');
|
|
5002
|
+
exact = scopes.some(scope => (scope && qualifier ? `${scope}::${qualifier}` : scope || qualifier) === pinNamespace);
|
|
5003
|
+
near = !exact && (pinNamespace === qualifier || pinNamespace.endsWith(`::${qualifier}`));
|
|
5004
|
+
}
|
|
5005
|
+
if (!exact && !near) continue;
|
|
5006
|
+
const edit = renameIdentifierTokens(index, file, fact.line, name, options.renameTo);
|
|
5007
|
+
if (exact && edit.renamed !== edit.source) {
|
|
5008
|
+
changes.push(markEvidence(tagTokenEdit({
|
|
5009
|
+
file: rel,
|
|
5010
|
+
line: fact.line,
|
|
5011
|
+
expression: edit.source,
|
|
5012
|
+
suggestion: `Rename the using-declaration: ${edit.renamed}`,
|
|
5013
|
+
newExpression: edit.renamed,
|
|
5014
|
+
editKind: 'reference',
|
|
5015
|
+
}, edit)));
|
|
5016
|
+
} else {
|
|
5017
|
+
reviewItems.push({
|
|
5018
|
+
file: rel,
|
|
5019
|
+
line: fact.line,
|
|
5020
|
+
expression: index.getLineContent(file, fact.line).trim(),
|
|
5021
|
+
suggestion: `A using-declaration names "${name}" through a qualifier UCN cannot resolve to the renamed declaration; rename it with the declaration if it names it`,
|
|
5022
|
+
needsReview: true,
|
|
5023
|
+
editKind: 'reference',
|
|
5024
|
+
});
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
// Every token that names a renamed TYPE (fix #386): base clauses,
|
|
5031
|
+
// field/parameter/return/local and generic-argument types, casts and
|
|
5032
|
+
// type tests, static member qualifiers, constructor and destructor
|
|
5033
|
+
// names, member initializers, qualified spellings and imports. Tokens
|
|
5034
|
+
// are resolved by the language's own name lookup; a token UCN cannot
|
|
5035
|
+
// resolve to exactly the renamed declaration is a review item.
|
|
5036
|
+
let typePass = null;
|
|
5037
|
+
const fileRenames = [];
|
|
5038
|
+
if (options.renameTo && isTypeRenamePin(def) && planGroundSet &&
|
|
5039
|
+
!def.macroExpansion) {
|
|
5040
|
+
typePass = applyTypeReferencePass(index, {
|
|
5041
|
+
def, name, renameTo: options.renameTo, changes, reviewItems,
|
|
5042
|
+
planUnverified, groundSet: planGroundSet,
|
|
5043
|
+
});
|
|
5044
|
+
// javac requires a public top-level type in a file named after it:
|
|
5045
|
+
// the file is renamed with the type.
|
|
5046
|
+
const pathMod = require('path');
|
|
5047
|
+
const extension = pathMod.extname(def.file);
|
|
5048
|
+
if (langTraits(planLang)?.typeNamedSourceFile && !def.enclosingType && !def.isNested &&
|
|
5049
|
+
pathMod.basename(def.file, extension) === name) {
|
|
5050
|
+
const from = def.relativePath || pathMod.relative(index.root, def.file);
|
|
5051
|
+
fileRenames.push({
|
|
5052
|
+
from,
|
|
5053
|
+
to: pathMod.join(pathMod.dirname(from), `${options.renameTo}${extension}`),
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
|
|
5058
|
+
// Token pasting can spell the renamed name without writing it (fix
|
|
5059
|
+
// #396: `struct sdshdr##T` in SDS_HDR_VAR, invoked as SDS_HDR_VAR(8, s)):
|
|
5060
|
+
// the pasting macro and each invocation that produces the name are
|
|
5061
|
+
// review items; nothing there spells the name to edit.
|
|
5062
|
+
if (options.renameTo && langTraits(planLang)?.macroInvocationSyntax === 'call' && !def.macroExpansion) {
|
|
5063
|
+
const { pastedNameSites } = require('./macro-expansion');
|
|
5064
|
+
const pasted = pastedNameSites(index, name);
|
|
5065
|
+
const taken = new Set([...changes, ...reviewItems].map(change => `${change.file}:${change.line}`));
|
|
5066
|
+
const push = (file, line, suggestion) => {
|
|
5067
|
+
const rel = path.relative(index.root, file);
|
|
5068
|
+
if (taken.has(`${rel}:${line}`)) return;
|
|
5069
|
+
taken.add(`${rel}:${line}`);
|
|
5070
|
+
reviewItems.push({
|
|
5071
|
+
file: rel,
|
|
5072
|
+
line,
|
|
5073
|
+
expression: (index.getLineContent(file, line) || '').trim(),
|
|
5074
|
+
suggestion,
|
|
5075
|
+
needsReview: true,
|
|
5076
|
+
templateDependency: true,
|
|
5077
|
+
reviewReason: 'macro-paste',
|
|
5078
|
+
editKind: 'macro-expansion',
|
|
5079
|
+
});
|
|
5080
|
+
};
|
|
5081
|
+
// A paste of parameters only (`a ## b`) can spell any name: its
|
|
5082
|
+
// invocations decide, not its definition.
|
|
5083
|
+
for (const macro of pasted.macros.filter(item =>
|
|
5084
|
+
item.chains.some(chain => chain.some(op => op.literal != null)))) {
|
|
5085
|
+
push(macro.symbol.file, macro.symbol.startLine,
|
|
5086
|
+
`Macro ${macro.symbol.name} pastes \`${macro.spelling}\`, which can spell "${name}" at its ` +
|
|
5087
|
+
'invocations; review the macro and its invocations');
|
|
5088
|
+
}
|
|
5089
|
+
for (const site of pasted.sites) {
|
|
5090
|
+
push(site.file, site.line,
|
|
5091
|
+
`Macro ${site.macro} pastes "${name}" at this invocation; change the macro arguments or ` +
|
|
5092
|
+
`definition so the expansion spells "${options.renameTo}"`);
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
|
|
5096
|
+
// A call site excluded inside a region the parser could not read (fix
|
|
5097
|
+
// #396: a decoration macro defined in another header broke the member
|
|
5098
|
+
// around it) was decided from a partial tree: listed for review, never
|
|
5099
|
+
// dropped silently.
|
|
5100
|
+
if (options.renameTo && planAccountParts?.excludedEntries?.length > 0) {
|
|
5101
|
+
const taken = new Set([...changes, ...reviewItems].map(change => `${change.file}:${change.line}`));
|
|
5102
|
+
const seen = new Set();
|
|
5103
|
+
for (const entry of planAccountParts.excludedEntries) {
|
|
5104
|
+
const regions = index.files.get(entry.file)?.parseErrorRegions;
|
|
5105
|
+
if (!Array.isArray(regions) || !regions.some(([start, end]) => start <= entry.line && entry.line <= end)) continue;
|
|
5106
|
+
const rel = path.relative(index.root, entry.file);
|
|
5107
|
+
const key = `${rel}:${entry.line}`;
|
|
5108
|
+
if (taken.has(key) || seen.has(key)) continue;
|
|
5109
|
+
seen.add(key);
|
|
5110
|
+
reviewItems.push({
|
|
5111
|
+
file: rel,
|
|
5112
|
+
line: entry.line,
|
|
5113
|
+
expression: (index.getLineContent(entry.file, entry.line) || '').trim(),
|
|
5114
|
+
suggestion: `This line sits in a region the parser could not fully read, where the call was ` +
|
|
5115
|
+
`excluded (${entry.reason}) from a partial tree; rename it too if it names this "${name}"`,
|
|
5116
|
+
needsReview: true,
|
|
5117
|
+
reviewReason: 'parse-recovery',
|
|
5118
|
+
editKind: 'call',
|
|
5119
|
+
});
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
// Documentation references the compiler resolves (fix #390): C# `cref`
|
|
5124
|
+
// attributes naming the renamed member are edited (CS1574 fails the
|
|
5125
|
+
// build once the member is gone); ones the lookup cannot settle are
|
|
5126
|
+
// listed for review, not left as comment text.
|
|
5127
|
+
if (options.renameTo && langTraits(planLang)?.docCrefReferences) {
|
|
5128
|
+
const renamedDefs = [];
|
|
5129
|
+
const sameNameDefs = index.symbols.get(name) || [];
|
|
5130
|
+
for (const change of changes) {
|
|
5131
|
+
if (change.editKind !== 'definition' || change.newExpression === undefined) continue;
|
|
5132
|
+
for (const candidate of sameNameDefs) {
|
|
5133
|
+
if ((candidate.relativePath || candidate.file) === change.file &&
|
|
5134
|
+
(candidate.nameLine || candidate.startLine) === change.line &&
|
|
5135
|
+
!renamedDefs.includes(candidate)) renamedDefs.push(candidate);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
if (!renamedDefs.includes(def)) renamedDefs.push(def);
|
|
5139
|
+
_docCrefMemberPass(index, name, options.renameTo, renamedDefs, changes, reviewItems);
|
|
5140
|
+
}
|
|
5141
|
+
|
|
5142
|
+
// Method groups (fix #395): C# simple names and `this.`/`base.`/type
|
|
5143
|
+
// accesses in value position that the language's lookup resolves to the
|
|
5144
|
+
// renamed method (delegates, events, LINQ arguments, `nameof`).
|
|
5145
|
+
if (options.renameTo && def.className && langTraits(planLang)?.methodGroupReferences &&
|
|
5146
|
+
(def.type === 'method' || def.memberType === 'method' || def.isMethod)) {
|
|
5147
|
+
const renamedDefs = [];
|
|
5148
|
+
const sameNameDefs = index.symbols.get(name) || [];
|
|
5149
|
+
for (const change of changes) {
|
|
5150
|
+
if (change.editKind !== 'definition' || change.newExpression === undefined) continue;
|
|
5151
|
+
for (const candidate of sameNameDefs) {
|
|
5152
|
+
if ((candidate.relativePath || candidate.file) === change.file &&
|
|
5153
|
+
(candidate.nameLine || candidate.startLine) === change.line &&
|
|
5154
|
+
!renamedDefs.includes(candidate)) renamedDefs.push(candidate);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
if (!renamedDefs.includes(def)) renamedDefs.push(def);
|
|
5158
|
+
_methodGroupPass(index, name, options.renameTo, renamedDefs, changes,
|
|
5159
|
+
planUsages || index.usages(name, { codeOnly: false, includeTests: true, internalEvidence: true }));
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
// Test functions the toolchain binds to a renamed declaration by NAME
|
|
5163
|
+
// (fix #383): Go examples `Example<Type>_<Method>` fail go vet once the
|
|
5164
|
+
// method is renamed alone. They are renamed with it (their definition
|
|
5165
|
+
// token), or listed for review when the binding is ambiguous.
|
|
5166
|
+
if (options.renameTo && langTraits(planLang)?.nameBoundTestFunctions) {
|
|
5167
|
+
const renamedDefs = [];
|
|
5168
|
+
const sameNameDefs = index.symbols.get(name) || [];
|
|
5169
|
+
for (const change of changes) {
|
|
5170
|
+
if (change.editKind !== 'definition' || change.newExpression === undefined) continue;
|
|
5171
|
+
for (const candidate of sameNameDefs) {
|
|
5172
|
+
if ((candidate.relativePath || candidate.file) === change.file &&
|
|
5173
|
+
(candidate.nameLine || candidate.startLine) === change.line &&
|
|
5174
|
+
!renamedDefs.includes(candidate)) renamedDefs.push(candidate);
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
const { nameBoundTestFunctions } = require('./test-name-bindings');
|
|
5178
|
+
for (const bound of nameBoundTestFunctions(index, renamedDefs, name, options.renameTo)) {
|
|
5179
|
+
const example = bound.def;
|
|
5180
|
+
const rel = example.relativePath || example.file;
|
|
5181
|
+
const line = example.nameLine || example.startLine;
|
|
5182
|
+
if (changes.some(change => change.file === rel && change.line === line)) continue;
|
|
5183
|
+
const edit = renameIdentifierTokens(index, example.file, line, example.name, bound.newName,
|
|
5184
|
+
null, null, { definitionNameOnly: true, definition: example });
|
|
5185
|
+
const reason = bound.binding === 'member'
|
|
5186
|
+
? `go vet binds ${example.name} to the renamed member`
|
|
5187
|
+
: `go vet binds ${example.name} to the renamed identifier`;
|
|
5188
|
+
if (bound.status === 'edit' && edit.renamed !== edit.source) {
|
|
5189
|
+
// Keep the file's changes in line order.
|
|
5190
|
+
const sameFile = changes.map((change, at) => ({ change, at }))
|
|
5191
|
+
.filter(item => item.change.file === rel);
|
|
5192
|
+
const after = sameFile.find(item => item.change.line > line);
|
|
5193
|
+
const at = after ? after.at
|
|
5194
|
+
: (sameFile.length > 0 ? sameFile[sameFile.length - 1].at + 1 : changes.length);
|
|
5195
|
+
changes.splice(at, 0, tagTokenEdit({
|
|
5196
|
+
file: rel,
|
|
5197
|
+
line,
|
|
5198
|
+
expression: edit.source,
|
|
5199
|
+
newExpression: edit.renamed,
|
|
5200
|
+
suggestion: `Rename example (${reason}): ${edit.renamed}`,
|
|
5201
|
+
editKind: 'example',
|
|
5202
|
+
}, edit));
|
|
5203
|
+
} else {
|
|
5204
|
+
reviewItems.push({
|
|
5205
|
+
file: rel,
|
|
5206
|
+
line,
|
|
5207
|
+
expression: edit.source,
|
|
5208
|
+
suggestion: bound.reason === 'example-referenced'
|
|
5209
|
+
? `${reason}; it is also referenced as a function, so rename it to ${bound.newName} with its references`
|
|
5210
|
+
: `${example.name} may name the renamed declaration or a same-name declaration of another imported package; rename it to ${bound.newName} if it names this one`,
|
|
5211
|
+
needsReview: true,
|
|
5212
|
+
testBinding: true,
|
|
5213
|
+
reviewReason: bound.reason,
|
|
5214
|
+
editKind: 'example',
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
// A member assignment onto a function-local or undeclared object is not
|
|
5221
|
+
// a project declaration (fix #359): its property name belongs to that
|
|
5222
|
+
// object's own type, so the edit is withheld and routed to review.
|
|
5223
|
+
if (options.renameTo) {
|
|
5224
|
+
const ownerScope = memberAssignmentOwnerScope(index, def);
|
|
5225
|
+
if (ownerScope === 'local' || ownerScope === 'external') {
|
|
5226
|
+
const defChange = changes.find(change =>
|
|
5227
|
+
change.file === definitionFile && change.line === definitionLine &&
|
|
5228
|
+
change.editKind === 'definition');
|
|
5229
|
+
if (defChange) {
|
|
5230
|
+
delete defChange.newExpression;
|
|
5231
|
+
defChange.needsReview = true;
|
|
5232
|
+
defChange.reviewReason = ownerScope === 'local'
|
|
5233
|
+
? 'member-assignment-local-object' : 'member-assignment-external-object';
|
|
5234
|
+
defChange.suggestion = `Property assignment on ${ownerScope === 'local'
|
|
5235
|
+
? 'function-local' : 'undeclared (runtime/global)'} object ` +
|
|
5236
|
+
`"${def.assignedReceiver}": "${name}" is that object's property, ` +
|
|
5237
|
+
'not a project declaration; rename only if you own its type';
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
|
|
5242
|
+
// Contract membership (fix #360). A member that fills the slot of a
|
|
5243
|
+
// contract outside the project cannot be renamed by editing project code:
|
|
5244
|
+
// a definite membership (external trait impl, marked override with no
|
|
5245
|
+
// project definer, language protocol or universal-supertype member)
|
|
5246
|
+
// withholds every edit; a possible one (the owner declares out-of-project
|
|
5247
|
+
// supertypes) keeps the edits and requires review at the definition and
|
|
5248
|
+
// the declaration a compiler would reject. Go satisfaction sites the
|
|
5249
|
+
// structural closure could not complete are listed where Go reports them.
|
|
5250
|
+
// Single-element annotations (fix #389): `@Marker(x)` sets the element
|
|
5251
|
+
// `value` without naming it. When the site's annotation type is the
|
|
5252
|
+
// renamed element's owner, the rename names the element there
|
|
5253
|
+
// (`@Marker(renamed = x)`); an undecided owner stays listed for review.
|
|
5254
|
+
if (options.renameTo && planUnverified.some(site => site.annotationShorthand?.certain)) {
|
|
5255
|
+
const pathMod = require('path');
|
|
5256
|
+
const kept = [];
|
|
5257
|
+
for (const site of planUnverified) {
|
|
5258
|
+
const shorthand = site.annotationShorthand;
|
|
5259
|
+
const abs = pathMod.isAbsolute(site.file) ? site.file : pathMod.join(index.root, site.file);
|
|
5260
|
+
const text = shorthand?.certain && Number.isInteger(shorthand.column)
|
|
5261
|
+
? (index._getFileLines(abs)[site.line - 1] || '') : '';
|
|
5262
|
+
if (!text || shorthand.column > text.length ||
|
|
5263
|
+
changes.some(change => change.file === site.file && change.line === site.line)) {
|
|
5264
|
+
kept.push(site);
|
|
5265
|
+
continue;
|
|
5266
|
+
}
|
|
5267
|
+
const renamed = `${text.slice(0, shorthand.column)}${options.renameTo} = ${text.slice(shorthand.column)}`;
|
|
5268
|
+
changes.push({
|
|
5269
|
+
file: site.file,
|
|
5270
|
+
line: site.line,
|
|
5271
|
+
expression: text.trim(),
|
|
5272
|
+
newExpression: renamed.trim(),
|
|
5273
|
+
suggestion: `Name the element in the single-element annotation: ${renamed.trim()}`,
|
|
5274
|
+
editKind: 'reference',
|
|
5275
|
+
});
|
|
5276
|
+
}
|
|
5277
|
+
planUnverified.length = 0;
|
|
5278
|
+
planUnverified.push(...kept);
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
// One source of truth for the plan's ACCOUNT (fix #376): a line the plan
|
|
5282
|
+
// edits as a call is confirmed and a line it lists as unverified is
|
|
5283
|
+
// unverified, whichever slot member's sweep contributed it; the pin's own
|
|
5284
|
+
// exclusions only keep the lines no listed site claims.
|
|
5285
|
+
if (options.renameTo && planAccountParts && planGroundSet &&
|
|
5286
|
+
(slotAccountSites.length > 0 || planUnverified.length > 0 || typePass)) {
|
|
5287
|
+
const pathMod = require('path');
|
|
5288
|
+
const absoluteOf = file => (pathMod.isAbsolute(file) ? file : pathMod.join(index.root, file));
|
|
5289
|
+
const editedCallLines = new Set(changes.filter(change =>
|
|
5290
|
+
change.editKind === 'call').map(change => `${change.file}:${change.line}`));
|
|
5291
|
+
const { buildAccount } = require('./account');
|
|
5292
|
+
// A type rename decides the call-shaped lines of its references too
|
|
5293
|
+
// (fix #386): edited ones are confirmed, the ones that provably name
|
|
5294
|
+
// another declaration are excluded, and the ones left for review are
|
|
5295
|
+
// listed with the unverified sites.
|
|
5296
|
+
const typeDecided = typePass?.decided || new Map();
|
|
5297
|
+
const relOf = file => pathMod.relative(index.root, file);
|
|
5298
|
+
const typeClaims = (keys) => {
|
|
5299
|
+
const confirmed = [];
|
|
5300
|
+
const excluded = [];
|
|
5301
|
+
for (const key of keys) {
|
|
5302
|
+
const hit = typeDecided.get(key);
|
|
5303
|
+
if (!hit) continue;
|
|
5304
|
+
const line = Number(key.slice(key.lastIndexOf(':') + 1));
|
|
5305
|
+
if (hit.edited) confirmed.push({ file: hit.abs, line });
|
|
5306
|
+
else excluded.push({ file: hit.abs, line, reason: 'other-declaration' });
|
|
5307
|
+
}
|
|
5308
|
+
return { confirmed, excluded };
|
|
5309
|
+
};
|
|
5310
|
+
const build = (extra) => buildAccount(index, name, {
|
|
5311
|
+
groundSet: planGroundSet,
|
|
5312
|
+
confirmedEntries: [
|
|
5313
|
+
...planAccountParts.confirmedEntries,
|
|
5314
|
+
...slotAccountSites.filter(site => editedCallLines.has(`${site.file}:${site.line}`))
|
|
5315
|
+
.map(site => ({ file: site.absoluteFile || absoluteOf(site.file), line: site.line })),
|
|
5316
|
+
...extra.confirmed,
|
|
5317
|
+
],
|
|
5318
|
+
unverifiedEntries: [
|
|
5319
|
+
...planAccountParts.unverifiedEntries.filter(entry =>
|
|
5320
|
+
!typeDecided.has(`${relOf(entry.file)}:${entry.line}`)),
|
|
5321
|
+
...planUnverified.map(site => ({ file: absoluteOf(site.file), line: site.line })),
|
|
5322
|
+
],
|
|
5323
|
+
excludedEntries: [...extra.excluded, ...planAccountParts.excludedEntries],
|
|
5324
|
+
});
|
|
5325
|
+
let extra = typeClaims((typePass?.removed || []).map(site => `${site.file}:${site.line}`));
|
|
5326
|
+
planAccount = build(extra);
|
|
5327
|
+
if (typePass && planAccount.callNotResolved?.length > 0) {
|
|
5328
|
+
// Call-shaped lines no engine candidate claimed: settled by the
|
|
5329
|
+
// type pass, or listed with the unverified sites for review.
|
|
5330
|
+
const keys = planAccount.callNotResolved.map(site =>
|
|
5331
|
+
`${site.relativePath || relOf(site.file)}:${site.line}`);
|
|
5332
|
+
const more = typeClaims(keys);
|
|
5333
|
+
let listedMore = false;
|
|
5334
|
+
for (const key of keys) {
|
|
5335
|
+
const review = typePass.reviewed.get(key);
|
|
5336
|
+
if (!review || planUnverified.some(site => `${site.file}:${site.line}` === key)) continue;
|
|
5337
|
+
const line = Number(key.slice(key.lastIndexOf(':') + 1));
|
|
5338
|
+
planUnverified.push({
|
|
5339
|
+
file: key.slice(0, key.lastIndexOf(':')),
|
|
5340
|
+
line,
|
|
5341
|
+
expression: (index._getFileLines(review.abs)[line - 1] || '').trim(),
|
|
5342
|
+
reason: `type-reference-${review.reason}`,
|
|
5343
|
+
});
|
|
5344
|
+
listedMore = true;
|
|
5345
|
+
}
|
|
5346
|
+
if (listedMore) {
|
|
5347
|
+
planUnverified.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
|
|
5348
|
+
}
|
|
5349
|
+
if (more.confirmed.length > 0 || more.excluded.length > 0 || listedMore) {
|
|
5350
|
+
extra = { confirmed: [...extra.confirmed, ...more.confirmed],
|
|
5351
|
+
excluded: [...extra.excluded, ...more.excluded] };
|
|
5352
|
+
planAccount = build(extra);
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
let contract = null;
|
|
5357
|
+
if (options.renameTo) {
|
|
5358
|
+
const findings = [];
|
|
5359
|
+
const seenMembers = new Set();
|
|
5360
|
+
for (const member of [def, ...contractRootDefs]) {
|
|
5361
|
+
const key = `${member.file}\0${member.startLine}`;
|
|
5362
|
+
if (seenMembers.has(key)) continue;
|
|
5363
|
+
seenMembers.add(key);
|
|
5364
|
+
const finding = externalContractOf(index, member) ||
|
|
5365
|
+
typedLiteralContract(index, member)?.external || null;
|
|
5366
|
+
if (finding) findings.push({ member, ...finding });
|
|
5367
|
+
}
|
|
5368
|
+
// Hierarchy slot the closure could not complete (fix #376): a member
|
|
5369
|
+
// marked as an override whose declaring ancestor was not located in
|
|
5370
|
+
// the project cannot be renamed alone (the compiler rejects the
|
|
5371
|
+
// marker, or dispatch silently changes); an ancestor spelling that
|
|
5372
|
+
// resolves to no single project class may declare the slot too.
|
|
5373
|
+
if (options.renameTo && def.className && !def.traitName &&
|
|
5374
|
+
!findings.some(finding => finding.member === def)) {
|
|
5375
|
+
const defSite = {
|
|
5376
|
+
file: def.file,
|
|
5377
|
+
relativePath: def.relativePath,
|
|
5378
|
+
line: def.nameLine || def.startLine,
|
|
5379
|
+
};
|
|
5380
|
+
const ownerDecl = hierarchySlot?.pinRef?.def;
|
|
5381
|
+
const classSites = ownerDecl && ownerDecl.file === def.file ? [{
|
|
5382
|
+
file: ownerDecl.file,
|
|
5383
|
+
relativePath: ownerDecl.relativePath || def.relativePath,
|
|
5384
|
+
line: ownerDecl.startLine,
|
|
5385
|
+
}] : [];
|
|
5386
|
+
const unresolvedDeclaring = (hierarchySlot?.unresolved || []).filter(owner =>
|
|
5387
|
+
(index.symbols.get(name) || []).some(member => member.className === owner));
|
|
5388
|
+
if (isOverrideMarked(def) && !(hierarchySlot?.ancestors.length > 0)) {
|
|
5389
|
+
findings.push({
|
|
5390
|
+
member: def,
|
|
5391
|
+
certainty: 'definite',
|
|
5392
|
+
via: unresolvedDeclaring.length > 0 ? unresolvedDeclaring
|
|
5393
|
+
: (hierarchySlot?.external.length > 0 ? hierarchySlot.external
|
|
5394
|
+
: [langTraits(planLang)?.universalSupertype || 'supertype']),
|
|
5395
|
+
reason: 'overrides-unlocated-member',
|
|
5396
|
+
sites: [defSite, ...classSites],
|
|
5397
|
+
});
|
|
5398
|
+
} else if (unresolvedDeclaring.length > 0) {
|
|
5399
|
+
findings.push({
|
|
5400
|
+
member: def,
|
|
5401
|
+
certainty: 'possible',
|
|
5402
|
+
via: unresolvedDeclaring,
|
|
5403
|
+
reason: 'ancestor-unresolved',
|
|
5404
|
+
sites: [defSite, ...classSites],
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
// A decorator that replaces the callable receives its name too
|
|
5409
|
+
// (fix #392): pytest fixtures, CLI commands, endpoints and registries
|
|
5410
|
+
// bind the function by it. Decorators proven to keep the name out
|
|
5411
|
+
// (standard ones, property accessors, project decorators that only
|
|
5412
|
+
// call/return/wrap it) need nothing more.
|
|
5413
|
+
{
|
|
5414
|
+
const { nameBindingDecorators } = require('./decorator-binding');
|
|
5415
|
+
const decoratorMemo = new Map();
|
|
5416
|
+
const decorated = new Set();
|
|
5417
|
+
// A pytest fixture is requested by name (fix #402): the
|
|
5418
|
+
// parameters pytest injects it into are renamed with it, each
|
|
5419
|
+
// with the uses that bind that parameter, and what pytest's
|
|
5420
|
+
// resolution leaves open is listed for review.
|
|
5421
|
+
const fixtureSites = options.renameTo && planLang === 'python'
|
|
5422
|
+
? require('./pytest-fixtures').fixtureParameterSites(index, def) : null;
|
|
5423
|
+
if (fixtureSites) {
|
|
5424
|
+
for (const site of fixtureSites.edits) {
|
|
5425
|
+
const rel = path.relative(index.root, site.file);
|
|
5426
|
+
const edit = renameIdentifierTokens(index, site.file, site.line, name,
|
|
5427
|
+
options.renameTo, site.byteColumns);
|
|
5428
|
+
if (edit.renamed === edit.source) continue;
|
|
5429
|
+
const existing = changes.find(change => change.file === rel && change.line === site.line);
|
|
5430
|
+
if (existing) {
|
|
5431
|
+
if (existing.newExpression !== undefined && !existing.needsReview) mergeTokenEdit(existing, edit);
|
|
5432
|
+
continue;
|
|
5433
|
+
}
|
|
5434
|
+
changes.push(tagTokenEdit({
|
|
5435
|
+
file: rel,
|
|
5436
|
+
line: site.line,
|
|
5437
|
+
expression: edit.source,
|
|
5438
|
+
suggestion: `Update fixture parameter: ${edit.renamed}`,
|
|
5439
|
+
newExpression: edit.renamed,
|
|
5440
|
+
editKind: 'reference',
|
|
5441
|
+
}, edit));
|
|
5442
|
+
}
|
|
5443
|
+
const reasonText = {
|
|
5444
|
+
'not-collected': 'pytest does not collect this module by default; if it is collected or imported into a test module, the parameter requests the fixture',
|
|
5445
|
+
'parametrized': 'the parameter is also a parametrize argument name',
|
|
5446
|
+
'plugin-fixture': 'the fixture is registered as a plugin and no project fixture of this name precedes it here',
|
|
5447
|
+
'class-fixtures': 'its class declares several fixtures of this name',
|
|
5448
|
+
'class-binding': 'its class binds the name',
|
|
5449
|
+
'class-bases': 'its class has bases whose fixtures are not resolved',
|
|
5450
|
+
'module-binding': 'the module binds the name otherwise than by one fixture definition',
|
|
5451
|
+
'module-import': 'the module imports the name from elsewhere',
|
|
5452
|
+
'module-namespace': 'the module may bind the name through a star import or a dynamically named fixture',
|
|
5453
|
+
'unreadable': 'a module on its fixture lookup path could not be read',
|
|
5454
|
+
};
|
|
5455
|
+
for (const site of fixtureSites.reviews) {
|
|
5456
|
+
const rel = path.relative(index.root, site.file);
|
|
5457
|
+
if (reviewItems.some(item => item.file === rel && item.line === site.line)) continue;
|
|
5458
|
+
reviewItems.push({
|
|
5459
|
+
file: rel,
|
|
5460
|
+
line: site.line,
|
|
5461
|
+
expression: (index.getLineContent(site.file, site.line) || '').trim(),
|
|
5462
|
+
suggestion: `pytest may inject fixture "${name}" here by name: ` +
|
|
5463
|
+
`${reasonText[site.reason] || site.reason}; rename the parameter if it requests this fixture`,
|
|
5464
|
+
needsReview: true,
|
|
5465
|
+
editKind: 'reference',
|
|
5466
|
+
});
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
for (const member of [def, ...contractRootDefs]) {
|
|
5470
|
+
const key = `${member.file}\0${member.startLine}`;
|
|
5471
|
+
if (decorated.has(key)) continue;
|
|
5472
|
+
decorated.add(key);
|
|
5473
|
+
let unproven = nameBindingDecorators(index, member, decoratorMemo);
|
|
5474
|
+
// The fixture pass above settled pytest's name binding.
|
|
5475
|
+
if (fixtureSites && !fixtureSites.dynamic && member === def) {
|
|
5476
|
+
const fileEntry = index.files.get(member.file);
|
|
5477
|
+
const vocab = langTraits(planLang)?.callableDecorators || {};
|
|
5478
|
+
const { resolveDecorator } = require('./async-producers');
|
|
5479
|
+
unproven = unproven.filter(text => resolveDecorator(text, fileEntry, vocab)?.name !== 'pytest.fixture');
|
|
5480
|
+
}
|
|
5481
|
+
if (unproven.length === 0) continue;
|
|
5482
|
+
// Point at each decorator line the declaration spans.
|
|
5483
|
+
const nameLine = member.nameLine || member.startLine;
|
|
5484
|
+
const lines = index._getFileLines(member.file) || [];
|
|
5485
|
+
const sites = [];
|
|
5486
|
+
for (let line = Math.max(1, member.startLine); line < nameLine; line++) {
|
|
5487
|
+
const text = (lines[line - 1] || '').trim();
|
|
5488
|
+
if (unproven.some(deco => text.startsWith(`@${deco.split('(')[0]}`))) {
|
|
5489
|
+
sites.push({ file: member.file, relativePath: member.relativePath, line });
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
if (sites.length === 0) {
|
|
5493
|
+
sites.push({ file: member.file, relativePath: member.relativePath, line: nameLine });
|
|
5494
|
+
}
|
|
5495
|
+
findings.push({
|
|
5496
|
+
member,
|
|
5497
|
+
certainty: 'possible',
|
|
5498
|
+
via: unproven.map(text => `@${text}`),
|
|
5499
|
+
reason: 'decorator-name-binding',
|
|
5500
|
+
sites,
|
|
5501
|
+
});
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
const definite = findings.find(finding => finding.certainty === 'definite');
|
|
5505
|
+
const defChange = changes.find(change =>
|
|
5506
|
+
change.file === definitionFile && change.line === definitionLine &&
|
|
5507
|
+
change.editKind === 'definition');
|
|
5508
|
+
const describe = finding => {
|
|
5509
|
+
const owner = finding.member.className || finding.member.registryContainer || finding.member.name;
|
|
5510
|
+
const via = finding.via.join(', ');
|
|
5511
|
+
switch (finding.reason) {
|
|
5512
|
+
case 'implements-external-trait':
|
|
5513
|
+
return `${owner}.${name} implements external trait ${via}; the trait fixes the name`;
|
|
5514
|
+
case 'language-protocol-member':
|
|
5515
|
+
return `${name} is a ${via} protocol member dispatched by name`;
|
|
5516
|
+
case 'possible-protocol-member':
|
|
5517
|
+
return `${owner} derives from ${via} (outside the project), which may carry a runtime protocol that uses ${name} by name`;
|
|
5518
|
+
case 'overrides-universal-supertype':
|
|
5519
|
+
return `${owner}.${name} overrides ${via}.${name}`;
|
|
5520
|
+
case 'overrides-external-member':
|
|
5521
|
+
return `${owner}.${name} overrides a member of ${via} (outside the project)`;
|
|
5522
|
+
case 'overrides-unlocated-member':
|
|
5523
|
+
return `${owner}.${name} is marked as an override, but the member it overrides (via ${via}) was not located in the project`;
|
|
5524
|
+
case 'ancestor-unresolved':
|
|
5525
|
+
return `${owner} extends ${via}, which does not resolve to one project class; it may declare ${name} too`;
|
|
5526
|
+
case 'implements-cfg-external-trait':
|
|
5527
|
+
return `${owner}.${name} implements ${via}, which is an external trait under some build configurations; the external trait fixes the name`;
|
|
5528
|
+
case 'compiler-protocol-type':
|
|
5529
|
+
return `${via} is a type the compiler references by its full name for a language feature the project uses`;
|
|
5530
|
+
case 'macro-generated-name':
|
|
5531
|
+
return `${name} is the callable the ${via} invocation defines; its name is generated by the macro, not written in the source`;
|
|
5532
|
+
case 'macro-invocation':
|
|
5533
|
+
return `${name} is a function-like macro (${via}) in effect where it is declared; this declaration is an invocation of the macro, not a function`;
|
|
5534
|
+
case 'standard-namespace-declaration':
|
|
5535
|
+
return `${name} is declared in the standard-library namespace ${via}; where the platform declares it, references bind the platform's`;
|
|
5536
|
+
case 'typed-literal-external-member':
|
|
5537
|
+
return `${owner}.${name} is a member of an object typed ${via} (outside the project)`;
|
|
5538
|
+
case 'decorator-name-binding':
|
|
5539
|
+
return `${via} receives ${name} and may bind it by its name (fixtures, commands, endpoints, registries); check what uses that name`;
|
|
5540
|
+
default:
|
|
5541
|
+
return `${owner} declares ${via} (outside the project); ${name} may implement or override a member of it`;
|
|
5542
|
+
}
|
|
5543
|
+
};
|
|
5544
|
+
const pushContractItem = (site, suggestion, reason) => {
|
|
5545
|
+
const rel = site.relativePath || site.file;
|
|
5546
|
+
if (reviewItems.some(item => item.file === rel && item.line === site.line &&
|
|
5547
|
+
item.contractDependency)) return;
|
|
5548
|
+
reviewItems.push({
|
|
5549
|
+
file: rel,
|
|
5550
|
+
line: site.line,
|
|
5551
|
+
expression: (index.getLineContent(site.file, site.line) || '').trim(),
|
|
5552
|
+
suggestion,
|
|
5553
|
+
needsReview: true,
|
|
5554
|
+
contractDependency: true,
|
|
5555
|
+
reviewReason: reason,
|
|
5556
|
+
editKind: 'contract',
|
|
5557
|
+
});
|
|
5558
|
+
};
|
|
5559
|
+
if (definite) {
|
|
5560
|
+
for (const change of changes) {
|
|
5561
|
+
if (change.newExpression === undefined) continue;
|
|
5562
|
+
delete change.newExpression;
|
|
5563
|
+
change.needsReview = true;
|
|
5564
|
+
change.reviewReason = change.reviewReason || 'contract-blocked';
|
|
5565
|
+
}
|
|
5566
|
+
if (defChange) {
|
|
5567
|
+
defChange.needsReview = true;
|
|
5568
|
+
defChange.reviewReason = definite.reason;
|
|
5569
|
+
defChange.suggestion = `Rename blocked: ${describe(definite)}. ` +
|
|
5570
|
+
(definite.reason === 'macro-generated-name'
|
|
5571
|
+
? 'Change the invocation that generates it instead'
|
|
5572
|
+
: definite.reason === 'macro-invocation' ? 'Rename the macro instead'
|
|
5573
|
+
: 'Renaming it here breaks the implementation; change the contract, not this member');
|
|
5574
|
+
}
|
|
5575
|
+
} else if (findings.length > 0 && defChange) {
|
|
5576
|
+
defChange.needsReview = true;
|
|
5577
|
+
defChange.reviewReason = findings[0].reason;
|
|
5578
|
+
defChange.suggestion = `${defChange.suggestion}. Review: ${describe(findings[0])}`;
|
|
5579
|
+
}
|
|
5580
|
+
for (const finding of findings) {
|
|
5581
|
+
for (const site of finding.sites) {
|
|
5582
|
+
const siteRel = site.relativePath || site.file;
|
|
5583
|
+
if (siteRel === definitionFile && site.line === definitionLine) continue;
|
|
5584
|
+
pushContractItem(site, describe(finding), finding.reason);
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
for (const review of goContractReviews) {
|
|
5588
|
+
pushContractItem(review, review.message, review.reason);
|
|
5589
|
+
}
|
|
5590
|
+
if (goContractReviews.length > 0 && defChange && !definite) {
|
|
5591
|
+
defChange.needsReview = true;
|
|
5592
|
+
defChange.reviewReason = defChange.reviewReason || 'contract-incomplete';
|
|
5593
|
+
}
|
|
5594
|
+
if (findings.length > 0 || goContractReviews.length > 0) {
|
|
5595
|
+
contract = {
|
|
5596
|
+
blocked: !!definite,
|
|
5597
|
+
external: findings.map(finding => ({
|
|
5598
|
+
member: `${finding.member.relativePath || finding.member.file}:` +
|
|
5599
|
+
`${finding.member.nameLine || finding.member.startLine}`,
|
|
5600
|
+
certainty: finding.certainty,
|
|
5601
|
+
reason: finding.reason,
|
|
5602
|
+
via: finding.via,
|
|
5603
|
+
})),
|
|
5604
|
+
incomplete: goContractReviews.map(review => ({
|
|
5605
|
+
file: review.relativePath,
|
|
5606
|
+
line: review.line,
|
|
5607
|
+
reason: review.reason,
|
|
5608
|
+
})),
|
|
5609
|
+
};
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
|
|
3314
5613
|
const changeSummary = {
|
|
3315
5614
|
// editKind is a partition: a public declaration that is also an
|
|
3316
5615
|
// export remains one definition edit, never two summary buckets.
|
|
@@ -3320,7 +5619,11 @@ function plan(index, name, options = {}) {
|
|
|
3320
5619
|
imports: changes.filter(change => change.editKind === 'import').length,
|
|
3321
5620
|
exports: changes.filter(change => change.editKind === 'export').length,
|
|
3322
5621
|
references: changes.filter(change => change.editKind === 'reference').length,
|
|
3323
|
-
|
|
5622
|
+
examples: changes.filter(change => change.editKind === 'example').length,
|
|
5623
|
+
textReferences: reviewItems.filter(item =>
|
|
5624
|
+
!item.contractDependency && !item.templateDependency && !item.testBinding).length,
|
|
5625
|
+
contracts: reviewItems.filter(item => item.contractDependency).length,
|
|
5626
|
+
templates: reviewItems.filter(item => item.templateDependency).length,
|
|
3324
5627
|
reviewRequired: changes.filter(change => change.needsReview).length +
|
|
3325
5628
|
reviewItems.length,
|
|
3326
5629
|
};
|
|
@@ -3344,11 +5647,13 @@ function plan(index, name, options = {}) {
|
|
|
3344
5647
|
},
|
|
3345
5648
|
totalChanges: changes.length,
|
|
3346
5649
|
filesAffected: new Set([...changes, ...reviewItems].map(c => c.file)).size,
|
|
5650
|
+
...(fileRenames.length > 0 && { fileRenames }),
|
|
3347
5651
|
changeSummary,
|
|
3348
5652
|
changes,
|
|
3349
5653
|
reviewItems,
|
|
3350
5654
|
totalReviewItems: reviewItems.length,
|
|
3351
5655
|
...(unchangedSites > 0 && { unchangedSites }),
|
|
5656
|
+
...(contract && { contract }),
|
|
3352
5657
|
// v4 tiered contract: sites that MAY also need this change but lack
|
|
3353
5658
|
// binding/receiver evidence — review manually before refactoring.
|
|
3354
5659
|
unverifiedCount: planUnverified.length,
|
|
@@ -3364,7 +5669,10 @@ function plan(index, name, options = {}) {
|
|
|
3364
5669
|
}),
|
|
3365
5670
|
...(resolved.warnings.length > 0 && { warnings: resolved.warnings }),
|
|
3366
5671
|
};
|
|
3367
|
-
} finally {
|
|
5672
|
+
} finally {
|
|
5673
|
+
if (ownsTreeCache) clearTreeCache(index);
|
|
5674
|
+
index._endOp();
|
|
5675
|
+
}
|
|
3368
5676
|
}
|
|
3369
5677
|
|
|
3370
5678
|
/**
|
|
@@ -3441,4 +5749,384 @@ function analyzeCallSiteAST(index, filePath, lineNum, funcName) {
|
|
|
3441
5749
|
return result;
|
|
3442
5750
|
}
|
|
3443
5751
|
|
|
3444
|
-
|
|
5752
|
+
/**
|
|
5753
|
+
* C# XML documentation `cref` references to a renamed MEMBER (fix #390).
|
|
5754
|
+
* Each `cref` spelling of the name is resolved the way the compiler resolves
|
|
5755
|
+
* it: unqualified in the scope of the documented declaration (its type, the
|
|
5756
|
+
* type's ancestors, then enclosing types), qualified through the named type.
|
|
5757
|
+
* A cref reaching a renamed member is edited; one reaching another member,
|
|
5758
|
+
* or naming a type or namespace, stays a comment-text item; one the lookup
|
|
5759
|
+
* cannot settle (an unresolved type, part of an overload group without a
|
|
5760
|
+
* parameter list) is a review item.
|
|
5761
|
+
*/
|
|
5762
|
+
function _docCrefMemberPass(index, name, renameTo, renamedDefs, changes, reviewItems) {
|
|
5763
|
+
const pathMod = require('path');
|
|
5764
|
+
const { ownerRefOf, resolveClassRef, findDeclaringClass } = require('./class-identity');
|
|
5765
|
+
const renamed = new Set(renamedDefs);
|
|
5766
|
+
const members = (index.symbols.get(name) || []).filter(d => d.className &&
|
|
5767
|
+
!IDENTITY_TYPE_KINDS_FOR_CREF.has(d.type));
|
|
5768
|
+
if (members.length === 0) return;
|
|
5769
|
+
const classRefOf = (fileEntry, cls) => {
|
|
5770
|
+
const entry = classDefsNamed(index, cls.name).entries.find(e => e.def === cls);
|
|
5771
|
+
return entry ? { name: cls.name, key: entry.key, def: cls } : null;
|
|
5772
|
+
};
|
|
5773
|
+
const scopeOf = (abs, fileEntry, line) => {
|
|
5774
|
+
// The documented declaration: the first one after the comment.
|
|
5775
|
+
let next = null;
|
|
5776
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
5777
|
+
if (symbol.startLine <= line) continue;
|
|
5778
|
+
if (!next || symbol.startLine < next.startLine) next = symbol;
|
|
5779
|
+
}
|
|
5780
|
+
if (!next) return [];
|
|
5781
|
+
const chain = [];
|
|
5782
|
+
let ref = IDENTITY_TYPE_KINDS_FOR_CREF.has(next.type)
|
|
5783
|
+
? classRefOf(fileEntry, next) : ownerRefOf(index, next);
|
|
5784
|
+
const seen = new Set();
|
|
5785
|
+
while (ref && !seen.has(ref.key || ref.name)) {
|
|
5786
|
+
seen.add(ref.key || ref.name);
|
|
5787
|
+
chain.push(ref);
|
|
5788
|
+
const outer = ref.def?.enclosingType;
|
|
5789
|
+
ref = outer ? resolveClassRef(index, outer, abs, { namespace: ref.def.namespace || null }) : null;
|
|
5790
|
+
}
|
|
5791
|
+
return chain;
|
|
5792
|
+
};
|
|
5793
|
+
const paramKey = text => String(text || '').replace(/\b(?:ref|out|in|params|this)\b/g, '')
|
|
5794
|
+
.replace(/[{<].*$/s, '').replace(/\s+/g, '').split('.').pop();
|
|
5795
|
+
// 'edit' | 'skip' | 'review' for a member found through `declaring`.
|
|
5796
|
+
const verdictFor = (declaring, paramsText) => {
|
|
5797
|
+
if (!declaring) return 'skip';
|
|
5798
|
+
if (declaring.uncertain) return 'review';
|
|
5799
|
+
const found = declaring.members;
|
|
5800
|
+
const hit = found.filter(member => renamed.has(member));
|
|
5801
|
+
if (hit.length === 0) return 'skip';
|
|
5802
|
+
if (hit.length === found.length) return 'edit';
|
|
5803
|
+
if (paramsText == null) return 'review';
|
|
5804
|
+
const wanted = paramsText.split(',').map(paramKey).filter(Boolean);
|
|
5805
|
+
const selected = found.filter(member => {
|
|
5806
|
+
const params = (member.paramsStructured || []).map(p => paramKey(p?.type));
|
|
5807
|
+
return params.length === wanted.length && params.every((p, i) => p === wanted[i]);
|
|
5808
|
+
});
|
|
5809
|
+
if (selected.length !== 1) return 'review';
|
|
5810
|
+
return renamed.has(selected[0]) ? 'edit' : 'skip';
|
|
5811
|
+
};
|
|
5812
|
+
const crefRe = /\bcref\s*=\s*"([^"]*)"/g;
|
|
5813
|
+
const idRe = new RegExp(`(^|[^A-Za-z0-9_])(${escapeRegExp(name)})(?![A-Za-z0-9_])`, 'g');
|
|
5814
|
+
for (let i = reviewItems.length - 1; i >= 0; i--) {
|
|
5815
|
+
const item = reviewItems[i];
|
|
5816
|
+
if (!item.textDependency) continue;
|
|
5817
|
+
const abs = pathMod.join(index.root, item.file);
|
|
5818
|
+
const fileEntry = index.files.get(abs);
|
|
5819
|
+
if (!fileEntry || !langTraits(fileEntry.language)?.docCrefReferences) continue;
|
|
5820
|
+
const text = index._getFileLines(abs)[item.line - 1] || '';
|
|
5821
|
+
if (!/^\s*\/\/\//.test(text)) continue;
|
|
5822
|
+
const edits = [];
|
|
5823
|
+
let review = false;
|
|
5824
|
+
let scope = null;
|
|
5825
|
+
crefRe.lastIndex = 0;
|
|
5826
|
+
let match;
|
|
5827
|
+
while ((match = crefRe.exec(text)) !== null) {
|
|
5828
|
+
const value = match[1];
|
|
5829
|
+
const valueStart = match.index + match[0].indexOf('"') + 1;
|
|
5830
|
+
const body = value.replace(/^[A-Za-z]:/, ' ');
|
|
5831
|
+
idRe.lastIndex = 0;
|
|
5832
|
+
let id;
|
|
5833
|
+
while ((id = idRe.exec(body)) !== null) {
|
|
5834
|
+
const start = id.index + id[1].length;
|
|
5835
|
+
const after = body.slice(start + name.length);
|
|
5836
|
+
// A qualifier segment (`Name.Member`) or generic type name.
|
|
5837
|
+
if (/^\s*[.{<]/.test(after)) continue;
|
|
5838
|
+
const paramsMatch = /^\s*\(([^)]*)\)/.exec(after);
|
|
5839
|
+
const paramsText = paramsMatch ? paramsMatch[1] : null;
|
|
5840
|
+
const qualifierText = body.slice(0, start).replace(/\.\s*$/, '');
|
|
5841
|
+
const qualified = /\.\s*$/.test(body.slice(0, start));
|
|
5842
|
+
let verdict;
|
|
5843
|
+
if (qualified) {
|
|
5844
|
+
const segment = qualifierText.split('.').pop().trim();
|
|
5845
|
+
const generic = /\{([^}]*)\}\s*$/.exec(segment);
|
|
5846
|
+
const typeName = segment.replace(/\{.*$/, '').trim();
|
|
5847
|
+
const arity = generic ? generic[1].split(',').length : 0;
|
|
5848
|
+
const traits = langTraits(fileEntry.language);
|
|
5849
|
+
const ref = resolveClassRef(index, typeName, abs, {
|
|
5850
|
+
...(traits?.genericArityIsIdentity && { arity }),
|
|
5851
|
+
});
|
|
5852
|
+
if (ref.external) verdict = 'skip';
|
|
5853
|
+
else if (!ref.key) verdict = 'review';
|
|
5854
|
+
else verdict = verdictFor(findDeclaringClass(index, ref, members), paramsText);
|
|
5855
|
+
} else {
|
|
5856
|
+
if (!scope) scope = scopeOf(abs, fileEntry, item.line);
|
|
5857
|
+
if (scope.length === 0) verdict = 'review';
|
|
5858
|
+
else {
|
|
5859
|
+
verdict = 'skip';
|
|
5860
|
+
for (const ref of scope) {
|
|
5861
|
+
const declaring = findDeclaringClass(index, ref, members);
|
|
5862
|
+
if (!declaring) continue;
|
|
5863
|
+
verdict = verdictFor(declaring, paramsText);
|
|
5864
|
+
break;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
if (verdict === 'edit') edits.push(valueStart + start);
|
|
5869
|
+
else if (verdict === 'review') review = true;
|
|
5870
|
+
}
|
|
5871
|
+
}
|
|
5872
|
+
if (review) {
|
|
5873
|
+
item.textDependency = false;
|
|
5874
|
+
item.reviewReason = 'doc-cref';
|
|
5875
|
+
item.suggestion = `Verify whether this documentation reference names the renamed ${name} ` +
|
|
5876
|
+
`and rename it to ${renameTo} if it does (the compiler resolves cref attributes)`;
|
|
5877
|
+
continue;
|
|
5878
|
+
}
|
|
5879
|
+
if (edits.length === 0) continue;
|
|
5880
|
+
let renamedText = text;
|
|
5881
|
+
for (const column of edits.sort((a, b) => b - a)) {
|
|
5882
|
+
renamedText = renamedText.slice(0, column) + renameTo + renamedText.slice(column + name.length);
|
|
5883
|
+
}
|
|
5884
|
+
reviewItems.splice(i, 1);
|
|
5885
|
+
const change = tagTokenEdit({
|
|
5886
|
+
file: item.file,
|
|
5887
|
+
line: item.line,
|
|
5888
|
+
expression: text.trim(),
|
|
5889
|
+
newExpression: renamedText.trim(),
|
|
5890
|
+
suggestion: `Update documentation reference: ${renamedText.trim()}`,
|
|
5891
|
+
editKind: 'reference',
|
|
5892
|
+
}, { columns: edits, rawSource: text, oldName: name, newName: renameTo, renamed: renamedText.trim() });
|
|
5893
|
+
// Keep the file's changes in line order.
|
|
5894
|
+
const sameFile = changes.map((other, at) => ({ other, at }))
|
|
5895
|
+
.filter(entry => entry.other.file === item.file);
|
|
5896
|
+
const after = sameFile.find(entry => entry.other.line > item.line);
|
|
5897
|
+
const at = after ? after.at
|
|
5898
|
+
: (sameFile.length > 0 ? sameFile[sameFile.length - 1].at + 1 : changes.length);
|
|
5899
|
+
changes.splice(at, 0, change);
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
const CSHARP_TYPE_DECLARATION_NODES = new Set(['class_declaration', 'struct_declaration',
|
|
5904
|
+
'record_declaration', 'record_struct_declaration', 'interface_declaration']);
|
|
5905
|
+
// Declarations whose `name` field binds a local of the member body.
|
|
5906
|
+
const CSHARP_LOCAL_DECLARATION_NODES = new Set(['parameter', 'catch_declaration', 'declaration_pattern',
|
|
5907
|
+
'declaration_expression', 'variable_declarator', 'local_function_statement', 'from_clause',
|
|
5908
|
+
'join_clause', 'query_continuation', 'recursive_pattern', 'var_pattern']);
|
|
5909
|
+
|
|
5910
|
+
const CSHARP_BLOCK_SCOPES = new Set(['block', 'switch_section', 'for_statement', 'using_statement',
|
|
5911
|
+
'fixed_statement', 'lambda_expression', 'anonymous_method_expression', 'arrow_expression_clause']);
|
|
5912
|
+
|
|
5913
|
+
/**
|
|
5914
|
+
* Where C# scopes a local named like the reference (spec 7.7): 'local' when
|
|
5915
|
+
* a parameter, local variable, local function, pattern, lambda, foreach,
|
|
5916
|
+
* catch or range variable of that name is in scope at `node`, 'none' when
|
|
5917
|
+
* no declaration of the member reaches it, 'unknown' for an unmodelled
|
|
5918
|
+
* declaration shape.
|
|
5919
|
+
*/
|
|
5920
|
+
function csharpLocalBinding(memberNode, node, name) {
|
|
5921
|
+
const contains = (scope, target) => scope && scope.startIndex <= target.startIndex &&
|
|
5922
|
+
target.endIndex <= scope.endIndex;
|
|
5923
|
+
const nearest = (from, kinds) => {
|
|
5924
|
+
for (let current = from.parent; current && !sameNode(current, memberNode); current = current.parent) {
|
|
5925
|
+
if (kinds.has(current.type)) return current;
|
|
5926
|
+
}
|
|
5927
|
+
return memberNode;
|
|
5928
|
+
};
|
|
5929
|
+
let verdict = 'none';
|
|
5930
|
+
const scopeOf = (declared) => {
|
|
5931
|
+
const parent = declared.parent;
|
|
5932
|
+
switch (parent.type) {
|
|
5933
|
+
case 'parameter': {
|
|
5934
|
+
const owner = parent.parent?.parent;
|
|
5935
|
+
return owner || memberNode;
|
|
5936
|
+
}
|
|
5937
|
+
case 'variable_declarator': case 'local_function_statement':
|
|
5938
|
+
case 'declaration_pattern': case 'declaration_expression': case 'single_variable_designation':
|
|
5939
|
+
case 'recursive_pattern': case 'var_pattern':
|
|
5940
|
+
return nearest(parent, CSHARP_BLOCK_SCOPES);
|
|
5941
|
+
case 'catch_declaration':
|
|
5942
|
+
return parent.parent;
|
|
5943
|
+
case 'foreach_statement':
|
|
5944
|
+
return parent;
|
|
5945
|
+
case 'from_clause': case 'join_clause': case 'let_clause': case 'query_continuation':
|
|
5946
|
+
return nearest(parent, new Set(['query_expression']));
|
|
5947
|
+
default:
|
|
5948
|
+
return null;
|
|
5949
|
+
}
|
|
5950
|
+
};
|
|
5951
|
+
const visit = current => {
|
|
5952
|
+
if (verdict === 'local') return;
|
|
5953
|
+
if ((current.type === 'implicit_parameter' || current.type === 'identifier') && current.text === name &&
|
|
5954
|
+
!sameNode(current, node)) {
|
|
5955
|
+
const parent = current.parent;
|
|
5956
|
+
let declares = current.type === 'implicit_parameter';
|
|
5957
|
+
if (!declares && parent) {
|
|
5958
|
+
declares = (CSHARP_LOCAL_DECLARATION_NODES.has(parent.type) &&
|
|
5959
|
+
sameNode(parent.childForFieldName('name'), current) && !sameNode(parent, memberNode)) ||
|
|
5960
|
+
(parent.type === 'foreach_statement' && sameNode(parent.childForFieldName('left'), current)) ||
|
|
5961
|
+
parent.type === 'single_variable_designation' ||
|
|
5962
|
+
(parent.type === 'let_clause' && sameNode(parent.namedChild(0), current));
|
|
5963
|
+
}
|
|
5964
|
+
if (declares) {
|
|
5965
|
+
const scope = current.type === 'implicit_parameter' ? parent : scopeOf(current);
|
|
5966
|
+
if (!scope) verdict = 'unknown';
|
|
5967
|
+
else if (contains(scope, node)) verdict = 'local';
|
|
5968
|
+
}
|
|
5969
|
+
return;
|
|
5970
|
+
}
|
|
5971
|
+
for (const child of current.namedChildren || []) visit(child);
|
|
5972
|
+
};
|
|
5973
|
+
for (const child of memberNode.namedChildren || []) visit(child);
|
|
5974
|
+
return verdict;
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
/**
|
|
5978
|
+
* Method groups in a C# member rename (fix #395): a simple name in value
|
|
5979
|
+
* position (`new(Build, 4)`, `Changed += OnChanged`, `.Select(Build)`,
|
|
5980
|
+
* `Func<int> f = Build`, `nameof(Build)`), a `this.`/`base.` access or a
|
|
5981
|
+
* type-qualified `Host.Build` names a method group, found by C# simple-name
|
|
5982
|
+
* lookup (spec 12.8.4): a local of that name in the enclosing member hides
|
|
5983
|
+
* it (review); then the enclosing class and its ancestry, then each
|
|
5984
|
+
* enclosing class outward. The first class that declares the name decides:
|
|
5985
|
+
* its members of that name are the renamed ones - an edit; the renamed ones
|
|
5986
|
+
* beside others (an overload chosen by the delegate's signature) or an
|
|
5987
|
+
* unresolved ancestry - a review item; none - not the pin.
|
|
5988
|
+
*/
|
|
5989
|
+
function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
5990
|
+
const pathMod = require('path');
|
|
5991
|
+
const { resolveClassRef, findDeclaringClass, parentRefsOf } = require('./class-identity');
|
|
5992
|
+
const renamed = new Set(renamedDefs);
|
|
5993
|
+
const members = (index.symbols.get(name) || []).filter(d => d.className &&
|
|
5994
|
+
!IDENTITY_TYPE_KINDS_FOR_CREF.has(d.type));
|
|
5995
|
+
if (!members.some(member => renamed.has(member))) return;
|
|
5996
|
+
const verdictOf = declaring => {
|
|
5997
|
+
if (!declaring) return null;
|
|
5998
|
+
if (declaring.uncertain) return 'review';
|
|
5999
|
+
const hit = declaring.members.filter(member => renamed.has(member));
|
|
6000
|
+
if (hit.length === 0) return 'skip';
|
|
6001
|
+
return hit.length === declaring.members.length ? 'edit' : 'review';
|
|
6002
|
+
};
|
|
6003
|
+
const classRefAt = (file, declNode) => {
|
|
6004
|
+
const nameNode = declNode.childForFieldName('name');
|
|
6005
|
+
if (!nameNode) return null;
|
|
6006
|
+
const line = nameNode.startPosition.row + 1;
|
|
6007
|
+
const entry = classDefsNamed(index, nameNode.text).entries.find(e => e.def.file === file &&
|
|
6008
|
+
(e.def.nameLine || e.def.startLine) <= line && e.def.endLine >= line &&
|
|
6009
|
+
e.def.startLine <= line);
|
|
6010
|
+
return entry ? { name: nameNode.text, key: entry.key, def: entry.def } : null;
|
|
6011
|
+
};
|
|
6012
|
+
const openAncestry = ref => parentRefsOf(index, ref).some(parent => parent.external || !parent.key);
|
|
6013
|
+
const verdictFor = (ref, tree) => {
|
|
6014
|
+
const row = ref.line - 1;
|
|
6015
|
+
const column = Number.isInteger(ref.column) ? ref.column : null;
|
|
6016
|
+
if (column == null || !tree) return 'review';
|
|
6017
|
+
const node = tree.rootNode.descendantForPosition({ row, column });
|
|
6018
|
+
if (!node || node.type !== 'identifier' || node.text !== name) return 'review';
|
|
6019
|
+
const types = [];
|
|
6020
|
+
let member = null;
|
|
6021
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
6022
|
+
if (CSHARP_TYPE_DECLARATION_NODES.has(current.type)) types.push(current);
|
|
6023
|
+
else if (types.length === 0 && current.parent?.type === 'declaration_list') member = current;
|
|
6024
|
+
}
|
|
6025
|
+
if (types.length === 0) return 'review';
|
|
6026
|
+
const receiver = ref.receiver || null;
|
|
6027
|
+
if (receiver && receiver !== 'this' && receiver !== 'base') {
|
|
6028
|
+
// `Host.Build`: a static method group of the named class.
|
|
6029
|
+
if (!/^[A-Z]/.test(receiver)) return null;
|
|
6030
|
+
const typeRef = resolveClassRef(index, receiver, ref.file, { line: ref.line });
|
|
6031
|
+
if (!typeRef?.key) return typeRef?.external ? null : 'review';
|
|
6032
|
+
return verdictOf(findDeclaringClass(index, typeRef, members)) === 'edit' ? 'edit'
|
|
6033
|
+
: verdictOf(findDeclaringClass(index, typeRef, members)) === 'review' ? 'review' : null;
|
|
6034
|
+
}
|
|
6035
|
+
if (!receiver && member) {
|
|
6036
|
+
const local = csharpLocalBinding(member, node, name);
|
|
6037
|
+
if (local === 'local') return null;
|
|
6038
|
+
if (local === 'unknown') return 'review';
|
|
6039
|
+
}
|
|
6040
|
+
for (let i = 0; i < types.length; i++) {
|
|
6041
|
+
const classRef = classRefAt(ref.file, types[i]);
|
|
6042
|
+
if (!classRef) return 'review';
|
|
6043
|
+
const declaring = findDeclaringClass(index, classRef, members,
|
|
6044
|
+
receiver === 'base' ? { parentsOnly: true } : {});
|
|
6045
|
+
const verdict = verdictOf(declaring);
|
|
6046
|
+
if (verdict === 'skip') return null;
|
|
6047
|
+
if (verdict) return verdict;
|
|
6048
|
+
// `this.`/`base.` name the innermost class only; a simple name
|
|
6049
|
+
// continues outward unless an unmodelled base could declare it.
|
|
6050
|
+
if (receiver || openAncestry(classRef)) return receiver ? null : 'review';
|
|
6051
|
+
}
|
|
6052
|
+
return null;
|
|
6053
|
+
};
|
|
6054
|
+
const trees = new Map();
|
|
6055
|
+
const treeOf = file => {
|
|
6056
|
+
if (trees.has(file)) return trees.get(file);
|
|
6057
|
+
let tree;
|
|
6058
|
+
try {
|
|
6059
|
+
const content = index._readFile(file);
|
|
6060
|
+
tree = index._getParsedTree?.(file, content, 'csharp') || safeParse(getParser('csharp'), content);
|
|
6061
|
+
} catch { tree = null; }
|
|
6062
|
+
trees.set(file, tree);
|
|
6063
|
+
return tree;
|
|
6064
|
+
};
|
|
6065
|
+
// Only simple names and `this.`/`base.`/type-qualified accesses can name
|
|
6066
|
+
// the method group; other member accesses are never parsed for.
|
|
6067
|
+
const candidateRecord = record => record.usageType === 'reference' && !record.isDefinition &&
|
|
6068
|
+
(!record.receiver || record.receiver === 'this' || record.receiver === 'base' ||
|
|
6069
|
+
/^[A-Z]/.test(String(record.receiver)));
|
|
6070
|
+
for (const first of usages) {
|
|
6071
|
+
if (first.usageType !== 'reference' || first.isDefinition) continue;
|
|
6072
|
+
const abs = first.file;
|
|
6073
|
+
if (!langTraits(index.files.get(abs)?.language)?.methodGroupReferences) continue;
|
|
6074
|
+
const records = [first, ...(first.sameLine || [])].filter(candidateRecord);
|
|
6075
|
+
if (records.length === 0) continue;
|
|
6076
|
+
const rel = first.relativePath || pathMod.relative(index.root, abs);
|
|
6077
|
+
const edits = [];
|
|
6078
|
+
let review = false;
|
|
6079
|
+
for (const record of records) {
|
|
6080
|
+
const verdict = verdictFor({ ...record, file: abs }, treeOf(abs));
|
|
6081
|
+
if (verdict === 'edit') edits.push(record.column);
|
|
6082
|
+
else if (verdict === 'review') review = true;
|
|
6083
|
+
}
|
|
6084
|
+
if (edits.length === 0 && !review) continue;
|
|
6085
|
+
const existing = changes.find(change => change.file === rel && change.line === first.line);
|
|
6086
|
+
if (existing) {
|
|
6087
|
+
if (edits.length > 0 && existing.newExpression !== undefined && !existing.needsReview) {
|
|
6088
|
+
mergeTokenEdit(existing, renameIdentifierTokens(index, abs, first.line, name, renameTo, edits));
|
|
6089
|
+
}
|
|
6090
|
+
if (review && !existing.needsReview) {
|
|
6091
|
+
existing.needsReview = true;
|
|
6092
|
+
existing.reviewReason = existing.reviewReason || 'method-group';
|
|
6093
|
+
}
|
|
6094
|
+
continue;
|
|
6095
|
+
}
|
|
6096
|
+
let change;
|
|
6097
|
+
if (edits.length > 0 && !review) {
|
|
6098
|
+
const edit = renameIdentifierTokens(index, abs, first.line, name, renameTo, edits);
|
|
6099
|
+
if (edit.renamed === edit.source) continue;
|
|
6100
|
+
change = tagTokenEdit({
|
|
6101
|
+
file: rel,
|
|
6102
|
+
line: first.line,
|
|
6103
|
+
expression: edit.source,
|
|
6104
|
+
suggestion: `Update method group reference: ${edit.renamed}`,
|
|
6105
|
+
newExpression: edit.renamed,
|
|
6106
|
+
editKind: 'reference',
|
|
6107
|
+
}, edit);
|
|
6108
|
+
} else {
|
|
6109
|
+
change = {
|
|
6110
|
+
file: rel,
|
|
6111
|
+
line: first.line,
|
|
6112
|
+
expression: (first.content || index.getLineContent(abs, first.line) || '').trim(),
|
|
6113
|
+
suggestion: `Verify whether this method group names the renamed ${name} ` +
|
|
6114
|
+
`(an overload chosen by the delegate type, or a local of that name) and rename it to ${renameTo} if it does`,
|
|
6115
|
+
needsReview: true,
|
|
6116
|
+
reviewReason: 'method-group',
|
|
6117
|
+
editKind: 'reference',
|
|
6118
|
+
};
|
|
6119
|
+
}
|
|
6120
|
+
// Keep the file's changes in line order.
|
|
6121
|
+
const sameFile = changes.map((other, at) => ({ other, at }))
|
|
6122
|
+
.filter(entry => entry.other.file === rel);
|
|
6123
|
+
const after = sameFile.find(entry => entry.other.line > first.line);
|
|
6124
|
+
const at = after ? after.at
|
|
6125
|
+
: (sameFile.length > 0 ? sameFile[sameFile.length - 1].at + 1 : changes.length);
|
|
6126
|
+
changes.splice(at, 0, change);
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
|
|
6130
|
+
const IDENTITY_TYPE_KINDS_FOR_CREF = new Set(['class', 'struct', 'interface', 'record', 'enum', 'trait', 'type']);
|
|
6131
|
+
|
|
6132
|
+
module.exports = { verify, plan, baseCounterpartOf, sitesReboundByChange, computePlanCallSites, analyzeCallSite, analyzeCallSiteAST, analyzeCallShape, classifyArgNode, findCallNode, clearTreeCache, identifyCallPatterns };
|