ucn 5.5.1 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/verify.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
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, isOverrideMarked, isMacroNamespaceDefinition
|
|
11
|
-
|
|
12
|
-
const {
|
|
10
|
+
const { escapeRegExp, codeUnitCompare, NON_CALLABLE_TYPES, isOverrideMarked, isMacroNamespaceDefinition,
|
|
11
|
+
isValueDefinition } = require('./shared');
|
|
12
|
+
const { classDefsNamed, ownerDefinitionOf, ownerRefOf, descendsFromTargets } = require('./class-identity');
|
|
13
|
+
const { isTypeRenamePin, referenceRole } = require('./type-references');
|
|
13
14
|
const { findAccessorReferences, isAccessorDefinition } = require('./accessors');
|
|
15
|
+
const { csharpLocalBinding } = require('../languages/csharp-scope');
|
|
14
16
|
const { validateCallMismatch, declarationIdentity } = require('./provenance');
|
|
15
17
|
const { UcnError } = require('./errors');
|
|
16
18
|
const { execFileSync, spawn } = require('child_process');
|
|
@@ -140,6 +142,66 @@ function selfMemberTokenColumns(index, filePath, lineNumber, name) {
|
|
|
140
142
|
return out;
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The object-pattern keys `name` on `lineNumber` of declarators that read an
|
|
147
|
+
* identifier (fix #423): `const { name } = src` / `const { name: local } =
|
|
148
|
+
* src`, defaults included. Returns [{ line, column, shorthand, local,
|
|
149
|
+
* source }]; nested patterns read members of members and are not listed.
|
|
150
|
+
*/
|
|
151
|
+
function objectPatternKeysAt(index, filePath, lineNumber, name) {
|
|
152
|
+
const path = require('path');
|
|
153
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.join(index.root, filePath);
|
|
154
|
+
const language = index.files.get(absolute)?.language;
|
|
155
|
+
const parser = language && getParser(language);
|
|
156
|
+
if (!parser) return [];
|
|
157
|
+
let content;
|
|
158
|
+
try { content = index._readFile(absolute); } catch { return []; }
|
|
159
|
+
const text = content.split('\n')[lineNumber - 1] || '';
|
|
160
|
+
if (!text.includes(name)) return [];
|
|
161
|
+
const tree = index._getParsedTree?.(absolute, content, language) || safeParse(parser, content);
|
|
162
|
+
if (!tree) return [];
|
|
163
|
+
const out = [];
|
|
164
|
+
for (let at = text.indexOf(name); at !== -1; at = text.indexOf(name, at + 1)) {
|
|
165
|
+
const node = tree.rootNode.descendantForPosition({ row: lineNumber - 1, column: at });
|
|
166
|
+
if (!node || node.text !== name || node.startPosition.column !== at) continue;
|
|
167
|
+
let property = node;
|
|
168
|
+
let shorthand = false;
|
|
169
|
+
let local = name;
|
|
170
|
+
if (node.type === 'shorthand_property_identifier_pattern') {
|
|
171
|
+
shorthand = true;
|
|
172
|
+
if (node.parent?.type === 'object_assignment_pattern') property = node.parent;
|
|
173
|
+
} else if (node.type === 'property_identifier' && node.parent?.type === 'pair_pattern' &&
|
|
174
|
+
sameNode(node.parent.childForFieldName('key'), node)) {
|
|
175
|
+
property = node.parent;
|
|
176
|
+
const value = property.childForFieldName('value');
|
|
177
|
+
const target = value?.type === 'assignment_pattern' ? value.childForFieldName('left') : value;
|
|
178
|
+
if (target?.type !== 'identifier') continue;
|
|
179
|
+
local = target.text;
|
|
180
|
+
} else continue;
|
|
181
|
+
const pattern = property.parent;
|
|
182
|
+
const declarator = pattern?.parent;
|
|
183
|
+
if (pattern?.type !== 'object_pattern' || declarator?.type !== 'variable_declarator' ||
|
|
184
|
+
!sameNode(declarator.childForFieldName('name'), pattern)) continue;
|
|
185
|
+
const source = declarator.childForFieldName('value');
|
|
186
|
+
if (source?.type !== 'identifier') continue;
|
|
187
|
+
out.push({ line: lineNumber, column: at, ...(shorthand && { shorthand: true }), local, source: source.text });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The embedded-script tree of a file's content (fix #419), the last few
|
|
193
|
+
// contents kept.
|
|
194
|
+
const EMBEDDED_TREES = new Map();
|
|
195
|
+
function embeddedTree(embedded, parser, content) {
|
|
196
|
+
if (EMBEDDED_TREES.has(content)) return EMBEDDED_TREES.get(content);
|
|
197
|
+
const scriptParser = parser && getParser(embedded.language);
|
|
198
|
+
const source = scriptParser ? embedded.source(content, parser) : null;
|
|
199
|
+
const tree = source ? safeParse(scriptParser, source) : null;
|
|
200
|
+
if (EMBEDDED_TREES.size >= 8) EMBEDDED_TREES.delete(EMBEDDED_TREES.keys().next().value);
|
|
201
|
+
EMBEDDED_TREES.set(content, tree);
|
|
202
|
+
return tree;
|
|
203
|
+
}
|
|
204
|
+
|
|
143
205
|
/** Replace only AST identifier tokens on one source line. */
|
|
144
206
|
function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
145
207
|
preferredByteColumns = null, expectedCallCount = null, tokenOptions = {}) {
|
|
@@ -150,13 +212,33 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
150
212
|
let byteColumns = Array.isArray(preferredByteColumns)
|
|
151
213
|
? preferredByteColumns.filter(Number.isInteger) : [];
|
|
152
214
|
const defNameByteColumns = [];
|
|
215
|
+
let syntaxDerived = false;
|
|
216
|
+
let declarationSyntax = null;
|
|
217
|
+
if (byteColumns.length === 0 && tokenOptions.definitionNameOnly) {
|
|
218
|
+
declarationSyntax = require('./rename-token-syntax').declarationTokenSyntax(index, absolute,
|
|
219
|
+
index.files.get(absolute), content, lineNumber, oldName, tokenOptions.definition,
|
|
220
|
+
preferredByteColumns == null);
|
|
221
|
+
if (declarationSyntax?.columns) {
|
|
222
|
+
byteColumns = declarationSyntax.columns;
|
|
223
|
+
syntaxDerived = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
153
226
|
|
|
154
|
-
if (byteColumns.length === 0) {
|
|
227
|
+
if (byteColumns.length === 0 && !syntaxDerived) {
|
|
155
228
|
const language = index.files.get(absolute)?.language ||
|
|
156
229
|
detectLanguage(absolute, index.root);
|
|
157
230
|
const parser = language && getParser(language);
|
|
158
|
-
|
|
159
|
-
|
|
231
|
+
// A file whose code is embedded (HTML scripts) is read in the
|
|
232
|
+
// script's own syntax, rows and columns kept (fix #419).
|
|
233
|
+
const embedded = langTraits(language)?.embeddedSyntax;
|
|
234
|
+
let tree = embedded ? embeddedTree(embedded, parser, content) : parser &&
|
|
235
|
+
(index._getParsedTree?.(absolute, content, language) || safeParse(parser, content));
|
|
236
|
+
// A row another configuration view reads (fix #426: a C# `#else`
|
|
237
|
+
// branch of a file whose literal parse was damaged) is blank in the
|
|
238
|
+
// primary view: its tokens are that view's.
|
|
239
|
+
const ownerView = tree && langTraits(language)?.queryBodyViews?.(tree)
|
|
240
|
+
?.find(view => view.owns?.has(lineNumber - 1));
|
|
241
|
+
if (ownerView?.tree) tree = ownerView.tree;
|
|
160
242
|
if (tree) {
|
|
161
243
|
const targetRow = lineNumber - 1;
|
|
162
244
|
const stack = [tree.rootNode];
|
|
@@ -231,6 +313,7 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
231
313
|
const columns = [...new Set(byteColumns
|
|
232
314
|
.map(column => tokenColumn(sourceLine, column, oldName))
|
|
233
315
|
.filter(Number.isInteger))].sort((a, b) => b - a);
|
|
316
|
+
if (!syntaxDerived) declarationSyntax?.save(columns);
|
|
234
317
|
if (expectedCallCount != null && columns.length !== expectedCallCount) {
|
|
235
318
|
return { source: sourceLine.trim(), renamed: sourceLine.trim(), count: 0 };
|
|
236
319
|
}
|
|
@@ -256,13 +339,24 @@ function callTokenColumn(index, filePath, range, name, line) {
|
|
|
256
339
|
if (!range || !filePath) return null;
|
|
257
340
|
const absolute = require('path').isAbsolute(filePath)
|
|
258
341
|
? filePath : require('path').join(index.root, filePath);
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
if (!parser) return null;
|
|
342
|
+
const entry = index.files.get(absolute);
|
|
343
|
+
const language = entry?.language;
|
|
262
344
|
let content;
|
|
263
345
|
try { content = index._readFile(absolute); } catch { return null; }
|
|
264
|
-
|
|
265
|
-
|
|
346
|
+
return require('./rename-token-syntax').callTokenSyntax(index, absolute, entry,
|
|
347
|
+
content, range, name, line, () => {
|
|
348
|
+
const parser = language && getParser(language);
|
|
349
|
+
let tree = parser &&
|
|
350
|
+
(index._getParsedTree?.(absolute, content, language) || safeParse(parser, content));
|
|
351
|
+
// A row another configuration view reads (fix #426).
|
|
352
|
+
const ownerView = tree && langTraits(language)?.queryBodyViews?.(tree)
|
|
353
|
+
?.find(view => view.owns?.has(line - 1));
|
|
354
|
+
if (ownerView?.tree) tree = ownerView.tree;
|
|
355
|
+
return tree ? callTokenColumnInTree(tree, content, range, name, line) : null;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function callTokenColumnInTree(tree, content, range, name, line) {
|
|
266
360
|
let node = tree.rootNode.descendantForIndex(range.start, Math.max(range.start, range.end - 1));
|
|
267
361
|
while (node && !(node.startIndex <= range.start && node.endIndex >= range.end)) node = node.parent;
|
|
268
362
|
if (!node) return null;
|
|
@@ -273,20 +367,25 @@ function callTokenColumn(index, filePath, range, name, line) {
|
|
|
273
367
|
node.startPosition.row === line - 1 && node.endPosition.row === line - 1) {
|
|
274
368
|
return node.startPosition.column + (range.start - node.startIndex);
|
|
275
369
|
}
|
|
276
|
-
const
|
|
370
|
+
const isPlainName = n => n && n.text === name && /identifier$/.test(n.type) &&
|
|
277
371
|
n.startPosition.row === line - 1;
|
|
278
|
-
|
|
372
|
+
// A generic callee head (`r.Get<int>(2)`, C++ `get<int>(1)`) names the
|
|
373
|
+
// call by its identifier, the type arguments aside (fix #426).
|
|
374
|
+
const isName = n => isPlainName(n) || (n && /generic_name$|^template_(?:function|method)$/.test(n.type) &&
|
|
375
|
+
isPlainName(n.childForFieldName?.('name') || n.namedChild(0)));
|
|
376
|
+
const nameColumn = n => (isPlainName(n) ? n : (n.childForFieldName?.('name') || n.namedChild(0))).startPosition.column;
|
|
377
|
+
if (isName(node)) return nameColumn(node);
|
|
279
378
|
// The callee part of the call node: its last same-name token outside
|
|
280
379
|
// the argument list names the call (`x.run(y.run())` -> the outer run).
|
|
281
380
|
const callee = node.childForFieldName?.('function') || node.childForFieldName?.('name') ||
|
|
282
381
|
node.childForFieldName?.('method') || node.childForFieldName?.('constructor') ||
|
|
283
382
|
node.childForFieldName?.('macro') || null;
|
|
284
383
|
if (!callee) return null;
|
|
285
|
-
if (isName(callee)) return callee
|
|
384
|
+
if (isName(callee)) return nameColumn(callee);
|
|
286
385
|
const named = ['property', 'attribute', 'field', 'name'];
|
|
287
386
|
for (const field of named) {
|
|
288
387
|
const part = callee.childForFieldName?.(field);
|
|
289
|
-
if (isName(part)) return part
|
|
388
|
+
if (isName(part)) return nameColumn(part);
|
|
290
389
|
}
|
|
291
390
|
return null;
|
|
292
391
|
}
|
|
@@ -304,20 +403,31 @@ function mergeTokenEdit(change, edit) {
|
|
|
304
403
|
prior.oldName !== edit.oldName || prior.newName !== edit.newName) return false;
|
|
305
404
|
const columns = [...new Set([...prior.columns, ...edit.columns])].sort((a, b) => b - a);
|
|
306
405
|
if (columns.length === prior.columns.length) return true;
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
}
|
|
406
|
+
const aliasColumns = [...new Set([...(prior.aliasColumns || []), ...(edit.aliasColumns || [])])];
|
|
407
|
+
const renamed = renameColumns(prior.rawSource, columns, prior.oldName, prior.newName, aliasColumns);
|
|
312
408
|
const previous = change.newExpression;
|
|
313
409
|
change.newExpression = renamed.trim();
|
|
314
410
|
if (typeof change.suggestion === 'string' && previous) {
|
|
315
411
|
change.suggestion = change.suggestion.replace(previous, change.newExpression);
|
|
316
412
|
}
|
|
317
|
-
tagTokenEdit(change, { ...prior, columns, renamed: change.newExpression
|
|
413
|
+
tagTokenEdit(change, { ...prior, columns, renamed: change.newExpression,
|
|
414
|
+
...(aliasColumns.length > 0 && { aliasColumns }) });
|
|
318
415
|
return true;
|
|
319
416
|
}
|
|
320
417
|
|
|
418
|
+
/** Replace the name at code-unit columns (descending), aliasing the
|
|
419
|
+
* import bindings among them (`name as newName`). */
|
|
420
|
+
function renameColumns(raw, columns, oldName, newName, aliasColumns = []) {
|
|
421
|
+
const aliased = new Set(aliasColumns);
|
|
422
|
+
let renamed = raw;
|
|
423
|
+
for (const column of [...columns].sort((a, b) => b - a)) {
|
|
424
|
+
if (renamed.slice(column, column + oldName.length) !== oldName) continue;
|
|
425
|
+
const replacement = aliased.has(column) ? `${oldName} as ${newName}` : newName;
|
|
426
|
+
renamed = renamed.slice(0, column) + replacement + renamed.slice(column + oldName.length);
|
|
427
|
+
}
|
|
428
|
+
return renamed;
|
|
429
|
+
}
|
|
430
|
+
|
|
321
431
|
/**
|
|
322
432
|
* Mark a change whose tokens a dedicated pass proved (a macro argument that
|
|
323
433
|
* declares the renamed class, an exactly resolved using-declaration): the
|
|
@@ -560,22 +670,47 @@ function _collectCallNodes(node, callTypes, targetRow, funcName, limit, out = []
|
|
|
560
670
|
// Children are in source order: binary-search the first one that can
|
|
561
671
|
// reach the target row instead of visiting every earlier sibling.
|
|
562
672
|
const count = node.childCount;
|
|
673
|
+
// A wide node (a file's top level, a class body) keeps its children's
|
|
674
|
+
// rows for the tree's lifetime: every call site of the file searches
|
|
675
|
+
// them (fix #436: two native reads per probe, per site).
|
|
676
|
+
const rows = count > 16 ? _childRowsOf(node) : null;
|
|
563
677
|
let lo = 0;
|
|
564
678
|
let hi = count;
|
|
565
679
|
while (lo < hi) {
|
|
566
680
|
const mid = (lo + hi) >> 1;
|
|
567
|
-
if (node.child(mid).endPosition.row < targetRow) lo = mid + 1;
|
|
681
|
+
if ((rows ? rows.ends[mid] : node.child(mid).endPosition.row) < targetRow) lo = mid + 1;
|
|
568
682
|
else hi = mid;
|
|
569
683
|
}
|
|
570
684
|
for (let i = lo; i < count; i++) {
|
|
685
|
+
if (rows && rows.starts[i] > targetRow) break;
|
|
571
686
|
const child = node.child(i);
|
|
572
|
-
if (child.startPosition.row > targetRow) break;
|
|
687
|
+
if (!rows && child.startPosition.row > targetRow) break;
|
|
573
688
|
_collectCallNodes(child, callTypes, targetRow, funcName, limit, out);
|
|
574
689
|
if (out.length >= limit) return out;
|
|
575
690
|
}
|
|
576
691
|
return out;
|
|
577
692
|
}
|
|
578
693
|
|
|
694
|
+
const CHILD_ROWS = new WeakMap(); // tree -> Map(node id -> { starts, ends })
|
|
695
|
+
function _childRowsOf(node) {
|
|
696
|
+
const tree = node.tree;
|
|
697
|
+
if (!tree) return null;
|
|
698
|
+
let byNode = CHILD_ROWS.get(tree);
|
|
699
|
+
if (!byNode) CHILD_ROWS.set(tree, byNode = new Map());
|
|
700
|
+
let rows = byNode.get(node.id);
|
|
701
|
+
if (!rows) {
|
|
702
|
+
const count = node.childCount;
|
|
703
|
+
rows = { starts: new Int32Array(count), ends: new Int32Array(count) };
|
|
704
|
+
for (let i = 0; i < count; i++) {
|
|
705
|
+
const child = node.child(i);
|
|
706
|
+
rows.starts[i] = child.startPosition.row;
|
|
707
|
+
rows.ends[i] = child.endPosition.row;
|
|
708
|
+
}
|
|
709
|
+
byNode.set(node.id, rows);
|
|
710
|
+
}
|
|
711
|
+
return rows;
|
|
712
|
+
}
|
|
713
|
+
|
|
579
714
|
/**
|
|
580
715
|
* Clear the AST tree cache (call after batch operations)
|
|
581
716
|
* @param {object} index - ProjectIndex instance
|
|
@@ -668,6 +803,34 @@ function computeModifierTokens(def) {
|
|
|
668
803
|
return tokens;
|
|
669
804
|
}
|
|
670
805
|
|
|
806
|
+
/**
|
|
807
|
+
* A parameter list as source text. Python binding-position markers (fix
|
|
808
|
+
* #281): the bare `*` before the first keyword-only param (unless `*args`
|
|
809
|
+
* already plays that role) and the `/` after the last positional-only
|
|
810
|
+
* param, so the signature matches the source contract (fix #424: a changed
|
|
811
|
+
* list keeps them too).
|
|
812
|
+
*/
|
|
813
|
+
function formatTypedParamList(ps, paramTypes = {}) {
|
|
814
|
+
const lastPosOnly = ps.reduce(
|
|
815
|
+
(acc, p, i) => (p && p.positionalOnly ? i : acc), -1);
|
|
816
|
+
let starShown = false;
|
|
817
|
+
const parts = [];
|
|
818
|
+
ps.forEach((p, i) => {
|
|
819
|
+
// Apply paramTypes mapping when paramsStructured doesn't carry types
|
|
820
|
+
const merged = { ...p };
|
|
821
|
+
if (!merged.type && paramTypes?.[p.name]) merged.type = paramTypes[p.name];
|
|
822
|
+
if (p && p.rest && /^\*(?!\*)/.test(String(p.name))) starShown = true;
|
|
823
|
+
if (!starShown && p && p.keywordOnly) {
|
|
824
|
+
parts.push('*');
|
|
825
|
+
starShown = true;
|
|
826
|
+
}
|
|
827
|
+
const tok = formatTypedParam(merged);
|
|
828
|
+
if (tok) parts.push(tok);
|
|
829
|
+
if (i === lastPosOnly) parts.push('/');
|
|
830
|
+
});
|
|
831
|
+
return parts.join(', ');
|
|
832
|
+
}
|
|
833
|
+
|
|
671
834
|
/**
|
|
672
835
|
* Build a function signature string from a definition, using
|
|
673
836
|
* TS-correct param formatting (BUG-BV). Local to verify.js to avoid
|
|
@@ -686,29 +849,7 @@ function formatTypedSignature(def, overrides = {}) {
|
|
|
686
849
|
parts.push(name);
|
|
687
850
|
const ps = overrides.paramsStructured != null ? overrides.paramsStructured : def.paramsStructured;
|
|
688
851
|
if (Array.isArray(ps)) {
|
|
689
|
-
|
|
690
|
-
// Python binding-position markers (fix #281): re-render the bare `*`
|
|
691
|
-
// before the first keyword-only param (unless `*args` already plays
|
|
692
|
-
// that role) and the `/` after the last positional-only param, so the
|
|
693
|
-
// displayed signature matches the source contract.
|
|
694
|
-
const lastPosOnly = ps.reduce(
|
|
695
|
-
(acc, p, i) => (p && p.positionalOnly ? i : acc), -1);
|
|
696
|
-
let starShown = false;
|
|
697
|
-
const parts2 = [];
|
|
698
|
-
ps.forEach((p, i) => {
|
|
699
|
-
// Apply paramTypes mapping when paramsStructured doesn't carry types
|
|
700
|
-
const merged = { ...p };
|
|
701
|
-
if (!merged.type && paramTypes[p.name]) merged.type = paramTypes[p.name];
|
|
702
|
-
if (p && p.rest && /^\*(?!\*)/.test(String(p.name))) starShown = true;
|
|
703
|
-
if (!starShown && p && p.keywordOnly) {
|
|
704
|
-
parts2.push('*');
|
|
705
|
-
starShown = true;
|
|
706
|
-
}
|
|
707
|
-
const tok = formatTypedParam(merged);
|
|
708
|
-
if (tok) parts2.push(tok);
|
|
709
|
-
if (i === lastPosOnly) parts2.push('/');
|
|
710
|
-
});
|
|
711
|
-
parts.push(`(${parts2.join(', ')})`);
|
|
852
|
+
parts.push(`(${formatTypedParamList(ps, def.paramTypes)})`);
|
|
712
853
|
} else if (def.params !== undefined) {
|
|
713
854
|
parts.push(`(${def.params})`);
|
|
714
855
|
}
|
|
@@ -735,7 +876,7 @@ function formatTypedSignature(def, overrides = {}) {
|
|
|
735
876
|
*/
|
|
736
877
|
function extractArrowTypesFromVarDecl(index, def) {
|
|
737
878
|
if (!def || !def.file || !def.startLine) return null;
|
|
738
|
-
const lang =
|
|
879
|
+
const lang = index.languageOf(def.file);
|
|
739
880
|
if (lang !== 'typescript' && lang !== 'tsx') return null;
|
|
740
881
|
// Already have types — nothing to enrich.
|
|
741
882
|
const ps = def.paramsStructured;
|
|
@@ -853,8 +994,8 @@ function extractArrowTypesFromVarDecl(index, def) {
|
|
|
853
994
|
* `new Task(id, name)` against 0..0 — a false red on every parameterized
|
|
854
995
|
* constructor, in every language. Sources: indexed constructor members
|
|
855
996
|
* (JS/TS `constructor`, Python `__init__` — emitted with type
|
|
856
|
-
* 'constructor'), or a Java AST walk
|
|
857
|
-
*
|
|
997
|
+
* 'constructor'), or a Java AST walk for an implicit record constructor.
|
|
998
|
+
* Returns an array of paramsStructured lists —
|
|
858
999
|
* one per constructor overload — or null when the class declares none.
|
|
859
1000
|
* @param {object} index - ProjectIndex instance
|
|
860
1001
|
* @param {object} def - Resolved definition (any type; non-class returns null)
|
|
@@ -864,20 +1005,26 @@ function extractArrowTypesFromVarDecl(index, def) {
|
|
|
864
1005
|
function _constructorParamLists(index, def, lang) {
|
|
865
1006
|
if (!def || !def.file || !['class', 'struct', 'enum', 'record'].includes(def.type)) return null;
|
|
866
1007
|
const lists = [];
|
|
1008
|
+
const { isConstructor } = require('./constructor-calls');
|
|
1009
|
+
const seen = new Set();
|
|
1010
|
+
const append = d => {
|
|
1011
|
+
if (seen.has(d) || !isConstructor(d, index)) return;
|
|
1012
|
+
seen.add(d); lists.push(d.paramsStructured);
|
|
1013
|
+
};
|
|
867
1014
|
const endLine = def.endLine != null ? def.endLine : Infinity;
|
|
868
1015
|
const inRange = (d) => d.file === def.file &&
|
|
869
1016
|
d.startLine >= def.startLine && d.startLine <= endLine &&
|
|
870
1017
|
Array.isArray(d.paramsStructured);
|
|
871
1018
|
for (const ctorName of ['constructor', '__init__']) {
|
|
872
1019
|
for (const d of (index.symbols.get(ctorName) || [])) {
|
|
873
|
-
if (d.className === def.name && inRange(d))
|
|
1020
|
+
if (d.className === def.name && inRange(d)) append(d);
|
|
874
1021
|
}
|
|
875
1022
|
}
|
|
876
1023
|
// Java constructor members are named after the CLASS (enum-body
|
|
877
1024
|
// constructors carry paramsStructured since fix #230).
|
|
878
1025
|
for (const d of (index.symbols.get(def.name) || [])) {
|
|
879
1026
|
if (d.type === 'constructor' && d.className === def.name && inRange(d)) {
|
|
880
|
-
|
|
1027
|
+
append(d);
|
|
881
1028
|
}
|
|
882
1029
|
}
|
|
883
1030
|
if (lists.length > 0) return lists;
|
|
@@ -951,6 +1098,7 @@ function contractedCallerSweep(index, name, def, options = {}) {
|
|
|
951
1098
|
includeMethods: true,
|
|
952
1099
|
targetDefinitions: [def],
|
|
953
1100
|
collectAccount: true,
|
|
1101
|
+
...(options.constructionSites === false && { constructorProjection: false }),
|
|
954
1102
|
});
|
|
955
1103
|
|
|
956
1104
|
const confirmed = [];
|
|
@@ -1039,7 +1187,8 @@ function contractedCallerSweep(index, name, def, options = {}) {
|
|
|
1039
1187
|
// overloads.
|
|
1040
1188
|
|
|
1041
1189
|
// Exclusions whose verdict depends on the pin's declared parameters.
|
|
1042
|
-
const SIGNATURE_DEPENDENT_EXCLUSIONS = new Set(['arity-mismatch', 'overload-mismatch', 'generic-arity-mismatch'
|
|
1190
|
+
const SIGNATURE_DEPENDENT_EXCLUSIONS = new Set(['arity-mismatch', 'overload-mismatch', 'generic-arity-mismatch',
|
|
1191
|
+
'constructor-overload-other']);
|
|
1043
1192
|
|
|
1044
1193
|
/** Declared parameter shape (types, optional/default/rest, method type parameters); names ignored. */
|
|
1045
1194
|
function declaredSignatureKey(symbol) {
|
|
@@ -1251,10 +1400,14 @@ function sitesReboundByChange(index, name, def, rawCallers, confirmed, unverifie
|
|
|
1251
1400
|
if (dependent.length === 0 && unverified.length === 0) return [];
|
|
1252
1401
|
const baseChanges = typeof changes === 'function' ? changes() : changes;
|
|
1253
1402
|
if (!baseChanges || baseChanges.length === 0) return [];
|
|
1254
|
-
const
|
|
1403
|
+
const lineKey = site => `${site.file}\0${site.line}`;
|
|
1404
|
+
const siteKey = site => {
|
|
1405
|
+
const at = site.provenance?.facts?.site || site.callSite || site;
|
|
1406
|
+
return `${lineKey(site)}\0${at.start != null ? `start:${at.start}` : at.column != null ? `column:${at.column}` : ''}`;
|
|
1407
|
+
};
|
|
1255
1408
|
const confirmedKeys = new Set(confirmed.map(siteKey));
|
|
1256
1409
|
const unverifiedKeys = new Set(unverified.map(siteKey));
|
|
1257
|
-
const reasonAt = new Map(excluded.map(entry => [
|
|
1410
|
+
const reasonAt = new Map(excluded.map(entry => [lineKey(entry), entry.reason]));
|
|
1258
1411
|
const before = callersBeforeChange(index, name, def, baseChanges);
|
|
1259
1412
|
const { getCachedCalls, _overloadDiscipline, _overloadApplicable } = require('./callers');
|
|
1260
1413
|
const pins = [...new Set([def, ...baseChanges.map(change => change.def)])];
|
|
@@ -1267,11 +1420,14 @@ function sitesReboundByChange(index, name, def, rawCallers, confirmed, unverifie
|
|
|
1267
1420
|
// declaration confirmed and the new one leaves unverified or
|
|
1268
1421
|
// excludes is checked against the new declaration here.
|
|
1269
1422
|
if (confirmedKeys.has(key) || seen.has(key)) continue;
|
|
1270
|
-
if (unverifiedKeys.has(key) ? boundBefore !== 'confirmed' : !reasonAt.has(
|
|
1423
|
+
if (unverifiedKeys.has(key) ? boundBefore !== 'confirmed' : !reasonAt.has(lineKey(site))) continue;
|
|
1271
1424
|
seen.add(key);
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1425
|
+
const { occurrenceIdentity } = require('./provenance-facts');
|
|
1426
|
+
const calls = (getCachedCalls(index, site.file) || []).filter(call => call.line === site.line &&
|
|
1427
|
+
(call.name === name || call.resolvedName === name || call.name === site.calledAs ||
|
|
1428
|
+
call.ownClass && call.ownClass.spelling === site.calledAs));
|
|
1429
|
+
const exact = calls.filter(call => siteKey({ ...occurrenceIdentity(site.file, call), file: site.file }) === key);
|
|
1430
|
+
const call = exact.length === 1 ? exact[0] : calls.length === 1 ? calls[0] : null;
|
|
1275
1431
|
const language = index.files.get(site.file)?.language;
|
|
1276
1432
|
let rebind = { kind: call ? 'count' : 'unread' };
|
|
1277
1433
|
if (call && langTraits(language)?.hasArityOverloads && call.argCount != null && !call.argSpread) {
|
|
@@ -1302,7 +1458,7 @@ function sitesReboundByChange(index, name, def, rawCallers, confirmed, unverifie
|
|
|
1302
1458
|
reason: 'bound-before-change',
|
|
1303
1459
|
...(prior?.reason && { priorReason: prior.reason }),
|
|
1304
1460
|
boundBefore,
|
|
1305
|
-
excludedAs: reasonAt.get(
|
|
1461
|
+
excludedAs: reasonAt.get(lineKey(site)) || null,
|
|
1306
1462
|
...(unverifiedKeys.has(key) && { stillUnverified: true }),
|
|
1307
1463
|
rebind,
|
|
1308
1464
|
});
|
|
@@ -1314,7 +1470,7 @@ function sitesReboundByChange(index, name, def, rawCallers, confirmed, unverifie
|
|
|
1314
1470
|
|
|
1315
1471
|
/** Map an unverified sweep entry to the public site shape (relative `file`). */
|
|
1316
1472
|
function unverifiedSiteShape(u) {
|
|
1317
|
-
|
|
1473
|
+
const shape = {
|
|
1318
1474
|
file: u.relativePath,
|
|
1319
1475
|
line: u.line,
|
|
1320
1476
|
expression: (u.content || '').trim(),
|
|
@@ -1333,11 +1489,40 @@ function unverifiedSiteShape(u) {
|
|
|
1333
1489
|
...(u.moduleAttribute && { moduleAttribute: true }),
|
|
1334
1490
|
...(u.annotationShorthand && { annotationShorthand: u.annotationShorthand }),
|
|
1335
1491
|
};
|
|
1492
|
+
// The site's own token (fix #422): a same-line edit of another token
|
|
1493
|
+
// leaves it undecided.
|
|
1494
|
+
const site = u.provenance?.facts?.site;
|
|
1495
|
+
const range = site && site.line === u.line && Number.isInteger(site.start) && Number.isInteger(site.end)
|
|
1496
|
+
? { start: site.start, end: site.end } : null;
|
|
1497
|
+
if (Number.isInteger(u.column) || range) {
|
|
1498
|
+
Object.defineProperty(shape, '_token', {
|
|
1499
|
+
value: { file: u.file, column: Number.isInteger(u.column) ? u.column : null, range },
|
|
1500
|
+
enumerable: false,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
return shape;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Does a planned change cover an unverified site (fix #422)? A change on
|
|
1508
|
+
* its line covers it unless the change edits known tokens and the site's
|
|
1509
|
+
* own token is another one.
|
|
1510
|
+
*/
|
|
1511
|
+
function siteCoveredByChanges(index, changes, site, name) {
|
|
1512
|
+
const change = changes.find(c => c.file === site.file && c.line === site.line);
|
|
1513
|
+
if (!change) return false;
|
|
1514
|
+
const columns = change._edit?.columns;
|
|
1515
|
+
const token = site._token;
|
|
1516
|
+
if (!Array.isArray(columns) || !token) return true;
|
|
1517
|
+
const column = token.column ?? (token.range
|
|
1518
|
+
? callTokenColumn(index, token.file, token.range, name, site.line) : null);
|
|
1519
|
+
return column == null || columns.includes(column);
|
|
1336
1520
|
}
|
|
1337
1521
|
|
|
1338
1522
|
/**
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1523
|
+
* Build callable sites through the same contracted sweep as verify.
|
|
1524
|
+
* Value renames resolve their tokens through the value-binding pass and
|
|
1525
|
+
* carry undecided call-shaped lines in their initial account.
|
|
1341
1526
|
*
|
|
1342
1527
|
* @param {object} index - ProjectIndex instance
|
|
1343
1528
|
* @param {string} name - Function name being refactored
|
|
@@ -1345,13 +1530,31 @@ function unverifiedSiteShape(u) {
|
|
|
1345
1530
|
* @returns {{ sites: Array, unverifiedSites: Array, account: object }}
|
|
1346
1531
|
*/
|
|
1347
1532
|
function computePlanCallSites(index, name, def, options = {}) {
|
|
1533
|
+
if (options.valueReferenceRename) {
|
|
1534
|
+
// The value-binding pass settles these source occurrences. Preserve
|
|
1535
|
+
// cross-language exclusions through the full caller sweep when an
|
|
1536
|
+
// unresolved call belongs to another runtime family.
|
|
1537
|
+
const { computeGroundSet, buildAccount } = require('./account');
|
|
1538
|
+
const groundSet = computeGroundSet(index, name);
|
|
1539
|
+
const accountParts = { confirmedEntries: [], unverifiedEntries: [], excludedEntries: [] };
|
|
1540
|
+
const account = buildAccount(index, name, { groundSet, ...accountParts });
|
|
1541
|
+
const { callNotResolvedEntries } = require('./analysis');
|
|
1542
|
+
const candidates = callNotResolvedEntries(index, account);
|
|
1543
|
+
const family = langTraits(index.files.get(def.file)?.language)?.nameReferenceFamily;
|
|
1544
|
+
if (family && candidates.every(site =>
|
|
1545
|
+
langTraits(index.files.get(site.file)?.language)?.nameReferenceFamily === family)) {
|
|
1546
|
+
return { sites: [], unverifiedSites: candidates.map(unverifiedSiteShape),
|
|
1547
|
+
account, groundSet, accountParts, valueReferenceRename: true };
|
|
1548
|
+
}
|
|
1549
|
+
index._opGroundSetCache?.set(name, groundSet);
|
|
1550
|
+
}
|
|
1348
1551
|
// A plan holds one call-site tree per file across its sweeps (fix #395).
|
|
1349
1552
|
const ownsTreeCache = !index._treeCache;
|
|
1350
1553
|
// A rename edits name tokens only; argument analysis (a parse of every
|
|
1351
1554
|
// caller file) serves signature changes (fix #396).
|
|
1352
1555
|
const analyzeArgs = options.analyzeArgs !== false;
|
|
1353
1556
|
const { confirmed, unverified, account, groundSet, accountParts } =
|
|
1354
|
-
contractedCallerSweep(index, name, def);
|
|
1557
|
+
contractedCallerSweep(index, name, def, { constructionSites: options.constructionSites });
|
|
1355
1558
|
|
|
1356
1559
|
const sites = [];
|
|
1357
1560
|
const planLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
@@ -1369,6 +1572,7 @@ function computePlanCallSites(index, name, def, options = {}) {
|
|
|
1369
1572
|
receiver: c.receiver,
|
|
1370
1573
|
calledAs: c.calledAs,
|
|
1371
1574
|
callSite: c.provenance?.facts?.site,
|
|
1575
|
+
constructorSyntax: c.provenance?.facts?.constructorCall?.call?.syntax,
|
|
1372
1576
|
};
|
|
1373
1577
|
const siteKey = `${c.file}:${c.line}:${c.calledAs || name}`;
|
|
1374
1578
|
const occurrence = planLineSeen.get(siteKey) || 0;
|
|
@@ -1389,11 +1593,14 @@ function computePlanCallSites(index, name, def, options = {}) {
|
|
|
1389
1593
|
expression: (call.content || '').trim(),
|
|
1390
1594
|
args: analysis.args,
|
|
1391
1595
|
argCount: analysis.argCount,
|
|
1596
|
+
...(analysis.implicitConstruction && { implicitConstruction: true }),
|
|
1392
1597
|
...(analysis.keywordArgNames && { keywordArgNames: analysis.keywordArgNames }),
|
|
1393
1598
|
...(analysis.positionalCount != null && { positionalCount: analysis.positionalCount }),
|
|
1394
1599
|
...(c.calledAs && { calledAs: c.calledAs }),
|
|
1395
1600
|
...(c.macroExpansion && { macroExpansion: c.macroExpansion }),
|
|
1396
1601
|
...(c.targetTyped && { targetTyped: true }),
|
|
1602
|
+
...(c.keywordDelegation && { keywordDelegation: true }),
|
|
1603
|
+
...(c.ownClassConstruction && { ownClassConstruction: true }),
|
|
1397
1604
|
...(c.destructured && { destructured: c.destructured }),
|
|
1398
1605
|
...(c.importAliasSpelling && { importAliasSpelling: true }),
|
|
1399
1606
|
});
|
|
@@ -1405,9 +1612,10 @@ function computePlanCallSites(index, name, def, options = {}) {
|
|
|
1405
1612
|
if (fc !== 0) return fc;
|
|
1406
1613
|
return (a.line || 0) - (b.line || 0);
|
|
1407
1614
|
});
|
|
1408
|
-
// A target-typed `new(..)`
|
|
1409
|
-
// edits it, so an undecided one is no
|
|
1410
|
-
|
|
1615
|
+
// A target-typed `new(..)` or a keyword delegation spells no name
|
|
1616
|
+
// (fixes #400, #421): a rename never edits it, so an undecided one is no
|
|
1617
|
+
// rename candidate either.
|
|
1618
|
+
const renameRelevant = u => u.macroExpansion?.origin !== 'template' && !u.targetTyped && !u.keywordDelegation;
|
|
1411
1619
|
return {
|
|
1412
1620
|
sites,
|
|
1413
1621
|
unverifiedSites: unverified.filter(renameRelevant).map(unverifiedSiteShape),
|
|
@@ -1464,13 +1672,21 @@ function sharedParsedTree(index, file, content, language) {
|
|
|
1464
1672
|
// compilation) shares the tree only where that view is the plain parse
|
|
1465
1673
|
// (fix #395).
|
|
1466
1674
|
const adapter = getLanguageAdapter(language);
|
|
1467
|
-
if (adapter?.queryTree && !adapter.queryTreeIsPlain?.(content, index.files?.get(file)))
|
|
1675
|
+
if (adapter?.queryTree && !adapter.queryTreeIsPlain?.(content, index.files?.get(file))) {
|
|
1676
|
+
// Its plain parse, kept across operations like the view (fix #436:
|
|
1677
|
+
// impact re-parsed every caller file with a conditional per query).
|
|
1678
|
+
return typeof index._plainParsedTree === 'function'
|
|
1679
|
+
? index._plainParsedTree(file, content, language) : null;
|
|
1680
|
+
}
|
|
1468
1681
|
return index._getParsedTree(file, content, language);
|
|
1469
1682
|
}
|
|
1470
1683
|
|
|
1471
1684
|
function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
1685
|
+
if ((call.constructorSyntax || call.provenance?.facts?.constructorCall?.call?.syntax) === 'implicit') {
|
|
1686
|
+
return { args: [], argCount: 0, isMethodCall: false, implicitConstruction: true };
|
|
1687
|
+
}
|
|
1472
1688
|
try {
|
|
1473
|
-
const language =
|
|
1689
|
+
const language = index.languageOf(call.file);
|
|
1474
1690
|
if (!language) return { args: null, argCount: 0 };
|
|
1475
1691
|
|
|
1476
1692
|
// Use tree cache to avoid re-parsing the same file in batch operations
|
|
@@ -1507,7 +1723,25 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1507
1723
|
|
|
1508
1724
|
// Find the call expression at the target line matching funcName
|
|
1509
1725
|
const spelling = call.calledAs && call.calledAs !== 'bound' ? call.calledAs : funcName;
|
|
1510
|
-
const
|
|
1726
|
+
const constructionSyntax = call.constructorSyntax || call.provenance?.facts?.constructorCall?.call?.syntax;
|
|
1727
|
+
const constructionShape = langTraits(language)?.constructorArgumentNodes?.[constructionSyntax];
|
|
1728
|
+
const constructionArguments = node => constructionShape?.argsField
|
|
1729
|
+
? node.childForFieldName(constructionShape.argsField)
|
|
1730
|
+
: node.namedChildren.find(child => child.type === constructionShape?.argsNodeType);
|
|
1731
|
+
let constructionNode = null;
|
|
1732
|
+
if (constructionShape) {
|
|
1733
|
+
const site = call.callSite || call.provenance?.facts?.site;
|
|
1734
|
+
if (site?.start != null) {
|
|
1735
|
+
for (let node = tree.rootNode.descendantForIndex(site.start); node; node = node.parent) {
|
|
1736
|
+
if (node.type === constructionShape.nodeType && node.startIndex <= site.start &&
|
|
1737
|
+
node.endIndex >= (site.end ?? site.start) && constructionArguments(node)) {
|
|
1738
|
+
constructionNode = node;
|
|
1739
|
+
break;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const callNode = constructionNode || findCallNode(tree.rootNode, callTypes, targetRow, spelling, occurrence,
|
|
1511
1745
|
call.callSite || call.provenance?.facts?.site);
|
|
1512
1746
|
if (!callNode) return { args: null, argCount: 0 };
|
|
1513
1747
|
|
|
@@ -1535,7 +1769,8 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1535
1769
|
// access to the enclosing-function symbol.
|
|
1536
1770
|
const ctx = classifyCallContext(callNode, language);
|
|
1537
1771
|
|
|
1538
|
-
const argsNode =
|
|
1772
|
+
const argsNode = (constructionNode && constructionArguments(constructionNode)) ||
|
|
1773
|
+
callNode.childForFieldName('arguments');
|
|
1539
1774
|
if (!argsNode) return { args: [], argCount: 0, isMethodCall, ...ctx };
|
|
1540
1775
|
|
|
1541
1776
|
let args = [];
|
|
@@ -1646,7 +1881,7 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1646
1881
|
*/
|
|
1647
1882
|
function analyzeCallShape(index, filePath, lineNum, funcName) {
|
|
1648
1883
|
try {
|
|
1649
|
-
const language =
|
|
1884
|
+
const language = index.languageOf(filePath);
|
|
1650
1885
|
if (!language) return null;
|
|
1651
1886
|
|
|
1652
1887
|
// Reuse tree cache to avoid re-parsing during a batch (clustering scans many sites)
|
|
@@ -1998,6 +2233,7 @@ function verify(index, name, options = {}) {
|
|
|
1998
2233
|
isMethod: c.isMethod,
|
|
1999
2234
|
calledAs: c.calledAs,
|
|
2000
2235
|
callSite: c.provenance?.facts?.site,
|
|
2236
|
+
constructorSyntax: c.provenance?.facts?.constructorCall?.call?.syntax,
|
|
2001
2237
|
// Preserve receiver identity through the usage-shaped adapter. Go
|
|
2002
2238
|
// permits a local value to have the same spelling as its type; only
|
|
2003
2239
|
// a type-qualified call is a method expression with an explicit
|
|
@@ -2375,6 +2611,28 @@ const {
|
|
|
2375
2611
|
* pin) or its class (a member): 'edit'; another namespace or class: null;
|
|
2376
2612
|
* unresolvable: 'review'.
|
|
2377
2613
|
*/
|
|
2614
|
+
/**
|
|
2615
|
+
* A Rust path value `Q::m` naming a member pin (fix #422): 'edit' when the
|
|
2616
|
+
* qualifier names the pin's own type or trait (or a type implementing the
|
|
2617
|
+
* pinned trait without a member of its own), null when it names another
|
|
2618
|
+
* type or trait, 'review' when the scopes cannot say.
|
|
2619
|
+
*/
|
|
2620
|
+
function rustPathValueVerdict(index, ref, def) {
|
|
2621
|
+
const callers = require('./callers');
|
|
2622
|
+
const call = { name: def.name, line: ref.line, receiver: ref.receiver, isPathCall: true,
|
|
2623
|
+
isMethod: true, isFunctionReference: true };
|
|
2624
|
+
if (callers.rustPathQualifierVerdict(index, ref.file, call, [def]) === 'other') return null;
|
|
2625
|
+
const { resolveRustPathQualifier } = require('./rust-names');
|
|
2626
|
+
const named = resolveRustPathQualifier(index, ref.file, ref.line, ref.receiver);
|
|
2627
|
+
const owner = require('./class-identity').ownerRefOf(index, def)?.def;
|
|
2628
|
+
const { classKeyOf } = require('./class-identity');
|
|
2629
|
+
if (named?.defs?.length === 1 && owner && classKeyOf(index, named.defs[0]) === classKeyOf(index, owner)) {
|
|
2630
|
+
return 'edit';
|
|
2631
|
+
}
|
|
2632
|
+
if (callers.rustTraitDeclPathProbe(index, ref.file, call, [def]) === 'target') return 'edit';
|
|
2633
|
+
return 'review';
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2378
2636
|
function cppQualifiedReferenceVerdict(index, ref, def) {
|
|
2379
2637
|
const { resolveQualifier, effectiveNamespace, macroPrefixAt, includeClosure } = require('./cpp-scope');
|
|
2380
2638
|
// Inside an indexed declaration the reference's scope is that
|
|
@@ -2416,8 +2674,9 @@ function cppQualifiedReferenceVerdict(index, ref, def) {
|
|
|
2416
2674
|
} catch {
|
|
2417
2675
|
return 'review';
|
|
2418
2676
|
}
|
|
2419
|
-
if (!resolved || resolved.kind === 'unknown') return 'review';
|
|
2420
2677
|
const owner = def.className || (def.receiver ? String(def.receiver).replace(/^\*/, '') : null);
|
|
2678
|
+
if (resolved?.namespaceOnly && owner) return null;
|
|
2679
|
+
if (!resolved || resolved.kind === 'unknown') return 'review';
|
|
2421
2680
|
if (resolved.kind === 'namespace') {
|
|
2422
2681
|
if (owner) return null;
|
|
2423
2682
|
return (resolved.resolvedNamespace || resolved.namespace) === effectiveNamespace(index, def) ? 'edit' : null;
|
|
@@ -2461,21 +2720,42 @@ const IMPORT_STATEMENT_TYPES = new Set(['import_statement', 'import_from_stateme
|
|
|
2461
2720
|
'future_import_statement', 'use_declaration', 'import_declaration', 'using_directive', 'export_statement']);
|
|
2462
2721
|
|
|
2463
2722
|
/** 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
|
-
|
|
2723
|
+
* line/column (1-based line, 0-based column), or `line` when none does. A
|
|
2724
|
+
* token of a declarator initialized by `require(..)`/`import(..)` belongs to
|
|
2725
|
+
* the binding recorded on the initializer's line (fix #423: `const {\n
|
|
2726
|
+
* getLevel,\n} = require('./levels')`); without a column the token `name`
|
|
2727
|
+
* spells on the line is used. */
|
|
2728
|
+
function importStatementLine(index, filePath, line, column, name = null) {
|
|
2466
2729
|
const entry = index.files.get(filePath);
|
|
2467
|
-
if (!entry || !Number.isInteger(column)) return line;
|
|
2730
|
+
if (!entry || (!Number.isInteger(column) && !name)) return line;
|
|
2468
2731
|
let tree;
|
|
2732
|
+
let content = null;
|
|
2469
2733
|
try {
|
|
2470
|
-
|
|
2734
|
+
content = index._readFile(filePath);
|
|
2471
2735
|
tree = index._getParsedTree?.(filePath, content, entry.language) ||
|
|
2472
2736
|
safeParse(getParser(entry.language), content);
|
|
2473
2737
|
} catch {
|
|
2474
2738
|
tree = null;
|
|
2475
2739
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2740
|
+
const columns = [];
|
|
2741
|
+
if (Number.isInteger(column)) columns.push(column);
|
|
2742
|
+
else if (content != null) {
|
|
2743
|
+
const text = content.split('\n')[line - 1] || '';
|
|
2744
|
+
for (let at = text.indexOf(name); at !== -1; at = text.indexOf(name, at + 1)) columns.push(at);
|
|
2745
|
+
}
|
|
2746
|
+
for (const at of columns) {
|
|
2747
|
+
let node = tree?.rootNode?.descendantForPosition({ row: line - 1, column: at });
|
|
2748
|
+
if (!Number.isInteger(column) && (!node || node.text !== name || node.startPosition.column !== at)) continue;
|
|
2749
|
+
while (node && !IMPORT_STATEMENT_TYPES.has(node.type) && node.type !== 'variable_declarator') {
|
|
2750
|
+
node = node.parent;
|
|
2751
|
+
}
|
|
2752
|
+
if (node?.type === 'variable_declarator') {
|
|
2753
|
+
const value = node.childForFieldName('value');
|
|
2754
|
+
return value ? value.startPosition.row + 1 : line;
|
|
2755
|
+
}
|
|
2756
|
+
if (node) return node.startPosition.row + 1;
|
|
2757
|
+
}
|
|
2758
|
+
return line;
|
|
2479
2759
|
}
|
|
2480
2760
|
|
|
2481
2761
|
/** Is a Go site file compiled under another build constraint than the
|
|
@@ -2521,6 +2801,61 @@ function cLinkAlternatives(index, def, definitions) {
|
|
|
2521
2801
|
|
|
2522
2802
|
// C: plain identifiers of one spelling at file scope name one entity
|
|
2523
2803
|
// (no overloading, no namespaces).
|
|
2804
|
+
/**
|
|
2805
|
+
* Go (fix #425): does a dot import of `refFile` (`import . "p"`) bring the
|
|
2806
|
+
* pinned package-level declaration into the file? Its package is the
|
|
2807
|
+
* imported directory's (never its tests) and only exported names enter.
|
|
2808
|
+
*/
|
|
2809
|
+
function goDotImportReaches(index, refFile, def) {
|
|
2810
|
+
if (!def?.file || !/^\p{Lu}/u.test(def.name || '') || def.file.endsWith('_test.go')) return false;
|
|
2811
|
+
const entry = index.files.get(refFile);
|
|
2812
|
+
const pinDir = path.dirname(def.file);
|
|
2813
|
+
return (entry?.importDetails || []).some(detail => {
|
|
2814
|
+
if (detail.type !== 'dot-import' || !detail.module) return false;
|
|
2815
|
+
const rel = entry.moduleResolved?.[detail.module];
|
|
2816
|
+
if (!rel) return false;
|
|
2817
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(index.root, rel);
|
|
2818
|
+
if (path.dirname(abs) !== pinDir) return false;
|
|
2819
|
+
const imported = index.files.get(abs)?.packageName;
|
|
2820
|
+
const pinPackage = index.files.get(def.file)?.packageName;
|
|
2821
|
+
return !imported || !pinPackage || imported === pinPackage;
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* Go (fix #425): does the import named `qualifier` in `refFile` (by package
|
|
2827
|
+
* clause, or the name the file writes) resolve to the package that declares
|
|
2828
|
+
* the pinned exported, non-test declaration?
|
|
2829
|
+
*/
|
|
2830
|
+
function goImportReaches(index, refFile, qualifier, def) {
|
|
2831
|
+
if (!def?.file || !/^\p{Lu}/u.test(def.name || '') || def.file.endsWith('_test.go')) return false;
|
|
2832
|
+
const entry = index.files.get(refFile);
|
|
2833
|
+
const modules = entry?.imports || [];
|
|
2834
|
+
const names = entry?.importNames || [];
|
|
2835
|
+
if (names.length !== modules.length) return false;
|
|
2836
|
+
const pinDir = path.dirname(def.file);
|
|
2837
|
+
return modules.some((module, i) => {
|
|
2838
|
+
if (names[i] !== qualifier) return false;
|
|
2839
|
+
const rel = entry.moduleResolved?.[module];
|
|
2840
|
+
if (!rel) return false;
|
|
2841
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(index.root, rel);
|
|
2842
|
+
return path.dirname(abs) === pinDir;
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
/** Is the receiver of a Go selector reference bound by a local? (fix #425) */
|
|
2847
|
+
function goSelectorReceiverIsLocal(index, ref) {
|
|
2848
|
+
if (!Number.isInteger(ref.column)) return null;
|
|
2849
|
+
try {
|
|
2850
|
+
const content = index._readFile(ref.file);
|
|
2851
|
+
const tree = index._getParsedTree?.(ref.file, content, 'go') ||
|
|
2852
|
+
safeParse(getParser('go'), content);
|
|
2853
|
+
return getLanguageAdapter('go')?.selectorReceiverIsLocal?.(tree, ref.line, ref.column) ?? null;
|
|
2854
|
+
} catch {
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2524
2859
|
function cExternalReferenceLanguage(index, file) {
|
|
2525
2860
|
const traits = langTraits(index.files.get(file)?.language);
|
|
2526
2861
|
return !!traits?.textualIncludes && !traits.hasArityOverloads;
|
|
@@ -2566,25 +2901,26 @@ function cExternalReferenceVerdict(index, ref, def, pinIdentity, definitions) {
|
|
|
2566
2901
|
function applyTypeReferencePass(index, ctx) {
|
|
2567
2902
|
const { def, name, renameTo, changes, reviewItems, planUnverified } = ctx;
|
|
2568
2903
|
const { typeReferenceSites } = require('./type-references');
|
|
2569
|
-
const { files } = typeReferenceSites(index, def, name, ctx.groundSet);
|
|
2904
|
+
const { files } = ctx.referenceSites || typeReferenceSites(index, def, name, ctx.groundSet);
|
|
2905
|
+
const referenceKind = ctx.referenceKind || 'type';
|
|
2570
2906
|
const decided = new Map(); // rel:line -> { abs, edited }
|
|
2571
2907
|
const reviewed = new Map(); // rel:line -> { abs, reason }
|
|
2572
2908
|
const stringEdited = new Set();
|
|
2573
2909
|
const target = `${def.relativePath || def.file}:${def.nameLine || def.startLine}`;
|
|
2574
2910
|
const reasonText = reason => String(reason || 'unresolved').replace(/-/g, ' ');
|
|
2575
2911
|
const special = change => change.contractDependency || change.templateDependency ||
|
|
2576
|
-
change.editKind === 'example' || change.editKind === 'macro-expansion'
|
|
2912
|
+
change.editKind === 'example' || change.editKind === 'macro-expansion' || change.scopeAlternative ||
|
|
2913
|
+
change.keepsLocalName;
|
|
2577
2914
|
// Rebuild a line edit from code-unit token columns.
|
|
2578
|
-
|
|
2915
|
+
// A Python import that binds the renamed module variable keeps its
|
|
2916
|
+
// module and gains an alias (`import json as j`, fix #420).
|
|
2917
|
+
const lineEdit = (raw, columns, aliases = []) => {
|
|
2579
2918
|
const sorted = [...columns].sort((a, b) => b - a);
|
|
2580
|
-
|
|
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
|
-
}
|
|
2919
|
+
const aliased = aliases.filter(column => sorted.includes(column));
|
|
2585
2920
|
return {
|
|
2586
|
-
source: raw.trim(), renamed:
|
|
2587
|
-
columns: sorted, rawSource: raw, oldName: name, newName: renameTo,
|
|
2921
|
+
source: raw.trim(), renamed: renameColumns(raw, sorted, name, renameTo, aliased).trim(),
|
|
2922
|
+
count: sorted.length, columns: sorted, rawSource: raw, oldName: name, newName: renameTo,
|
|
2923
|
+
...(aliased.length > 0 && { aliasColumns: aliased }),
|
|
2588
2924
|
};
|
|
2589
2925
|
};
|
|
2590
2926
|
for (const [abs, info] of files) {
|
|
@@ -2616,6 +2952,7 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2616
2952
|
const units = columns => columns.map(column => tokenColumn(raw, column, name))
|
|
2617
2953
|
.filter(Number.isInteger);
|
|
2618
2954
|
const editUnits = units(slot.edits);
|
|
2955
|
+
const aliasUnits = units(slot.importAliases || []);
|
|
2619
2956
|
const skipUnits = new Set(units(slot.skipColumns || []));
|
|
2620
2957
|
const reviewList = slot.reviews.map(review => ({
|
|
2621
2958
|
...review,
|
|
@@ -2628,14 +2965,32 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2628
2965
|
// its replacement list uses in another role) leaves the edit,
|
|
2629
2966
|
// unless the edit is the pin's own declaration or one a
|
|
2630
2967
|
// dedicated pass proved.
|
|
2968
|
+
// A binding of the pin's own variable keeps its edit; the other
|
|
2969
|
+
// tokens of its line the lookup proves join it (fix #424: `x =
|
|
2970
|
+
// getattr(x, ..)` reads the variable it rebinds).
|
|
2971
|
+
if (existing && existing.scopeAlternative && existing.newExpression !== undefined) {
|
|
2972
|
+
if (editUnits.length > 0) mergeTokenEdit(existing, lineEdit(raw, editUnits));
|
|
2973
|
+
const edited = existing._edit?.columns || [];
|
|
2974
|
+
const open = reviewList.filter(review => review.unit == null || !edited.includes(review.unit));
|
|
2975
|
+
if (open.length > 0) {
|
|
2976
|
+
existing.needsReview = true;
|
|
2977
|
+
existing.reviewReason = existing.reviewReason || open[0].reason;
|
|
2978
|
+
existing.suggestion += `; verify the "${name}" token(s) UCN could not resolve ` +
|
|
2979
|
+
`(${reasonText(open[0].reason)})`;
|
|
2980
|
+
reviewed.set(key, { abs, reason: open[0].reason });
|
|
2981
|
+
} else {
|
|
2982
|
+
decided.set(key, { abs, edited: true });
|
|
2983
|
+
}
|
|
2984
|
+
continue;
|
|
2985
|
+
}
|
|
2631
2986
|
if (existing && existing.newExpression !== undefined && !special(existing)) {
|
|
2632
2987
|
const prior = existing._edit && existing._edit.rawSource === raw ? existing._edit.columns
|
|
2633
2988
|
: renameIdentifierTokens(index, abs, line, name, renameTo).columns || [];
|
|
2634
|
-
const trusted = (existing.isDefinition || existing._evidence) &&
|
|
2989
|
+
const trusted = referenceKind !== 'value' && (existing.isDefinition || existing._evidence) &&
|
|
2635
2990
|
!reviewList.some(review => review.reason === 'macro-argument-other-use');
|
|
2636
2991
|
const hardUnits = new Set(reviewList.filter(review => review.reason === 'macro-argument-other-use')
|
|
2637
2992
|
.map(review => review.unit).filter(Number.isInteger));
|
|
2638
|
-
const keep = trusted ? prior
|
|
2993
|
+
const keep = referenceKind === 'value' ? prior.filter(column => editUnits.includes(column)) : trusted ? prior.filter(column => !skipUnits.has(column))
|
|
2639
2994
|
: prior.filter(column => !skipUnits.has(column) && !hardUnits.has(column));
|
|
2640
2995
|
const final = new Set([...keep, ...editUnits]);
|
|
2641
2996
|
const open = reviewList.filter(review => review.unit == null || !(trusted && final.has(review.unit)));
|
|
@@ -2653,7 +3008,7 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2653
3008
|
reviewed.set(key, { abs, reason: open[0].reason });
|
|
2654
3009
|
continue;
|
|
2655
3010
|
}
|
|
2656
|
-
const edit = lineEdit(raw, final);
|
|
3011
|
+
const edit = lineEdit(raw, final, aliasUnits);
|
|
2657
3012
|
const previous = existing.newExpression;
|
|
2658
3013
|
existing.expression = edit.source;
|
|
2659
3014
|
existing.newExpression = edit.renamed;
|
|
@@ -2678,7 +3033,7 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2678
3033
|
continue;
|
|
2679
3034
|
}
|
|
2680
3035
|
const certain = reviewList.length === 0;
|
|
2681
|
-
const edit = editUnits.length > 0 ? lineEdit(raw, editUnits) : null;
|
|
3036
|
+
const edit = editUnits.length > 0 ? lineEdit(raw, editUnits, aliasUnits) : null;
|
|
2682
3037
|
const hasEdit = !!edit && edit.renamed !== edit.source;
|
|
2683
3038
|
if (certain) {
|
|
2684
3039
|
decided.set(key, { abs, edited: hasEdit });
|
|
@@ -2698,7 +3053,7 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2698
3053
|
file: rel,
|
|
2699
3054
|
line,
|
|
2700
3055
|
expression: edit.source,
|
|
2701
|
-
suggestion: `${kind === 'call' ? 'Rename to' :
|
|
3056
|
+
suggestion: `${kind === 'call' ? 'Rename to' : `Update ${referenceKind} reference`}: ${edit.renamed}`,
|
|
2702
3057
|
newExpression: edit.renamed,
|
|
2703
3058
|
editKind: kind,
|
|
2704
3059
|
});
|
|
@@ -2718,7 +3073,7 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2718
3073
|
file: rel,
|
|
2719
3074
|
line,
|
|
2720
3075
|
expression: edit.source,
|
|
2721
|
-
suggestion: `Update
|
|
3076
|
+
suggestion: `Update ${referenceKind} reference: ${edit.renamed}${reviewNote}`,
|
|
2722
3077
|
newExpression: edit.renamed,
|
|
2723
3078
|
editKind: 'reference',
|
|
2724
3079
|
...(!certain && { needsReview: true, reviewReason: reviewList[0].reason }),
|
|
@@ -2740,7 +3095,7 @@ function applyTypeReferencePass(index, ctx) {
|
|
|
2740
3095
|
// C# applies `XAttribute` as `[X]` (fix #386).
|
|
2741
3096
|
const { csharpAttributeShortSites } = require('./type-references');
|
|
2742
3097
|
const suffix = 'Attribute';
|
|
2743
|
-
for (const site of csharpAttributeShortSites(index, def, name)) {
|
|
3098
|
+
for (const site of referenceKind === 'value' ? [] : csharpAttributeShortSites(index, def, name)) {
|
|
2744
3099
|
if (site.verdict === 'no') continue;
|
|
2745
3100
|
const short = name.slice(0, -suffix.length);
|
|
2746
3101
|
const newShort = renameTo.endsWith(suffix) && renameTo.length > suffix.length
|
|
@@ -2913,6 +3268,66 @@ function memberAssignmentOwnerScope(index, def) {
|
|
|
2913
3268
|
return 'external';
|
|
2914
3269
|
}
|
|
2915
3270
|
|
|
3271
|
+
/** A named constructor outside its class body still names that class. */
|
|
3272
|
+
function constructorRenameOwner(index, member, requireConstructorSyntax = false) {
|
|
3273
|
+
const owners = classDefsNamed(index, member.name).entries.map(entry => entry.def);
|
|
3274
|
+
const contained = owners.filter(d => d.file === member.file &&
|
|
3275
|
+
d.startLine <= member.startLine && d.endLine >= member.endLine)
|
|
3276
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
3277
|
+
if (contained && (!requireConstructorSyntax || member.type === 'constructor' ||
|
|
3278
|
+
member.memberType === 'constructor')) return contained;
|
|
3279
|
+
const line = member.nameLine || member.startLine;
|
|
3280
|
+
const token = renameIdentifierTokens(index, member.file, line, member.name, member.name,
|
|
3281
|
+
null, null, { definitionNameOnly: true, definition: member });
|
|
3282
|
+
if (token.columns?.length !== 1) return requireConstructorSyntax ? undefined : null;
|
|
3283
|
+
if (requireConstructorSyntax) {
|
|
3284
|
+
const entry = index.files.get(member.file);
|
|
3285
|
+
const language = entry?.language;
|
|
3286
|
+
const content = index._readFile(member.file);
|
|
3287
|
+
const tree = index._getParsedTree?.(member.file, content, language) || safeParse(getParser(language), content);
|
|
3288
|
+
const position = { row: line - 1, column: token.columns[0] };
|
|
3289
|
+
const isConstructor = candidate => {
|
|
3290
|
+
const node = candidate?.rootNode.descendantForPosition(position);
|
|
3291
|
+
// A position outside every name token resolves to the root.
|
|
3292
|
+
return !!node?.parent && ['ctor', 'ctor-qualified'].includes(referenceRole(node, language).role);
|
|
3293
|
+
};
|
|
3294
|
+
if (!isConstructor(tree)) {
|
|
3295
|
+
// Decoration macros can make a constructor look return-typed.
|
|
3296
|
+
// Read the same recovered declaration that the index verified.
|
|
3297
|
+
const blanks = entry?.recoveryBlanks;
|
|
3298
|
+
if (!blanks?.length) return undefined;
|
|
3299
|
+
const module = require('../languages').LANGUAGES[language]?.module();
|
|
3300
|
+
const fresh = entry.hash && require('crypto').createHash('md5').update(content).digest('hex') === entry.hash;
|
|
3301
|
+
const recovered = module?.recoveredTree?.(content, getParser(language), fresh ? blanks : null);
|
|
3302
|
+
if (!isConstructor(recovered)) return undefined;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
if (contained) return contained;
|
|
3306
|
+
const { typeReferenceSites } = require('./type-references');
|
|
3307
|
+
const ground = { perFile: new Map([[member.file, new Set([line])]]) };
|
|
3308
|
+
const proven = [];
|
|
3309
|
+
for (const candidate of owners) {
|
|
3310
|
+
const sites = typeReferenceSites(index, candidate, candidate.name, ground);
|
|
3311
|
+
const columns = sites.files.get(member.file)?.lines.get(line)?.edits || [];
|
|
3312
|
+
if (!columns.includes(token.columns[0])) continue;
|
|
3313
|
+
// A class template name alone cannot identify an unindexed
|
|
3314
|
+
// specialization's constructor. Its own declaration must agree.
|
|
3315
|
+
const { _cFamilySignatureKey } = require('./callers');
|
|
3316
|
+
const signature = _cFamilySignatureKey(member);
|
|
3317
|
+
const declaredOwner = [...sites.resolver.group].find(owner => !owner.forwardDeclaration &&
|
|
3318
|
+
(index.symbols.get(member.name) || []).some(declaration => declaration !== member &&
|
|
3319
|
+
(declaration.type === 'constructor' || declaration.memberType === 'constructor') &&
|
|
3320
|
+
declaration.file === owner.file && declaration.startLine >= owner.startLine &&
|
|
3321
|
+
declaration.endLine <= owner.endLine && _cFamilySignatureKey(declaration) === signature));
|
|
3322
|
+
if (declaredOwner) proven.push({ def: declaredOwner, group: sites.resolver.group });
|
|
3323
|
+
}
|
|
3324
|
+
if (!proven.length) return null;
|
|
3325
|
+
const first = proven[0];
|
|
3326
|
+
if (proven.some(hit => hit.group.size !== first.group.size ||
|
|
3327
|
+
[...hit.group].some(def => !first.group.has(def)))) return null;
|
|
3328
|
+
return first.def;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
2916
3331
|
function plan(index, name, options = {}) {
|
|
2917
3332
|
index._beginOp();
|
|
2918
3333
|
// Every slot member of a rename is swept over the same files: their call
|
|
@@ -2927,15 +3342,41 @@ function plan(index, name, options = {}) {
|
|
|
2927
3342
|
|
|
2928
3343
|
const resolved = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
2929
3344
|
let def = resolved.def || definitions[0];
|
|
3345
|
+
if (options.renameTo && definitions.some(other => other !== def && other.file === def.file &&
|
|
3346
|
+
(other.nameLine || other.startLine) === (def.nameLine || def.startLine) &&
|
|
3347
|
+
(!options.className || other.className === options.className) &&
|
|
3348
|
+
(isValueDefinition(def) && !def.className && isTypeRenamePin(other) ||
|
|
3349
|
+
isTypeRenamePin(def) && isValueDefinition(other) && !other.className))) {
|
|
3350
|
+
return { found: true, function: name,
|
|
3351
|
+
error: 'This source handle names both a type and a value. Select the member with class_name or put the declarations on separate source lines.' };
|
|
3352
|
+
}
|
|
2930
3353
|
// A constructor or destructor carries its class's name (Java, C#, C++):
|
|
2931
3354
|
// renaming it is renaming the class (fix #386).
|
|
2932
3355
|
// (A C# member can never carry its type's name except the finalizer.)
|
|
2933
|
-
|
|
3356
|
+
const constructorOwnerName = def.className && langTraits(index.files.get(def.file)?.language)?.textualIncludes
|
|
3357
|
+
? require('./cpp-scope').stripTemplateArguments(def.className).split('::').pop() : def.className;
|
|
3358
|
+
const namedConstructor = constructorOwnerName === def.name && !isTypeRenamePin(def) &&
|
|
2934
3359
|
(def.type === 'constructor' || def.memberType === 'constructor' ||
|
|
2935
|
-
(def.type === 'method' && index.files.get(def.file)?.language === 'csharp'))
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3360
|
+
(def.type === 'method' && index.files.get(def.file)?.language === 'csharp'));
|
|
3361
|
+
// A C/C++ member out of its class can be a constructor or destructor
|
|
3362
|
+
// whose kind the parse could not record (a macro, a specialization, an
|
|
3363
|
+
// alias qualifier). It is named like a project type; any other member is
|
|
3364
|
+
// neither (fix #418: the owner proof ran for every method rename).
|
|
3365
|
+
const qualifiedCandidate = def.className && ['function', 'method', 'constructor'].includes(def.type) &&
|
|
3366
|
+
langTraits(index.files.get(def.file)?.language)?.textualIncludes &&
|
|
3367
|
+
classDefsNamed(index, def.name.startsWith('~') ? def.name.slice(1) : def.name).entries.length > 0;
|
|
3368
|
+
const fixedConstructor = def.type === 'constructor' &&
|
|
3369
|
+
langTraits(index.files.get(def.file)?.language)?.fixedConstructorName === def.name;
|
|
3370
|
+
if (options.renameTo && (namedConstructor || qualifiedCandidate || fixedConstructor)) {
|
|
3371
|
+
const owner = fixedConstructor ? ownerDefinitionOf(index, def) :
|
|
3372
|
+
constructorRenameOwner(index, def, !namedConstructor ||
|
|
3373
|
+
langTraits(index.files.get(def.file)?.language)?.textualIncludes);
|
|
3374
|
+
if (owner !== undefined) {
|
|
3375
|
+
if (!owner) return { found: true, function: name,
|
|
3376
|
+
error: 'The constructor owner could not be resolved. Select the class declaration to plan its rename.' };
|
|
3377
|
+
def = owner;
|
|
3378
|
+
if (fixedConstructor) name = owner.name;
|
|
3379
|
+
}
|
|
2939
3380
|
}
|
|
2940
3381
|
// BUG-BY: enrich types for typed-arrow-fn declarations.
|
|
2941
3382
|
const arrowTypes = extractArrowTypesFromVarDecl(index, def);
|
|
@@ -2960,9 +3401,17 @@ function plan(index, name, options = {}) {
|
|
|
2960
3401
|
// run contractedCallerSweep (v4 tiered contract), so plan and verify stay
|
|
2961
3402
|
// in lock-step by construction. Unverified candidates are NOT planned
|
|
2962
3403
|
// (they may target another symbol) but stay visible with reasons.
|
|
3404
|
+
// A rename the language's own protocol blocks never edits a class
|
|
3405
|
+
// construction: the class is constructed by name, and the initializer
|
|
3406
|
+
// is reached by the protocol, not by its spelling (fix #418: the sweep
|
|
3407
|
+
// listed every construction as a review item of a blocked rename).
|
|
3408
|
+
const constructionSites = !(options.renameTo && require('./constructor-calls').isConstructor(def, index) &&
|
|
3409
|
+
externalContractOf(index, def)?.reason === 'language-protocol-member');
|
|
2963
3410
|
const { sites: planCallSites, unverifiedSites: planUnverified, account: pinAccount,
|
|
2964
|
-
groundSet: planGroundSet, accountParts: planAccountParts
|
|
2965
|
-
|
|
3411
|
+
groundSet: planGroundSet, accountParts: planAccountParts,
|
|
3412
|
+
valueReferenceRename = false } = computePlanCallSites(index, name, def,
|
|
3413
|
+
{ analyzeArgs: !options.renameTo, constructionSites,
|
|
3414
|
+
valueReferenceRename: !!options.renameTo && isValueDefinition(def) && !def.macroExpansion });
|
|
2966
3415
|
let planAccount = pinAccount;
|
|
2967
3416
|
// Call sites a slot member's sweep contributed to this plan (fix #376):
|
|
2968
3417
|
// the plan's ACCOUNT is rebuilt over the same sites it lists.
|
|
@@ -3012,6 +3461,9 @@ function plan(index, name, options = {}) {
|
|
|
3012
3461
|
let operation = null;
|
|
3013
3462
|
let changes = [];
|
|
3014
3463
|
const reviewItems = [];
|
|
3464
|
+
// The declarations a rename changes together: the pin and its
|
|
3465
|
+
// configuration alternatives (fix #419).
|
|
3466
|
+
const renameClosure = [def];
|
|
3015
3467
|
// Contract membership (fix #360): project slot roots the rename closed
|
|
3016
3468
|
// over, and Go satisfaction sites the closure could not complete.
|
|
3017
3469
|
const contractRootDefs = [];
|
|
@@ -3042,10 +3494,27 @@ function plan(index, name, options = {}) {
|
|
|
3042
3494
|
|
|
3043
3495
|
// When adding a param, insert before rest params (*args/**kwargs) and
|
|
3044
3496
|
// before optional params (required must precede optional in Python/TS).
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3497
|
+
// Where calls bind arguments by name (Python), a list with `*args`
|
|
3498
|
+
// or keyword-only params takes the new param as keyword-only, after
|
|
3499
|
+
// them and before `**kwargs` (fix #424): a positional param before
|
|
3500
|
+
// `*args` would rebind every call passing extra positionals.
|
|
3501
|
+
// Inserted before a rest param elsewhere, calls passing arguments
|
|
3502
|
+
// there now bind the new param: they are listed (`rebindIndex`).
|
|
3503
|
+
let rebindIndex = null;
|
|
3504
|
+
const selfNames = ['self', 'cls', '&self', '&mut self', 'mut self'];
|
|
3505
|
+
const minIdx = (newParams.length > 0 && selfNames.includes(newParams[0].name)) ? 1 : 0;
|
|
3506
|
+
const varPositional = p => p.rest && /^\*(?!\*)/.test(String(p.name));
|
|
3507
|
+
const varKeyword = p => p.rest && String(p.name).startsWith('**');
|
|
3508
|
+
if (langTraits(planLang)?.keywordArguments &&
|
|
3509
|
+
newParams.some(p => varPositional(p) || p.keywordOnly)) {
|
|
3510
|
+
newParam.keywordOnly = true;
|
|
3511
|
+
const kwIdx = newParams.findIndex(varKeyword);
|
|
3512
|
+
newParams.splice(kwIdx !== -1 ? kwIdx : newParams.length, 0, newParam);
|
|
3513
|
+
} else {
|
|
3048
3514
|
const firstRestIdx = newParams.findIndex(p => p.rest || (p.name && (p.name.startsWith('*') || p.name.startsWith('...'))));
|
|
3515
|
+
if (firstRestIdx !== -1 && !varKeyword(newParams[firstRestIdx])) {
|
|
3516
|
+
rebindIndex = Math.max(firstRestIdx, minIdx) - minIdx;
|
|
3517
|
+
}
|
|
3049
3518
|
if (firstRestIdx !== -1) {
|
|
3050
3519
|
// Always insert before rest params (*args, **kwargs, ...rest)
|
|
3051
3520
|
const insertIdx = Math.max(firstRestIdx, minIdx);
|
|
@@ -3066,7 +3535,7 @@ function plan(index, name, options = {}) {
|
|
|
3066
3535
|
// Generate new signature with TS-correct optional marker (BUG-BV)
|
|
3067
3536
|
// and arrow-fn enriched return type (BUG-BY).
|
|
3068
3537
|
// BUG-5: preserve all modifier tokens (async/static/public/...).
|
|
3069
|
-
const paramsList = newParams
|
|
3538
|
+
const paramsList = formatTypedParamList(newParams, def.paramTypes);
|
|
3070
3539
|
const modTokens = computeModifierTokens(def);
|
|
3071
3540
|
const modPrefix = modTokens.length ? modTokens.join(' ') + ' ' : '';
|
|
3072
3541
|
newSignature = `${modPrefix}${name}(${paramsList})`;
|
|
@@ -3077,16 +3546,39 @@ function plan(index, name, options = {}) {
|
|
|
3077
3546
|
// for default values, every call site must pass the new argument.
|
|
3078
3547
|
for (const site of planCallSites) {
|
|
3079
3548
|
let suggestion;
|
|
3549
|
+
const positional = site.positionalCount ?? site.argCount;
|
|
3550
|
+
if (rebindIndex !== null && !site.implicitConstruction &&
|
|
3551
|
+
(!site.args || site.args.some(arg => /^(\.\.\.|\*)/.test(arg)) || positional > rebindIndex)) {
|
|
3552
|
+
// An argument this call passes at the new param's position
|
|
3553
|
+
// now binds it instead of the rest param.
|
|
3554
|
+
changes.push({
|
|
3555
|
+
file: site.file,
|
|
3556
|
+
line: site.line,
|
|
3557
|
+
expression: site.expression,
|
|
3558
|
+
suggestion: `Argument ${rebindIndex + 1} now binds ${options.addParam}: pass ${options.addParam} ` +
|
|
3559
|
+
`at position ${rebindIndex + 1} so the remaining arguments still reach the rest parameter`,
|
|
3560
|
+
args: site.args,
|
|
3561
|
+
needsReview: true,
|
|
3562
|
+
editKind: 'call',
|
|
3563
|
+
});
|
|
3564
|
+
continue;
|
|
3565
|
+
}
|
|
3080
3566
|
if (options.defaultValue && langHasDefaults) {
|
|
3081
3567
|
// The default makes the existing call valid. Keep the fact in
|
|
3082
3568
|
// metadata, but do not inflate the concrete edit plan with a
|
|
3083
3569
|
// no-op entry (UCN5-166).
|
|
3084
3570
|
unchangedSites++;
|
|
3085
3571
|
continue;
|
|
3572
|
+
} else if (site.implicitConstruction) {
|
|
3573
|
+
reviewItems.push({ file: site.file, line: site.line, expression: site.expression,
|
|
3574
|
+
suggestion: 'Add an explicit base constructor initializer with the required argument',
|
|
3575
|
+
needsReview: true, editKind: 'implicit-construction', implicitConstruction: true });
|
|
3576
|
+
continue;
|
|
3086
3577
|
} else if (options.defaultValue) {
|
|
3087
3578
|
suggestion = `Add argument: ${options.defaultValue} (no default parameter values in ${planFileEntry?.language || 'this language'})`;
|
|
3088
3579
|
} else {
|
|
3089
|
-
const keywordCall = langTraits(planLang)?.keywordArguments &&
|
|
3580
|
+
const keywordCall = langTraits(planLang)?.keywordArguments &&
|
|
3581
|
+
(newParam.keywordOnly || site.keywordArgNames?.length > 0);
|
|
3090
3582
|
suggestion = keywordCall
|
|
3091
3583
|
? `Add keyword argument: ${options.addParam}=${options.addParam} (replace the right-hand value with the intended expression; keep existing keyword arguments)`
|
|
3092
3584
|
: `Add argument: ${options.addParam}`;
|
|
@@ -3124,7 +3616,7 @@ function plan(index, name, options = {}) {
|
|
|
3124
3616
|
// Generate new signature with TS-correct optional marker (BUG-BV)
|
|
3125
3617
|
// and arrow-fn enriched return type (BUG-BY).
|
|
3126
3618
|
// BUG-5: preserve all modifier tokens (async/static/public/...).
|
|
3127
|
-
const paramsList = newParams
|
|
3619
|
+
const paramsList = formatTypedParamList(newParams, def.paramTypes);
|
|
3128
3620
|
const modTokens = computeModifierTokens(def);
|
|
3129
3621
|
const modPrefix = modTokens.length ? modTokens.join(' ') + ' ' : '';
|
|
3130
3622
|
newSignature = `${modPrefix}${name}(${paramsList})`;
|
|
@@ -3177,6 +3669,9 @@ function plan(index, name, options = {}) {
|
|
|
3177
3669
|
|
|
3178
3670
|
// The rename's usage records, shared with the method-group pass below.
|
|
3179
3671
|
let planUsages = null;
|
|
3672
|
+
// The pin's scope binds its name by other statements too (fix #424): the
|
|
3673
|
+
// rename is the variable's, so references resolving there follow it.
|
|
3674
|
+
let pinScopeRebound = false;
|
|
3180
3675
|
if (options.renameTo) {
|
|
3181
3676
|
operation = 'rename';
|
|
3182
3677
|
newSignature = currentSignature.replace(new RegExp('\\b' + escapeRegExp(name) + '\\b'), options.renameTo);
|
|
@@ -3245,9 +3740,12 @@ function plan(index, name, options = {}) {
|
|
|
3245
3740
|
const emitRenameCallSites = (siteList) => {
|
|
3246
3741
|
const touched = new Set();
|
|
3247
3742
|
for (const incoming of siteList) {
|
|
3248
|
-
// A target-typed `new(..)`
|
|
3249
|
-
//
|
|
3250
|
-
|
|
3743
|
+
// A target-typed `new(..)` or a keyword delegation (`base(..)`)
|
|
3744
|
+
// spells no name: the type written at its declaration site is
|
|
3745
|
+
// what the rename edits (fixes #393, #421).
|
|
3746
|
+
// `cls(..)` / `type(self)(..)` / `new this(..)` spell the
|
|
3747
|
+
// method's receiver, never the class (fix #434).
|
|
3748
|
+
if (incoming.targetTyped || incoming.keywordDelegation || incoming.ownClassConstruction) continue;
|
|
3251
3749
|
// A call of a name destructured from the member (`const { run }
|
|
3252
3750
|
// = make(); run()`) spells the member at the pattern key (fix
|
|
3253
3751
|
// #397): the key is renamed and the local binding kept
|
|
@@ -3280,6 +3778,11 @@ function plan(index, name, options = {}) {
|
|
|
3280
3778
|
}
|
|
3281
3779
|
continue;
|
|
3282
3780
|
}
|
|
3781
|
+
// A call spelled by an import alias (`h()` after `const h =
|
|
3782
|
+
// require('./a').helper`) keeps its spelling; the import's
|
|
3783
|
+
// source-side name is the edit (fix #434: it made a same-line
|
|
3784
|
+
// spelled call a manual review).
|
|
3785
|
+
if (incoming.calledAs && incoming.calledAs !== name && incoming.calledAs !== 'bound') continue;
|
|
3283
3786
|
const lineKey = `${incoming.file}:${incoming.line}`;
|
|
3284
3787
|
const site = emittedRenameCalls.get(lineKey) || {
|
|
3285
3788
|
file: incoming.file,
|
|
@@ -3402,7 +3905,7 @@ function plan(index, name, options = {}) {
|
|
|
3402
3905
|
const usages = index.usages(name, {
|
|
3403
3906
|
codeOnly: false,
|
|
3404
3907
|
includeTests: true,
|
|
3405
|
-
internalEvidence:
|
|
3908
|
+
internalEvidence: !valueReferenceRename,
|
|
3406
3909
|
});
|
|
3407
3910
|
planUsages = usages;
|
|
3408
3911
|
const importUsages = usages.filter(u => u.usageType === 'import' && !u.isDefinition);
|
|
@@ -3438,7 +3941,7 @@ function plan(index, name, options = {}) {
|
|
|
3438
3941
|
// `from m import (\n a,\n)`) belongs to that statement (fix #400).
|
|
3439
3942
|
const onLine = (fileEntry?.importBindings || []).some(binding => binding.line === line &&
|
|
3440
3943
|
(binding.name === name || binding.alias === name));
|
|
3441
|
-
const statementLine = onLine ? line : importStatementLine(index, filePath, line, column);
|
|
3944
|
+
const statementLine = onLine ? line : importStatementLine(index, filePath, line, column, name);
|
|
3442
3945
|
const bindings = (fileEntry?.importBindings || []).filter(binding =>
|
|
3443
3946
|
(binding.line === line || binding.line === statementLine) &&
|
|
3444
3947
|
binding.name === name && binding.module);
|
|
@@ -3451,13 +3954,23 @@ function plan(index, name, options = {}) {
|
|
|
3451
3954
|
binding.alias === name && binding.name !== name)) return false;
|
|
3452
3955
|
if (bindings.length === 0) return true;
|
|
3453
3956
|
return bindings.some(binding => {
|
|
3957
|
+
// A Rust `use` path names the item its module declares (fix
|
|
3958
|
+
// #422): another module's namesake is never the pin.
|
|
3959
|
+
if (langTraits(fileEntry.language)?.itemScopeResolution && !def.className && !def.receiver) {
|
|
3960
|
+
const named = require('./rust-names').resolveRustUsePath(index, filePath, binding.line,
|
|
3961
|
+
binding.module, ['struct', 'enum', 'union', 'trait', 'type', 'module'].includes(def.type)
|
|
3962
|
+
? 'type' : 'value');
|
|
3963
|
+
if (named?.external) return false;
|
|
3964
|
+
if (named?.defs) return named.defs.some(d => d.name === name && renameTargetFiles.has(d.file));
|
|
3965
|
+
}
|
|
3454
3966
|
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
3455
3967
|
const abs = rel ? (require('path').isAbsolute(rel) ? rel
|
|
3456
3968
|
: require('path').join(index.root, rel)) : null;
|
|
3457
3969
|
// An import from a module outside the project (`const parse =
|
|
3458
3970
|
// require('parseurl')`, fix #397) binds that module's value,
|
|
3459
3971
|
// never the renamed definition; a resolver gap keeps the edit.
|
|
3460
|
-
if (!abs && !require('./callers')._unresolvedModuleIsGap(index, binding.module, binding)
|
|
3972
|
+
if (!abs && (!require('./callers')._unresolvedModuleIsGap(index, binding.module, binding) ||
|
|
3973
|
+
require('./callers')._nonSourceModuleFile(index, filePath, binding.module))) return false;
|
|
3461
3974
|
if (!abs) return filePath !== def.file;
|
|
3462
3975
|
if (abs === filePath) return false;
|
|
3463
3976
|
// A module binding the name through one alternative of a
|
|
@@ -3465,16 +3978,40 @@ function plan(index, name, options = {}) {
|
|
|
3465
3978
|
// importer's token names the item, not the renamed pin.
|
|
3466
3979
|
const verdict = _nameBindingReaches(index, abs, name, renameTargetFiles, 4,
|
|
3467
3980
|
{ configurationVerdict: true });
|
|
3981
|
+
// The value-token pass can review an unresolved import. Do
|
|
3982
|
+
// not preempt it with an automatic import edit, especially
|
|
3983
|
+
// one that preserves a configuration item's local name.
|
|
3984
|
+
if (isValueDefinition(def)) return verdict === 'yes';
|
|
3468
3985
|
return verdict !== 'no' && verdict !== 'configuration';
|
|
3469
3986
|
});
|
|
3470
3987
|
};
|
|
3471
3988
|
// An unaliased import that is one alternative of a configuration
|
|
3472
3989
|
// item in its own scope (fix #398F: `try: from ._speedups import f` /
|
|
3473
|
-
// `except ImportError: def f`) keeps the item's local name
|
|
3474
|
-
|
|
3990
|
+
// `except ImportError: def f`) keeps the item's local name, as does
|
|
3991
|
+
// one whose scope also declares the name by a def or class statement
|
|
3992
|
+
// (fix #424: `class OMD` then `try: from .dictutils import OMD`): the
|
|
3993
|
+
// importer's variable is not the renamed definition alone (a plain
|
|
3994
|
+
// rebinding joins the variable's rename, fix #420).
|
|
3995
|
+
const importKeepsItemName = (filePath, line, column = null) => {
|
|
3475
3996
|
const { itemsNamed } = require('./configuration-items');
|
|
3476
|
-
|
|
3477
|
-
item.sites.some(site => site.kind === 'import' && site.line === line));
|
|
3997
|
+
if (itemsNamed(index, filePath, name).some(item =>
|
|
3998
|
+
item.sites.some(site => site.kind === 'import' && site.line === line))) return true;
|
|
3999
|
+
const entry = index.files.get(filePath);
|
|
4000
|
+
if (!langTraits(entry?.language)?.assignmentDeclaresLocal) return false;
|
|
4001
|
+
let tree;
|
|
4002
|
+
try {
|
|
4003
|
+
const content = index._readFile(filePath);
|
|
4004
|
+
tree = index._getParsedTree?.(filePath, content, entry.language) ||
|
|
4005
|
+
safeParse(getParser(entry.language), content);
|
|
4006
|
+
} catch { tree = null; }
|
|
4007
|
+
const text = index._getFileLines(filePath)[line - 1] || '';
|
|
4008
|
+
const at = Number.isInteger(column) ? column : text.length - text.trimStart().length;
|
|
4009
|
+
let importNode = tree?.rootNode.descendantForPosition({ row: line - 1, column: at }) || null;
|
|
4010
|
+
while (importNode && importNode.type !== 'import_from_statement' &&
|
|
4011
|
+
importNode.type !== 'import_statement') importNode = importNode.parent;
|
|
4012
|
+
if (!importNode) return false;
|
|
4013
|
+
const { pythonScopeBindingKinds } = require('../languages/lexical-scope');
|
|
4014
|
+
return pythonScopeBindingKinds(importNode, name).some(site => site.kind === 'def');
|
|
3478
4015
|
};
|
|
3479
4016
|
for (const imp of importUsages) {
|
|
3480
4017
|
// Skip if already covered by a call site change in the same file:line
|
|
@@ -3485,14 +4022,17 @@ function plan(index, name, options = {}) {
|
|
|
3485
4022
|
// The token is the importer's own local alias (fix #397): an
|
|
3486
4023
|
// import alias keeps its local name.
|
|
3487
4024
|
if (imp.importAlias) continue;
|
|
4025
|
+
// A module path segment names a module (fix #424).
|
|
4026
|
+
if (imp.modulePath) continue;
|
|
3488
4027
|
if (imp.file && def.file && !importMayBindPin(imp.file)) continue;
|
|
3489
4028
|
if (imp.file && !importSourceBindsPin(imp.file, imp.line, imp.column)) continue;
|
|
4029
|
+
const keepsLocalName = !!imp.file && importKeepsItemName(imp.file, imp.line, imp.column);
|
|
3490
4030
|
const edit = renameIdentifierTokens(index, imp.file,
|
|
3491
|
-
imp.line, name,
|
|
4031
|
+
imp.line, name, keepsLocalName
|
|
3492
4032
|
? `${options.renameTo} as ${name}` : options.renameTo);
|
|
3493
4033
|
const newImport = edit.renamed;
|
|
3494
4034
|
if (newImport === edit.source) continue;
|
|
3495
|
-
|
|
4035
|
+
const importChange = tagTokenEdit({
|
|
3496
4036
|
file: imp.relativePath || imp.file,
|
|
3497
4037
|
line: imp.line,
|
|
3498
4038
|
expression: edit.source,
|
|
@@ -3500,7 +4040,11 @@ function plan(index, name, options = {}) {
|
|
|
3500
4040
|
newExpression: newImport,
|
|
3501
4041
|
isImport: true,
|
|
3502
4042
|
editKind: 'import',
|
|
3503
|
-
}, edit)
|
|
4043
|
+
}, edit);
|
|
4044
|
+
// The module still binds the old name (fix #424): its `__all__`
|
|
4045
|
+
// entry names that binding.
|
|
4046
|
+
if (keepsLocalName) Object.defineProperty(importChange, 'keepsLocalName', { value: true });
|
|
4047
|
+
changes.push(importChange);
|
|
3504
4048
|
}
|
|
3505
4049
|
|
|
3506
4050
|
// Renamed CJS/Python imports are intentionally surfaced as reference
|
|
@@ -3542,8 +4086,10 @@ function plan(index, name, options = {}) {
|
|
|
3542
4086
|
for (const exported of targetEntry.exportDetails || []) {
|
|
3543
4087
|
if ((exported.name !== name && exported.alias !== name) ||
|
|
3544
4088
|
!exported.line) continue;
|
|
4089
|
+
// Under the pin's own name (fix #423): `export { other as
|
|
4090
|
+
// name } from './pin'` exposes another declaration.
|
|
3545
4091
|
const ownership = _nameBindingReaches(
|
|
3546
|
-
index, exportPath, name, renameTargetFiles, 8);
|
|
4092
|
+
index, exportPath, name, renameTargetFiles, 8, { targetName: name });
|
|
3547
4093
|
// Transitive file reachability is not name ownership. A file
|
|
3548
4094
|
// can import the target somewhere in its dependency closure
|
|
3549
4095
|
// while exporting its own same-spelled local declaration.
|
|
@@ -3616,7 +4162,7 @@ function plan(index, name, options = {}) {
|
|
|
3616
4162
|
const pathMod = require('path');
|
|
3617
4163
|
const dunderFiles = new Set([def.file]);
|
|
3618
4164
|
for (const change of changes) {
|
|
3619
|
-
if (change.editKind !== 'import') continue;
|
|
4165
|
+
if (change.editKind !== 'import' || change.keepsLocalName) continue;
|
|
3620
4166
|
const abs = pathMod.isAbsolute(change.file)
|
|
3621
4167
|
? change.file : pathMod.join(index.root, change.file);
|
|
3622
4168
|
dunderFiles.add(abs);
|
|
@@ -3653,14 +4199,18 @@ function plan(index, name, options = {}) {
|
|
|
3653
4199
|
const quoteRe = new RegExp(`(["'])${escapeRegExp(name)}\\1`, 'g');
|
|
3654
4200
|
const newLine = sourceLine.replace(quoteRe, `$1${options.renameTo}$1`);
|
|
3655
4201
|
if (newLine === sourceLine) continue;
|
|
3656
|
-
|
|
4202
|
+
const entryChange = {
|
|
3657
4203
|
file: rel,
|
|
3658
4204
|
line: lineNo,
|
|
3659
4205
|
expression: sourceLine.trim(),
|
|
3660
4206
|
suggestion: `Update __all__ entry: ${newLine.trim()}`,
|
|
3661
4207
|
newExpression: newLine.trim(),
|
|
3662
4208
|
editKind: 'reference',
|
|
3663
|
-
}
|
|
4209
|
+
};
|
|
4210
|
+
// The edited entry is no text dependency left
|
|
4211
|
+
// for review (fix #424).
|
|
4212
|
+
Object.defineProperty(entryChange, 'dunderAllEntry', { value: true });
|
|
4213
|
+
changes.push(entryChange);
|
|
3664
4214
|
} else {
|
|
3665
4215
|
for (let i = 0; i < s.namedChildCount; i++) strStack.push(s.namedChild(i));
|
|
3666
4216
|
}
|
|
@@ -3699,6 +4249,8 @@ function plan(index, name, options = {}) {
|
|
|
3699
4249
|
? `Update declaration: ${edit.renamed}`
|
|
3700
4250
|
: isAccessorDefinition(member) && isAccessorDefinition(def)
|
|
3701
4251
|
? `Update accessor: ${edit.renamed}`
|
|
4252
|
+
: member.modifiers?.includes('partial') && def.modifiers?.includes('partial')
|
|
4253
|
+
? `Update partial method part: ${edit.renamed}`
|
|
3702
4254
|
: `Update overload signature: ${edit.renamed}`,
|
|
3703
4255
|
newExpression: edit.renamed,
|
|
3704
4256
|
isDefinition: true,
|
|
@@ -3734,36 +4286,23 @@ function plan(index, name, options = {}) {
|
|
|
3734
4286
|
});
|
|
3735
4287
|
}
|
|
3736
4288
|
if (conditionalStyle && !def.isSignature) {
|
|
3737
|
-
const {
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
//
|
|
3743
|
-
//
|
|
3744
|
-
//
|
|
3745
|
-
|
|
3746
|
-
other
|
|
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))) ||
|
|
4289
|
+
const { _isCrossFileConfigurationMember, _isConfigurationAlternative } = require('./callers');
|
|
4290
|
+
const alternatives = definitions.filter(other => other !== def && (
|
|
4291
|
+
// One rule with the caller engine (fix #426): Python runtime
|
|
4292
|
+
// configuration items (fix #398F), Go build-constraint files
|
|
4293
|
+
// (fix #425), and same-file definitions of one owner, owner
|
|
4294
|
+
// path, signature and generic arity with configuration
|
|
4295
|
+
// evidence (`#[cfg]`, a conditional branch the other does not
|
|
4296
|
+
// share): overloads differing in generic arity and local
|
|
4297
|
+
// functions of two methods are two items.
|
|
4298
|
+
_isConfigurationAlternative(index, def, other, { macros: true }) ||
|
|
3761
4299
|
// The same class defined again in a file no translation unit
|
|
3762
4300
|
// includes beside this one (fix #385: `#ifdef _WIN32
|
|
3763
4301
|
// #include "client-windows.h" #else #include "client.h"`).
|
|
3764
4302
|
(!other.isSignature && _isCrossFileConfigurationMember(index, def, other)) ||
|
|
3765
4303
|
linkAlternatives.defines.includes(other)));
|
|
3766
4304
|
const alternativeSites = [];
|
|
4305
|
+
renameClosure.push(...alternatives);
|
|
3767
4306
|
for (const other of alternatives) {
|
|
3768
4307
|
const line = other.nameLine || other.startLine;
|
|
3769
4308
|
const rel = other.relativePath || other.file;
|
|
@@ -3784,7 +4323,7 @@ function plan(index, name, options = {}) {
|
|
|
3784
4323
|
}, edit));
|
|
3785
4324
|
}
|
|
3786
4325
|
}
|
|
3787
|
-
const sweep = computePlanCallSites(index, name, other, { analyzeArgs: !options.renameTo });
|
|
4326
|
+
const sweep = computePlanCallSites(index, name, other, { analyzeArgs: !options.renameTo, constructionSites });
|
|
3788
4327
|
const seen = new Set([...planCallSites, ...alternativeSites].map(site =>
|
|
3789
4328
|
`${site.file}:${site.line}:${site.column ?? '*'}`));
|
|
3790
4329
|
const lines = new Set([...planCallSites, ...alternativeSites].map(site =>
|
|
@@ -3842,8 +4381,7 @@ function plan(index, name, options = {}) {
|
|
|
3842
4381
|
if (alternativeSites.length > 0) {
|
|
3843
4382
|
emitRenameCallSites(alternativeSites);
|
|
3844
4383
|
slotAccountSites.push(...alternativeSites);
|
|
3845
|
-
const
|
|
3846
|
-
const kept = planUnverified.filter(site => !changedLines.has(`${site.file}:${site.line}`));
|
|
4384
|
+
const kept = planUnverified.filter(site => !siteCoveredByChanges(index, changes, site, name));
|
|
3847
4385
|
planUnverified.length = 0;
|
|
3848
4386
|
planUnverified.push(...kept);
|
|
3849
4387
|
planUnverified.sort((a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line);
|
|
@@ -3859,7 +4397,9 @@ function plan(index, name, options = {}) {
|
|
|
3859
4397
|
// alias, and class-body reads of a member are references.
|
|
3860
4398
|
{
|
|
3861
4399
|
let adapter = null;
|
|
3862
|
-
try {
|
|
4400
|
+
try {
|
|
4401
|
+
if (langTraits(planLang)?.scopeAlternativeBindings !== false) adapter = getLanguageAdapter(planLang);
|
|
4402
|
+
} catch { adapter = null; }
|
|
3863
4403
|
if (typeof adapter?.findScopeAlternativeBindings === 'function') {
|
|
3864
4404
|
let tree = null;
|
|
3865
4405
|
try {
|
|
@@ -3872,6 +4412,7 @@ function plan(index, name, options = {}) {
|
|
|
3872
4412
|
? adapter.findScopeAlternativeBindings(tree, def.nameLine || def.startLine, name)
|
|
3873
4413
|
: [];
|
|
3874
4414
|
const rel = def.relativePath || def.file;
|
|
4415
|
+
if (bindings.some(binding => binding.kind !== 'reference')) pinScopeRebound = true;
|
|
3875
4416
|
for (const binding of bindings) {
|
|
3876
4417
|
if (changes.some(change => change.file === rel && change.line === binding.line)) continue;
|
|
3877
4418
|
const replacement = binding.kind === 'import-name'
|
|
@@ -3880,7 +4421,7 @@ function plan(index, name, options = {}) {
|
|
|
3880
4421
|
replacement, [binding.column]);
|
|
3881
4422
|
if (edit.renamed === edit.source) continue;
|
|
3882
4423
|
const isReference = binding.kind === 'reference';
|
|
3883
|
-
|
|
4424
|
+
const change = tagTokenEdit({
|
|
3884
4425
|
file: rel,
|
|
3885
4426
|
line: binding.line,
|
|
3886
4427
|
expression: edit.source,
|
|
@@ -3890,7 +4431,12 @@ function plan(index, name, options = {}) {
|
|
|
3890
4431
|
newExpression: edit.renamed,
|
|
3891
4432
|
...(!isReference && { isDefinition: true }),
|
|
3892
4433
|
editKind: isReference ? 'reference' : 'definition',
|
|
3893
|
-
}, edit)
|
|
4434
|
+
}, edit);
|
|
4435
|
+
// A binding of the pin's own variable (fix #424): the
|
|
4436
|
+
// type lookup never re-decides it (`from threading
|
|
4437
|
+
// import RLock` beside `class RLock` keeps its alias).
|
|
4438
|
+
Object.defineProperty(change, 'scopeAlternative', { value: true });
|
|
4439
|
+
changes.push(change);
|
|
3894
4440
|
}
|
|
3895
4441
|
}
|
|
3896
4442
|
}
|
|
@@ -4189,8 +4735,17 @@ function plan(index, name, options = {}) {
|
|
|
4189
4735
|
// band with slot attribution. Bounded — a pathological slot
|
|
4190
4736
|
// discloses the cut instead of sweeping forever.
|
|
4191
4737
|
const SLOT_SWEEP_CAP = 25;
|
|
4192
|
-
const siteTokenKey = site =>
|
|
4193
|
-
|
|
4738
|
+
const siteTokenKey = site => {
|
|
4739
|
+
// A slot's members can confirm different calls on one line.
|
|
4740
|
+
// Resolve column-less records by their own source range before
|
|
4741
|
+
// deduplicating them, just as the edit pass does.
|
|
4742
|
+
const column = Number.isInteger(site.column) ? site.column
|
|
4743
|
+
: callTokenColumn(index, site.absoluteFile || site.file,
|
|
4744
|
+
site.siteRange, name, site.line);
|
|
4745
|
+
const token = Number.isInteger(column) ? column
|
|
4746
|
+
: site.siteRange ? `${site.siteRange.start}-${site.siteRange.end}` : '*';
|
|
4747
|
+
return `${site.file}:${site.line}:${token}`;
|
|
4748
|
+
};
|
|
4194
4749
|
const seenSiteTokens = new Set(planCallSites.map(site => siteTokenKey(site)));
|
|
4195
4750
|
const slotMemberSites = [];
|
|
4196
4751
|
// Members a macro generated (fix #374) are swept after, and
|
|
@@ -4201,7 +4756,7 @@ function plan(index, name, options = {}) {
|
|
|
4201
4756
|
const sweptMembers = [...writtenMembers.slice(0, SLOT_SWEEP_CAP),
|
|
4202
4757
|
...generatedMembers.slice(0, SLOT_SWEEP_CAP)];
|
|
4203
4758
|
for (const memberDef of sweptMembers) {
|
|
4204
|
-
const memberSweep = computePlanCallSites(index, name, memberDef, { analyzeArgs: !options.renameTo });
|
|
4759
|
+
const memberSweep = computePlanCallSites(index, name, memberDef, { analyzeArgs: !options.renameTo, constructionSites });
|
|
4205
4760
|
for (const site of memberSweep.sites) {
|
|
4206
4761
|
const key = siteTokenKey(site);
|
|
4207
4762
|
if (seenSiteTokens.has(key)) continue;
|
|
@@ -4280,10 +4835,8 @@ function plan(index, name, options = {}) {
|
|
|
4280
4835
|
slotAccountSites.push(...slotMemberSites);
|
|
4281
4836
|
// A site a slot member's sweep CONFIRMED is a planned edit now —
|
|
4282
4837
|
// it no longer belongs in the pin's "may need this change" band.
|
|
4283
|
-
const changedLines = new Set(changes.map(change =>
|
|
4284
|
-
`${change.file}:${change.line}`));
|
|
4285
4838
|
const keptUnverified = planUnverified.filter(site =>
|
|
4286
|
-
!
|
|
4839
|
+
!siteCoveredByChanges(index, changes, site, name));
|
|
4287
4840
|
planUnverified.length = 0;
|
|
4288
4841
|
planUnverified.push(...keptUnverified);
|
|
4289
4842
|
planUnverified.sort((a, b) => codeUnitCompare(a.file, b.file) ||
|
|
@@ -4448,8 +5001,7 @@ function plan(index, name, options = {}) {
|
|
|
4448
5001
|
}, edit));
|
|
4449
5002
|
}
|
|
4450
5003
|
if (editsByLine.size > 0) {
|
|
4451
|
-
const
|
|
4452
|
-
const kept = planUnverified.filter(site => !changedLines.has(`${site.file}:${site.line}`));
|
|
5004
|
+
const kept = planUnverified.filter(site => !siteCoveredByChanges(index, changes, site, name));
|
|
4453
5005
|
planUnverified.length = 0;
|
|
4454
5006
|
planUnverified.push(...kept);
|
|
4455
5007
|
}
|
|
@@ -4483,14 +5035,25 @@ function plan(index, name, options = {}) {
|
|
|
4483
5035
|
// so those rows surface needsReview instead of a synthesized edit.
|
|
4484
5036
|
// A NESTED pin's own container is exempt: inside it the pin IS the
|
|
4485
5037
|
// binder (`return decorator`).
|
|
4486
|
-
|
|
5038
|
+
// Value references use the binding pass below, including local
|
|
5039
|
+
// shadows and receivers, rather than the callable reference rules.
|
|
5040
|
+
if (!valueReferenceRename) {
|
|
4487
5041
|
const fileSymbols = index.files.get(def.file)?.symbols || [];
|
|
4488
5042
|
const scopeKinds = new Set(['function', 'method', 'constructor',
|
|
4489
5043
|
'private', 'get', 'set', 'property', 'classmethod', 'special']);
|
|
5044
|
+
// Source spans (line, column) decide nesting on one line (fix
|
|
5045
|
+
// #426: `int Go() { int Handler() => 3; return Handler(); }`).
|
|
5046
|
+
const spanOf = symbol => {
|
|
5047
|
+
const span = symbol.sourceSpan;
|
|
5048
|
+
return span && Number.isInteger(span.startColumn) && Number.isInteger(span.endColumn) ? span : null;
|
|
5049
|
+
};
|
|
5050
|
+
const atOrBefore = (l1, c1, l2, c2) => l1 < l2 || (l1 === l2 && c1 <= c2);
|
|
4490
5051
|
const rangeOf = symbol => ({
|
|
4491
5052
|
start: symbol.startLine,
|
|
4492
5053
|
end: symbol.endLine || symbol.startLine,
|
|
5054
|
+
span: spanOf(symbol),
|
|
4493
5055
|
});
|
|
5056
|
+
const spanWidth = range => (range.span ? range.span.end - range.span.start : Infinity);
|
|
4494
5057
|
const containerOf = (symbol) => {
|
|
4495
5058
|
const target = rangeOf(symbol);
|
|
4496
5059
|
let best = null;
|
|
@@ -4498,18 +5061,35 @@ function plan(index, name, options = {}) {
|
|
|
4498
5061
|
if (candidate === symbol || !scopeKinds.has(candidate.type)) continue;
|
|
4499
5062
|
const range = rangeOf(candidate);
|
|
4500
5063
|
if (!(range.start <= target.start && range.end >= target.end)) continue;
|
|
4501
|
-
if (range.start === target.start && range.end === target.end
|
|
4502
|
-
|
|
5064
|
+
if (range.start === target.start && range.end === target.end &&
|
|
5065
|
+
!(range.span && target.span && spanWidth(range) > spanWidth(target) &&
|
|
5066
|
+
atOrBefore(range.span.startLine, range.span.startColumn,
|
|
5067
|
+
target.span.startLine, target.span.startColumn) &&
|
|
5068
|
+
atOrBefore(target.span.endLine, target.span.endColumn,
|
|
5069
|
+
range.span.endLine, range.span.endColumn))) continue;
|
|
5070
|
+
if (!best || (range.end - range.start) < (best.end - best.start) ||
|
|
5071
|
+
((range.end - range.start) === (best.end - best.start) && spanWidth(range) < spanWidth(best))) {
|
|
4503
5072
|
best = range;
|
|
4504
5073
|
}
|
|
4505
5074
|
}
|
|
4506
5075
|
return best; // null = module scope
|
|
4507
5076
|
};
|
|
5077
|
+
// Does a container hold a reference? By its span when both have
|
|
5078
|
+
// columns and the container shares a line with others.
|
|
5079
|
+
const holds = (container, ref) => {
|
|
5080
|
+
if (!(container.start <= ref.line && container.end >= ref.line)) return false;
|
|
5081
|
+
if (!container.span || !Number.isInteger(ref.column) ||
|
|
5082
|
+
(ref.line !== container.start && ref.line !== container.end)) return true;
|
|
5083
|
+
return atOrBefore(container.span.startLine, container.span.startColumn, ref.line, ref.column) &&
|
|
5084
|
+
atOrBefore(ref.line, ref.column, container.span.endLine, container.span.endColumn);
|
|
5085
|
+
};
|
|
4508
5086
|
// A prototype and its definition (C/C++ forward declarations,
|
|
4509
5087
|
// overload signatures) are one callable (fix #396): each binds
|
|
4510
5088
|
// the name as the pin.
|
|
4511
|
-
|
|
4512
|
-
|
|
5089
|
+
// Configuration alternatives are one item with the pin too (fix
|
|
5090
|
+
// #419): a reference ambiguous only between them names the item.
|
|
5091
|
+
const pinIdentity = new Set([...require('./callers')._closeCallableIdentityGroup(
|
|
5092
|
+
index, [def], definitions), ...renameClosure]);
|
|
4513
5093
|
const binders = definitions
|
|
4514
5094
|
.filter(candidate => candidate.file === def.file &&
|
|
4515
5095
|
!NON_CALLABLE_TYPES.has(candidate.type))
|
|
@@ -4546,6 +5126,37 @@ function plan(index, name, options = {}) {
|
|
|
4546
5126
|
// declared at file scope for a file-scope resolution to reach it
|
|
4547
5127
|
// (a Rust inline module is its own scope).
|
|
4548
5128
|
const pinNameLine = def.nameLine || def.startLine;
|
|
5129
|
+
// Does a Python reference in the pin's file resolve to the scope
|
|
5130
|
+
// the pin's statement binds its name in (fix #424)?
|
|
5131
|
+
let pinScopeTree;
|
|
5132
|
+
let pinScopeNode;
|
|
5133
|
+
const referenceInPinScope = (ref) => {
|
|
5134
|
+
if (ref.file !== def.file || !Number.isInteger(ref.column)) return false;
|
|
5135
|
+
if (pinScopeTree === undefined) {
|
|
5136
|
+
pinScopeTree = null;
|
|
5137
|
+
try {
|
|
5138
|
+
const content = index._readFile(def.file);
|
|
5139
|
+
pinScopeTree = index._getParsedTree?.(def.file, content, planLang) ||
|
|
5140
|
+
safeParse(getParser(planLang), content);
|
|
5141
|
+
} catch { pinScopeTree = null; }
|
|
5142
|
+
let declNode = pinScopeTree?.rootNode.descendantForPosition({ row: pinNameLine - 1,
|
|
5143
|
+
column: Math.max(0, (index._getFileLines(def.file)[pinNameLine - 1] || '').indexOf(name)) });
|
|
5144
|
+
while (declNode && declNode.type !== 'function_definition' && declNode.type !== 'class_definition') {
|
|
5145
|
+
declNode = declNode.parent;
|
|
5146
|
+
}
|
|
5147
|
+
pinScopeNode = declNode?.parent || null;
|
|
5148
|
+
while (pinScopeNode && pinScopeNode.type !== 'function_definition' && pinScopeNode.type !== 'module') {
|
|
5149
|
+
if (pinScopeNode.type === 'class_definition') { pinScopeNode = null; break; }
|
|
5150
|
+
pinScopeNode = pinScopeNode.parent;
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
if (!pinScopeNode || pinScopeNode.type !== 'function_definition') return false;
|
|
5154
|
+
const node = pinScopeTree.rootNode.descendantForPosition({ row: ref.line - 1, column: ref.column });
|
|
5155
|
+
if (node?.type !== 'identifier' || node.text !== name) return false;
|
|
5156
|
+
const { referenceScopeNode } = require('../languages/lexical-scope');
|
|
5157
|
+
const scope = referenceScopeNode(node, name);
|
|
5158
|
+
return !!scope && sameNode(scope, pinScopeNode);
|
|
5159
|
+
};
|
|
4549
5160
|
const pinAtFileScope = !pinContainer && !fileSymbols.some(symbol =>
|
|
4550
5161
|
symbol.type === 'module' && symbol !== def &&
|
|
4551
5162
|
symbol.startLine <= pinNameLine &&
|
|
@@ -4562,6 +5173,12 @@ function plan(index, name, options = {}) {
|
|
|
4562
5173
|
if (ref.receiver && index.files.get(ref.file)?.language === 'cpp') {
|
|
4563
5174
|
return cppQualifiedReferenceVerdict(index, ref, def);
|
|
4564
5175
|
}
|
|
5176
|
+
// A path value naming a member (`let f = T::method;`, fix
|
|
5177
|
+
// #422) names what its qualifier resolves to by item scopes.
|
|
5178
|
+
if (def.className && ref.receiver && ref.scopedReference &&
|
|
5179
|
+
langTraits(index.files.get(ref.file)?.language)?.itemScopeResolution) {
|
|
5180
|
+
return rustPathValueVerdict(index, ref, def);
|
|
5181
|
+
}
|
|
4565
5182
|
// A function declared in a function body (a C# local
|
|
4566
5183
|
// function, a nested def) is in scope only in its block:
|
|
4567
5184
|
// never from another file, and never as the member of an
|
|
@@ -4596,6 +5213,18 @@ function plan(index, name, options = {}) {
|
|
|
4596
5213
|
}
|
|
4597
5214
|
return null;
|
|
4598
5215
|
}
|
|
5216
|
+
// A member access (`options.revalidate`) never names a
|
|
5217
|
+
// non-member declaration through its binding (fix #423,
|
|
5218
|
+
// swr: a pin in an untyped-cast arrow had no container
|
|
5219
|
+
// and module-scope binder logic edited the property);
|
|
5220
|
+
// only a module namespace receiver can, as below.
|
|
5221
|
+
if (ref.receiver) {
|
|
5222
|
+
const viaModule = !ref.receiverLocalBinding
|
|
5223
|
+
? _moduleAttributeBindingReaches(index, ref.file, ref.receiver, name,
|
|
5224
|
+
renameTargetFiles)
|
|
5225
|
+
: null;
|
|
5226
|
+
return viaModule === 'yes' ? 'edit' : viaModule === 'unknown' ? 'review' : null;
|
|
5227
|
+
}
|
|
4599
5228
|
if (!ref.receiver && ref.scopeBinding === 'module') {
|
|
4600
5229
|
// Resolves at file scope: the pin when it is the
|
|
4601
5230
|
// file's only binding of the name there (a nested pin
|
|
@@ -4604,24 +5233,25 @@ function plan(index, name, options = {}) {
|
|
|
4604
5233
|
if (!pinAtFileScope) return null;
|
|
4605
5234
|
} else if (!ref.receiver && ref.scopeBinding === 'local') {
|
|
4606
5235
|
// An enclosing function or block binds the name: the
|
|
4607
|
-
// pin only when that binding is the pin's own def
|
|
4608
|
-
|
|
4609
|
-
|
|
5236
|
+
// pin only when that binding is the pin's own def, or
|
|
5237
|
+
// the variable the pin's scope binds by other
|
|
5238
|
+
// statements too, renamed whole (fix #424).
|
|
5239
|
+
if (Array.isArray(ref.scopeDefLines) && ref.scopeDefLines.includes(pinNameLine)) return 'edit';
|
|
5240
|
+
return pinScopeRebound && referenceInPinScope(ref) ? 'edit' : null;
|
|
4610
5241
|
}
|
|
4611
5242
|
let winner = null;
|
|
4612
5243
|
let ambiguous = false;
|
|
4613
5244
|
for (const binder of binders) {
|
|
4614
|
-
const contains = !binder.container ||
|
|
4615
|
-
(binder.container.start <= ref.line &&
|
|
4616
|
-
binder.container.end >= ref.line);
|
|
5245
|
+
const contains = !binder.container || holds(binder.container, ref);
|
|
4617
5246
|
if (!contains) continue;
|
|
4618
5247
|
const size = binder.container
|
|
4619
5248
|
? binder.container.end - binder.container.start
|
|
4620
5249
|
: Infinity;
|
|
4621
|
-
|
|
4622
|
-
|
|
5250
|
+
const width = binder.container ? spanWidth(binder.container) : Infinity;
|
|
5251
|
+
if (!winner || size < winner.size || (size === winner.size && width < winner.width)) {
|
|
5252
|
+
winner = { def: binder.def, size, width };
|
|
4623
5253
|
ambiguous = false;
|
|
4624
|
-
} else if (size === winner.size &&
|
|
5254
|
+
} else if (size === winner.size && width === winner.width &&
|
|
4625
5255
|
binder.def !== winner.def) {
|
|
4626
5256
|
ambiguous = true;
|
|
4627
5257
|
}
|
|
@@ -4634,6 +5264,24 @@ function plan(index, name, options = {}) {
|
|
|
4634
5264
|
return null;
|
|
4635
5265
|
}
|
|
4636
5266
|
if (def.className) return null;
|
|
5267
|
+
// Go (fix #425): a bare name no local binds denotes the
|
|
5268
|
+
// package-level declaration of the reference's own package
|
|
5269
|
+
// (directory and package clause, test visibility), or the
|
|
5270
|
+
// exported name a dot import brings into the file.
|
|
5271
|
+
if (!ref.receiver && ref.scopeBinding === 'module' && !def.receiver &&
|
|
5272
|
+
langTraits(index.files.get(ref.file)?.language)?.packageScope === 'directory') {
|
|
5273
|
+
const { goSamePackage } = require('./go-packages');
|
|
5274
|
+
if (goSamePackage(index, ref.file, def.file)) return 'edit';
|
|
5275
|
+
return goDotImportReaches(index, ref.file, def) ? 'edit' : null;
|
|
5276
|
+
}
|
|
5277
|
+
// `pkg.F` (fix #425): an import no local shadows names the
|
|
5278
|
+
// imported package's declaration.
|
|
5279
|
+
if (ref.receiver && !def.receiver &&
|
|
5280
|
+
langTraits(index.files.get(ref.file)?.language)?.packageScope === 'directory') {
|
|
5281
|
+
if (!goImportReaches(index, ref.file, ref.receiver, def)) return null;
|
|
5282
|
+
const local = goSelectorReceiverIsLocal(index, ref);
|
|
5283
|
+
return local === false ? 'edit' : local === true ? null : 'review';
|
|
5284
|
+
}
|
|
4637
5285
|
// Module-attribute references carry stronger evidence
|
|
4638
5286
|
// than a bare name: `import requests; requests.put` binds
|
|
4639
5287
|
// through requests' export chain even inside a function.
|
|
@@ -4657,8 +5305,29 @@ function plan(index, name, options = {}) {
|
|
|
4657
5305
|
if (ref.scopeBinding === 'module' && cExternalReferenceLanguage(index, ref.file)) {
|
|
4658
5306
|
return cExternalReferenceVerdict(index, ref, def, pinIdentity, definitions);
|
|
4659
5307
|
}
|
|
5308
|
+
// A file-scope reference of a statically scoped module names
|
|
5309
|
+
// that module's own top-level declaration of the name when no
|
|
5310
|
+
// import binds it (fix #423, pino `const { epochTime } =
|
|
5311
|
+
// time`): a local declaration, never the pin of another file.
|
|
5312
|
+
if (ref.scopeBinding === 'module' &&
|
|
5313
|
+
langTraits(index.files.get(ref.file)?.language)?.staticModuleScope) {
|
|
5314
|
+
const refEntry = index.files.get(ref.file);
|
|
5315
|
+
const imported = (refEntry?.importBindings || []).some(binding =>
|
|
5316
|
+
(binding.alias || binding.name) === name);
|
|
5317
|
+
if (!imported && (refEntry?.symbols || []).some(symbol => symbol.name === name &&
|
|
5318
|
+
!symbol.className && !symbol.memberAssigned && !symbol.bodyScopedName &&
|
|
5319
|
+
!(refEntry.symbols || []).some(scope => scope !== symbol &&
|
|
5320
|
+
scopeKinds.has(scope.type) && scope.startLine <= symbol.startLine &&
|
|
5321
|
+
(scope.endLine || scope.startLine) >= (symbol.endLine || symbol.startLine) &&
|
|
5322
|
+
!(scope.startLine === symbol.startLine && scope.endLine === symbol.endLine)))) {
|
|
5323
|
+
return null;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
// The chain must arrive under the pin's own name (fix #423,
|
|
5327
|
+
// swr `import { middleware as preload }`): an import alias
|
|
5328
|
+
// spelled like the pin binds the other export.
|
|
4660
5329
|
const ownership = _nameBindingReaches(
|
|
4661
|
-
index, ref.file, name, renameTargetFiles);
|
|
5330
|
+
index, ref.file, name, renameTargetFiles, 4, { targetName: name });
|
|
4662
5331
|
if (ownership === 'yes' &&
|
|
4663
5332
|
(ref.scopeBinding === 'module' ||
|
|
4664
5333
|
!insideFunctionLike(ref.file, ref.line))) {
|
|
@@ -4668,6 +5337,10 @@ function plan(index, name, options = {}) {
|
|
|
4668
5337
|
};
|
|
4669
5338
|
const eligible = (ref) => {
|
|
4670
5339
|
if (ref.usageType !== 'reference' || ref.isDefinition) return false;
|
|
5340
|
+
// A module path segment names a module; the name of `from m
|
|
5341
|
+
// import name as alias` is decided by m in the import pass
|
|
5342
|
+
// (fix #424), never by this file's binding of the name.
|
|
5343
|
+
if (ref.modulePath || ref.importSource) return false;
|
|
4671
5344
|
// Keyword-argument names, object keys and struct field keys
|
|
4672
5345
|
// are not references to a non-member binding (a member pin
|
|
4673
5346
|
// reads its receiver tokens from the line below).
|
|
@@ -4700,7 +5373,9 @@ function plan(index, name, options = {}) {
|
|
|
4700
5373
|
let refColumns = records.filter((record, i) => verdicts[i] === 'edit')
|
|
4701
5374
|
.map(record => record.column);
|
|
4702
5375
|
if (!refColumns.every(Number.isInteger)) refColumns = null;
|
|
4703
|
-
|
|
5376
|
+
// A path value (`T::m`, fix #422) is its own token.
|
|
5377
|
+
if (verdict === 'edit' && ref.file === def.file && def.className &&
|
|
5378
|
+
records.some((record, i) => verdicts[i] === 'edit' && !record.scopedReference)) {
|
|
4704
5379
|
const selfColumns = selfMemberTokenColumns(index, def.file, ref.line, name);
|
|
4705
5380
|
if (selfColumns.length === 0) verdict = 'review';
|
|
4706
5381
|
else refColumns = selfColumns;
|
|
@@ -4742,6 +5417,22 @@ function plan(index, name, options = {}) {
|
|
|
4742
5417
|
continue;
|
|
4743
5418
|
}
|
|
4744
5419
|
if (existing) {
|
|
5420
|
+
const uneditedReview = records.some((record, i) => verdicts[i] === 'review' &&
|
|
5421
|
+
(!Number.isInteger(record.column) || !existing._edit?.columns.includes(record.column)));
|
|
5422
|
+
// Type lookup decides every token in its later pass,
|
|
5423
|
+
// including another type token beside this construction.
|
|
5424
|
+
if (uneditedReview && !isTypeRenamePin(def) && !existing.needsReview) {
|
|
5425
|
+
changes.push({
|
|
5426
|
+
file: rel,
|
|
5427
|
+
line: ref.line,
|
|
5428
|
+
expression: (ref.content || '').trim(),
|
|
5429
|
+
suggestion: `Verify this reference resolves to ` +
|
|
5430
|
+
`${name} at ${def.relativePath || def.file}:` +
|
|
5431
|
+
`${def.startLine} before renaming`,
|
|
5432
|
+
needsReview: true,
|
|
5433
|
+
editKind: 'reference',
|
|
5434
|
+
});
|
|
5435
|
+
}
|
|
4745
5436
|
// A line another pass already edits (a call beside the
|
|
4746
5437
|
// reference) gains the reference tokens.
|
|
4747
5438
|
// A member pin's own `this.m` tokens join a bound call's
|
|
@@ -4768,7 +5459,7 @@ function plan(index, name, options = {}) {
|
|
|
4768
5459
|
editKind: 'reference',
|
|
4769
5460
|
}, edit));
|
|
4770
5461
|
} else {
|
|
4771
|
-
|
|
5462
|
+
const review = {
|
|
4772
5463
|
file: rel,
|
|
4773
5464
|
line: ref.line,
|
|
4774
5465
|
expression: (ref.content || '').trim(),
|
|
@@ -4777,7 +5468,53 @@ function plan(index, name, options = {}) {
|
|
|
4777
5468
|
`${def.startLine} before renaming`,
|
|
4778
5469
|
needsReview: true,
|
|
4779
5470
|
editKind: 'reference',
|
|
4780
|
-
}
|
|
5471
|
+
};
|
|
5472
|
+
// The tokens under review (fix #419): a declaration edit
|
|
5473
|
+
// of this line that renames them already decides them.
|
|
5474
|
+
const reviewed = records.map(record => record.column);
|
|
5475
|
+
if (reviewed.every(Number.isInteger)) {
|
|
5476
|
+
Object.defineProperty(review, '_reviewColumns', { value: reviewed, enumerable: false });
|
|
5477
|
+
}
|
|
5478
|
+
changes.push(review);
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
// An object-pattern key that destructures the pin from a module
|
|
5482
|
+
// object (fix #423, pino `const time = require('./lib/time');
|
|
5483
|
+
// const { epochTime } = time`) reads the module member: the key
|
|
5484
|
+
// is renamed and the local kept, as for a destructured member
|
|
5485
|
+
// call. A module chain UCN cannot follow is a review item.
|
|
5486
|
+
if (!def.className) {
|
|
5487
|
+
const seenPatternLines = new Set();
|
|
5488
|
+
for (const u of usages) {
|
|
5489
|
+
if (!u.isDefinition || u.file === def.file || !u.file) continue;
|
|
5490
|
+
const fileLanguage = index.files.get(u.file)?.language;
|
|
5491
|
+
if (!langTraits(fileLanguage)?.staticModuleScope) continue;
|
|
5492
|
+
const lineKey = `${u.file}:${u.line}`;
|
|
5493
|
+
if (seenPatternLines.has(lineKey)) continue;
|
|
5494
|
+
seenPatternLines.add(lineKey);
|
|
5495
|
+
for (let line = u.startLine || u.line; line <= (u.endLine || u.line); line++) {
|
|
5496
|
+
for (const key of objectPatternKeysAt(index, u.file, line, name)) {
|
|
5497
|
+
const verdict = _moduleAttributeBindingReaches(index, u.file, key.source, name,
|
|
5498
|
+
renameTargetFiles);
|
|
5499
|
+
if (verdict === 'yes') {
|
|
5500
|
+
emitPatternKeyEdit(u.file, u.relativePath || u.file, key);
|
|
5501
|
+
} else if (verdict === 'unknown') {
|
|
5502
|
+
const rel = u.relativePath || u.file;
|
|
5503
|
+
if (!reviewItems.some(item => item.file === rel && item.line === key.line)) {
|
|
5504
|
+
reviewItems.push({
|
|
5505
|
+
file: rel,
|
|
5506
|
+
line: key.line,
|
|
5507
|
+
expression: (index.getLineContent(u.file, key.line) || '').trim(),
|
|
5508
|
+
suggestion: `Verify whether "${key.source}.${name}" names ${name} at ` +
|
|
5509
|
+
`${def.relativePath || def.file}:${def.startLine}; if it does, ` +
|
|
5510
|
+
`rename the destructured property to "${options.renameTo}", keeping the local binding`,
|
|
5511
|
+
needsReview: true,
|
|
5512
|
+
editKind: 'reference',
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
4781
5518
|
}
|
|
4782
5519
|
}
|
|
4783
5520
|
}
|
|
@@ -4786,9 +5523,46 @@ function plan(index, name, options = {}) {
|
|
|
4786
5523
|
// form: attribute reads and writes. Reuse impact's receiver-evidence
|
|
4787
5524
|
// query so a typed field is edited mechanically and an unresolved
|
|
4788
5525
|
// receiver is surfaced for review instead of silently omitted.
|
|
4789
|
-
|
|
5526
|
+
let accessorReferences = findAccessorReferences(index, name, def, {
|
|
4790
5527
|
includeTests: true,
|
|
4791
5528
|
});
|
|
5529
|
+
// A property's dispatch slot (fix #426): an access the pin's lookup
|
|
5530
|
+
// leaves to an overriding member (`Count` read inside a subclass
|
|
5531
|
+
// declaring `override Count`) is an access of that slot member, and
|
|
5532
|
+
// so is one the pin's own lookup could not decide but a slot
|
|
5533
|
+
// member's lookup confirms. The members' confirmed accesses join the
|
|
5534
|
+
// pin's answer; what they leave undecided the pin's own answer
|
|
5535
|
+
// already partitions.
|
|
5536
|
+
const slotAccessors = accessorReferences && hierarchySlot
|
|
5537
|
+
? [...hierarchySlot.ancestors, ...hierarchySlot.descendants]
|
|
5538
|
+
.flatMap(entry => entry.members)
|
|
5539
|
+
.filter(member => member !== def && isAccessorDefinition(member))
|
|
5540
|
+
: [];
|
|
5541
|
+
if (slotAccessors.length > 0) {
|
|
5542
|
+
const tokenKey = ref => `${ref.absoluteFile}\0${ref.line}\0${ref.column ?? '*'}`;
|
|
5543
|
+
const confirmed = new Map(accessorReferences.confirmed.map(ref => [tokenKey(ref), ref]));
|
|
5544
|
+
const unverified = [...accessorReferences.unverified];
|
|
5545
|
+
for (const member of slotAccessors.slice(0, 25)) {
|
|
5546
|
+
const answer = findAccessorReferences(index, name, member, { includeTests: true });
|
|
5547
|
+
if (!answer) continue;
|
|
5548
|
+
for (const ref of answer.confirmed) {
|
|
5549
|
+
if (!confirmed.has(tokenKey(ref))) confirmed.set(tokenKey(ref), ref);
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
const sort = (a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line ||
|
|
5553
|
+
(a.column ?? -1) - (b.column ?? -1);
|
|
5554
|
+
const seenUnverified = new Set();
|
|
5555
|
+
accessorReferences = {
|
|
5556
|
+
...accessorReferences,
|
|
5557
|
+
confirmed: [...confirmed.values()].sort(sort),
|
|
5558
|
+
unverified: unverified.filter(ref => {
|
|
5559
|
+
const key = tokenKey(ref);
|
|
5560
|
+
if (confirmed.has(key) || seenUnverified.has(key)) return false;
|
|
5561
|
+
seenUnverified.add(key);
|
|
5562
|
+
return true;
|
|
5563
|
+
}).sort(sort),
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
4792
5566
|
if (accessorReferences) {
|
|
4793
5567
|
const confirmedByLine = new Map();
|
|
4794
5568
|
for (const ref of accessorReferences.confirmed) {
|
|
@@ -4833,8 +5607,7 @@ function plan(index, name, options = {}) {
|
|
|
4833
5607
|
const existing = changes.find(change =>
|
|
4834
5608
|
change.file === ref.file && change.line === ref.line &&
|
|
4835
5609
|
!change.needsReview);
|
|
4836
|
-
if (existing)
|
|
4837
|
-
else changes.push(concrete);
|
|
5610
|
+
if (!existing || !mergeTokenEdit(existing, edit)) changes.push(tagTokenEdit(concrete, edit));
|
|
4838
5611
|
}
|
|
4839
5612
|
const listedAccess = new Set();
|
|
4840
5613
|
for (const ref of accessorReferences.unverified) {
|
|
@@ -4861,6 +5634,8 @@ function plan(index, name, options = {}) {
|
|
|
4861
5634
|
const rel = textRef.relativePath || textRef.file;
|
|
4862
5635
|
if (reviewItems.some(item =>
|
|
4863
5636
|
item.file === rel && item.line === textRef.line)) continue;
|
|
5637
|
+
if (changes.some(change => change.dunderAllEntry && change.file === rel &&
|
|
5638
|
+
change.line === textRef.line)) continue;
|
|
4864
5639
|
reviewItems.push({
|
|
4865
5640
|
file: rel,
|
|
4866
5641
|
line: textRef.line,
|
|
@@ -4882,8 +5657,23 @@ function plan(index, name, options = {}) {
|
|
|
4882
5657
|
const definitionLine = def.nameLine || def.startLine;
|
|
4883
5658
|
const definitionFile = def.relativePath || def.file;
|
|
4884
5659
|
const definitionSource = index.getLineContent(def.file, definitionLine).trim();
|
|
5660
|
+
// A reference awaiting review on this line stays a separate item, with
|
|
5661
|
+
// its reason; the declaration token still has its own certain edit
|
|
5662
|
+
// (fix #419: only accessor pins kept it apart). A type rename's own pass
|
|
5663
|
+
// decides every token of its lines.
|
|
5664
|
+
let definitionEdit;
|
|
5665
|
+
const declarationEdit = () => definitionEdit ||= renameIdentifierTokens(index, def.file,
|
|
5666
|
+
definitionLine, name, options.renameTo, null, null, { definitionNameOnly: true, definition: def });
|
|
5667
|
+
const decidedByDeclaration = change => Array.isArray(change._reviewColumns) && (() => {
|
|
5668
|
+
const edit = declarationEdit();
|
|
5669
|
+
return Array.isArray(edit.columns) && change._reviewColumns.every(column =>
|
|
5670
|
+
edit.columns.includes(tokenColumn(edit.rawSource || '', column, name)));
|
|
5671
|
+
})();
|
|
5672
|
+
const separateReview = change => !!options.renameTo && !!change.needsReview &&
|
|
5673
|
+
(isAccessorDefinition(def) || (!isTypeRenamePin(def) && change.editKind === 'reference' &&
|
|
5674
|
+
!change.isDefinition && !decidedByDeclaration(change)));
|
|
4885
5675
|
const existingDefinitionLine = changes.find(change =>
|
|
4886
|
-
change.file === definitionFile && change.line === definitionLine);
|
|
5676
|
+
change.file === definitionFile && change.line === definitionLine && !separateReview(change));
|
|
4887
5677
|
// A definition a macro_rules! transcriber generated (fix #374) is
|
|
4888
5678
|
// spelled in the macro's template, never at the invocation: the
|
|
4889
5679
|
// template pass below edits or lists it; the invocation line is only
|
|
@@ -4903,9 +5693,7 @@ function plan(index, name, options = {}) {
|
|
|
4903
5693
|
existingDefinitionLine.isDefinition = true;
|
|
4904
5694
|
existingDefinitionLine.editKind = 'definition';
|
|
4905
5695
|
if (options.renameTo) {
|
|
4906
|
-
const defEdit =
|
|
4907
|
-
definitionLine, name, options.renameTo,
|
|
4908
|
-
null, null, { definitionNameOnly: true, definition: def });
|
|
5696
|
+
const defEdit = declarationEdit();
|
|
4909
5697
|
// Keep the tokens the line's other edit renames (a recursive or
|
|
4910
5698
|
// delegating call on the declaration line) (fix #376).
|
|
4911
5699
|
if (!(existingDefinitionLine.newExpression !== undefined &&
|
|
@@ -4924,9 +5712,7 @@ function plan(index, name, options = {}) {
|
|
|
4924
5712
|
editKind: 'definition',
|
|
4925
5713
|
};
|
|
4926
5714
|
if (options.renameTo) {
|
|
4927
|
-
const defEdit =
|
|
4928
|
-
definitionLine, name, options.renameTo,
|
|
4929
|
-
null, null, { definitionNameOnly: true, definition: def });
|
|
5715
|
+
const defEdit = declarationEdit();
|
|
4930
5716
|
const renamed = defEdit.renamed;
|
|
4931
5717
|
definitionChange.newExpression = renamed;
|
|
4932
5718
|
definitionChange.suggestion = `Update definition: ${renamed}`;
|
|
@@ -5035,6 +5821,25 @@ function plan(index, name, options = {}) {
|
|
|
5035
5821
|
// resolve to exactly the renamed declaration is a review item.
|
|
5036
5822
|
let typePass = null;
|
|
5037
5823
|
const fileRenames = [];
|
|
5824
|
+
if (options.renameTo && isValueDefinition(def) && planGroundSet && !def.macroExpansion) {
|
|
5825
|
+
const { valueReferenceSites } = require('./value-references');
|
|
5826
|
+
const references = valueReferenceSites(index, def, name, planGroundSet);
|
|
5827
|
+
if (references.ambiguousDeclaration) return { found: true, function: name,
|
|
5828
|
+
error: references.ambiguityReason === 'storage'
|
|
5829
|
+
? 'This source handle names both a static and an instance field. Put the declarations on separate source lines to select one.'
|
|
5830
|
+
: 'This source handle names multiple value declarations in different scopes. Put the declarations on separate source lines to select one.' };
|
|
5831
|
+
const valueRename = references.privateIdentifier ? `#${options.renameTo}` : options.renameTo;
|
|
5832
|
+
if (references.privateIdentifier) {
|
|
5833
|
+
const at = currentSignature.indexOf(name);
|
|
5834
|
+
if (at >= 0) newSignature = currentSignature.slice(0, at) + valueRename +
|
|
5835
|
+
currentSignature.slice(at + name.length);
|
|
5836
|
+
}
|
|
5837
|
+
typePass = applyTypeReferencePass(index, {
|
|
5838
|
+
def, name, renameTo: valueRename, changes, reviewItems,
|
|
5839
|
+
planUnverified, groundSet: planGroundSet, referenceKind: 'value',
|
|
5840
|
+
referenceSites: references,
|
|
5841
|
+
});
|
|
5842
|
+
}
|
|
5038
5843
|
if (options.renameTo && isTypeRenamePin(def) && planGroundSet &&
|
|
5039
5844
|
!def.macroExpansion) {
|
|
5040
5845
|
typePass = applyTypeReferencePass(index, {
|
|
@@ -5055,6 +5860,29 @@ function plan(index, name, options = {}) {
|
|
|
5055
5860
|
}
|
|
5056
5861
|
}
|
|
5057
5862
|
|
|
5863
|
+
// A descendant's instance write can read the inherited method before
|
|
5864
|
+
// replacing it. The field declaration is a different binding, but its
|
|
5865
|
+
// initializer still needs review when this method is renamed.
|
|
5866
|
+
if (options.renameTo && def.className && !isValueDefinition(def)) {
|
|
5867
|
+
const { instanceAttributeInitializerReads } = require('./value-references');
|
|
5868
|
+
for (const member of definitions) {
|
|
5869
|
+
if (!member.instanceAttribute) continue;
|
|
5870
|
+
const owner = ownerRefOf(index, member);
|
|
5871
|
+
if (!owner || descendsFromTargets(index, owner, [def]) === 'no') continue;
|
|
5872
|
+
for (const site of instanceAttributeInitializerReads(index, member)) {
|
|
5873
|
+
const file = member.relativePath || path.relative(index.root, member.file);
|
|
5874
|
+
if (changes.some(change => change.file === file && change.line === site.line &&
|
|
5875
|
+
change._edit?.columns?.includes(site.column))) continue;
|
|
5876
|
+
if (reviewItems.some(item => item.file === file && item.line === site.line &&
|
|
5877
|
+
item.reviewReason === 'instance-attribute-before-write')) continue;
|
|
5878
|
+
reviewItems.push({ file, line: site.line,
|
|
5879
|
+
expression: index.getLineContent(member.file, site.line).trim(),
|
|
5880
|
+
suggestion: `Review the previous ${name} value read before this instance attribute is written`,
|
|
5881
|
+
needsReview: true, reviewReason: 'instance-attribute-before-write', editKind: 'reference' });
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5058
5886
|
// Token pasting can spell the renamed name without writing it (fix
|
|
5059
5887
|
// #396: `struct sdshdr##T` in SDS_HDR_VAR, invoked as SDS_HDR_VAR(8, s)):
|
|
5060
5888
|
// the pasting macro and each invocation that produces the name are
|
|
@@ -5278,12 +6106,43 @@ function plan(index, name, options = {}) {
|
|
|
5278
6106
|
planUnverified.push(...kept);
|
|
5279
6107
|
}
|
|
5280
6108
|
|
|
6109
|
+
// Configuration-dependent C/C++ bindings (fix #413): a site in a known
|
|
6110
|
+
// arm where an imported declaration, not any declaration the rename
|
|
6111
|
+
// changes, binds the name keeps its spelling; the plan's ACCOUNT lists
|
|
6112
|
+
// it as another target (fix #419: never dropped unreported). A site the
|
|
6113
|
+
// build configuration decides is a review item.
|
|
6114
|
+
const configurationOther = [];
|
|
6115
|
+
if (options.renameTo && langTraits(planLang)?.textualIncludes) {
|
|
6116
|
+
const { conditionalImportBinding } = require('./cpp-scope');
|
|
6117
|
+
for (let i = changes.length - 1; i >= 0; i--) {
|
|
6118
|
+
const change = changes[i];
|
|
6119
|
+
if (change.editKind === 'definition' || change.needsReview ||
|
|
6120
|
+
change.newExpression === undefined) continue;
|
|
6121
|
+
// A site in another alternative's branch binds that alternative,
|
|
6122
|
+
// which the rename changes too: 'other' means outside the whole
|
|
6123
|
+
// closure (fix #419).
|
|
6124
|
+
const configuration = conditionalImportBinding(index, renameClosure, {
|
|
6125
|
+
file: path.resolve(index.root, change.file), line: change.line,
|
|
6126
|
+
});
|
|
6127
|
+
if (configuration === 'other') {
|
|
6128
|
+
changes.splice(i, 1);
|
|
6129
|
+
configurationOther.push({ file: path.resolve(index.root, change.file), line: change.line,
|
|
6130
|
+
reason: 'configuration-other' });
|
|
6131
|
+
} else if (configuration === 'uncertain') {
|
|
6132
|
+
delete change.newExpression;
|
|
6133
|
+
change.needsReview = true;
|
|
6134
|
+
change.reviewReason = 'configuration-alternative';
|
|
6135
|
+
change.suggestion = 'Build configuration selects this name\'s declaration; review this reference before renaming it';
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
|
|
5281
6140
|
// One source of truth for the plan's ACCOUNT (fix #376): a line the plan
|
|
5282
6141
|
// edits as a call is confirmed and a line it lists as unverified is
|
|
5283
6142
|
// unverified, whichever slot member's sweep contributed it; the pin's own
|
|
5284
6143
|
// exclusions only keep the lines no listed site claims.
|
|
5285
6144
|
if (options.renameTo && planAccountParts && planGroundSet &&
|
|
5286
|
-
(slotAccountSites.length > 0 || planUnverified.length > 0 || typePass)) {
|
|
6145
|
+
(slotAccountSites.length > 0 || planUnverified.length > 0 || typePass || configurationOther.length > 0)) {
|
|
5287
6146
|
const pathMod = require('path');
|
|
5288
6147
|
const absoluteOf = file => (pathMod.isAbsolute(file) ? file : pathMod.join(index.root, file));
|
|
5289
6148
|
const editedCallLines = new Set(changes.filter(change =>
|
|
@@ -5307,10 +6166,12 @@ function plan(index, name, options = {}) {
|
|
|
5307
6166
|
}
|
|
5308
6167
|
return { confirmed, excluded };
|
|
5309
6168
|
};
|
|
6169
|
+
const otherLines = new Set(configurationOther.filter(entry => !editedCallLines.has(
|
|
6170
|
+
`${relOf(entry.file)}:${entry.line}`)).map(entry => `${entry.file}:${entry.line}`));
|
|
5310
6171
|
const build = (extra) => buildAccount(index, name, {
|
|
5311
6172
|
groundSet: planGroundSet,
|
|
5312
6173
|
confirmedEntries: [
|
|
5313
|
-
...planAccountParts.confirmedEntries,
|
|
6174
|
+
...planAccountParts.confirmedEntries.filter(entry => !otherLines.has(`${absoluteOf(entry.file)}:${entry.line}`)),
|
|
5314
6175
|
...slotAccountSites.filter(site => editedCallLines.has(`${site.file}:${site.line}`))
|
|
5315
6176
|
.map(site => ({ file: site.absoluteFile || absoluteOf(site.file), line: site.line })),
|
|
5316
6177
|
...extra.confirmed,
|
|
@@ -5320,7 +6181,7 @@ function plan(index, name, options = {}) {
|
|
|
5320
6181
|
!typeDecided.has(`${relOf(entry.file)}:${entry.line}`)),
|
|
5321
6182
|
...planUnverified.map(site => ({ file: absoluteOf(site.file), line: site.line })),
|
|
5322
6183
|
],
|
|
5323
|
-
excludedEntries: [...extra.excluded, ...planAccountParts.excludedEntries],
|
|
6184
|
+
excludedEntries: [...configurationOther, ...extra.excluded, ...planAccountParts.excludedEntries],
|
|
5324
6185
|
});
|
|
5325
6186
|
let extra = typeClaims((typePass?.removed || []).map(site => `${site.file}:${site.line}`));
|
|
5326
6187
|
planAccount = build(extra);
|
|
@@ -5692,7 +6553,7 @@ function analyzeCallSiteAST(index, filePath, lineNum, funcName) {
|
|
|
5692
6553
|
};
|
|
5693
6554
|
|
|
5694
6555
|
try {
|
|
5695
|
-
const language =
|
|
6556
|
+
const language = index.languageOf(filePath);
|
|
5696
6557
|
if (!language) return result;
|
|
5697
6558
|
|
|
5698
6559
|
const parser = getParser(language);
|
|
@@ -5902,78 +6763,6 @@ function _docCrefMemberPass(index, name, renameTo, renamedDefs, changes, reviewI
|
|
|
5902
6763
|
|
|
5903
6764
|
const CSHARP_TYPE_DECLARATION_NODES = new Set(['class_declaration', 'struct_declaration',
|
|
5904
6765
|
'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
6766
|
/**
|
|
5978
6767
|
* Method groups in a C# member rename (fix #395): a simple name in value
|
|
5979
6768
|
* position (`new(Build, 4)`, `Changed += OnChanged`, `.Select(Build)`,
|
|
@@ -5988,7 +6777,8 @@ function csharpLocalBinding(memberNode, node, name) {
|
|
|
5988
6777
|
*/
|
|
5989
6778
|
function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
5990
6779
|
const pathMod = require('path');
|
|
5991
|
-
const { resolveClassRef, findDeclaringClass, parentRefsOf } = require('./class-identity');
|
|
6780
|
+
const { resolveClassRef, findDeclaringClass, parentRefsOf, relationToTargets } = require('./class-identity');
|
|
6781
|
+
const { externalTypeDenotation } = require('./type-denotation');
|
|
5992
6782
|
const renamed = new Set(renamedDefs);
|
|
5993
6783
|
const members = (index.symbols.get(name) || []).filter(d => d.className &&
|
|
5994
6784
|
!IDENTITY_TYPE_KINDS_FOR_CREF.has(d.type));
|
|
@@ -6016,6 +6806,9 @@ function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
|
6016
6806
|
if (column == null || !tree) return 'review';
|
|
6017
6807
|
const node = tree.rootNode.descendantForPosition({ row, column });
|
|
6018
6808
|
if (!node || node.type !== 'identifier' || node.text !== name) return 'review';
|
|
6809
|
+
const position = referenceRole(node, index.files.get(ref.file)?.language);
|
|
6810
|
+
if (position.role === 'unknown') return 'review';
|
|
6811
|
+
if (position.role !== 'value' && position.role !== 'member') return null;
|
|
6019
6812
|
const types = [];
|
|
6020
6813
|
let member = null;
|
|
6021
6814
|
for (let current = node.parent; current; current = current.parent) {
|
|
@@ -6025,12 +6818,37 @@ function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
|
6025
6818
|
if (types.length === 0) return 'review';
|
|
6026
6819
|
const receiver = ref.receiver || null;
|
|
6027
6820
|
if (receiver && receiver !== 'this' && receiver !== 'base') {
|
|
6028
|
-
//
|
|
6029
|
-
|
|
6030
|
-
const
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6821
|
+
// Typed instance method values and static method groups use
|
|
6822
|
+
// the receiver's lookup, never the enclosing class (fix #411).
|
|
6823
|
+
const typed = ref.receiverType;
|
|
6824
|
+
const written = !typed && langTraits(index.files.get(ref.file)?.language)?.methodGroupReceiver?.(node);
|
|
6825
|
+
if (!typed) {
|
|
6826
|
+
if (!written) return 'review';
|
|
6827
|
+
if (member && csharpLocalBinding(member, node, written.name) !== 'none') return 'review';
|
|
6828
|
+
const receiverMembers = (index.symbols.get(receiver) || []).filter(d => d.className &&
|
|
6829
|
+
!d.isConstructor && d.type !== 'constructor' && d.className !== receiver &&
|
|
6830
|
+
!IDENTITY_TYPE_KINDS_FOR_CREF.has(d.type));
|
|
6831
|
+
if (receiverMembers.length > 0) {
|
|
6832
|
+
for (const type of types) {
|
|
6833
|
+
const owner = classRefAt(ref.file, type);
|
|
6834
|
+
if (!owner || findDeclaringClass(index, owner, receiverMembers)) return 'review';
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
const typeName = typed || written.name;
|
|
6839
|
+
const qualifier = typed ? ref.receiverTypeQualifier : written.qualifier;
|
|
6840
|
+
const arity = typed ? ref.receiverTypeArgs : written.arity;
|
|
6841
|
+
if (externalTypeDenotation(index, ref.file, typeName, qualifier || null, ref.line)) return 'review';
|
|
6842
|
+
const typeRef = resolveClassRef(index, typeName, ref.file, {
|
|
6843
|
+
line: ref.line,
|
|
6844
|
+
...(qualifier && { namespace: qualifier }),
|
|
6845
|
+
...(Number.isInteger(arity) && { arity }),
|
|
6846
|
+
});
|
|
6847
|
+
if (!typeRef?.key || !typeRef.def) return 'review';
|
|
6848
|
+
const relation = relationToTargets(index, typeRef, renamedDefs);
|
|
6849
|
+
if (typed && (relation === 'ancestor' || relation === 'maybe')) return 'review';
|
|
6850
|
+
const verdict = verdictOf(findDeclaringClass(index, typeRef, members));
|
|
6851
|
+
return verdict === 'skip' ? null : verdict || (openAncestry(typeRef) ? 'review' : null);
|
|
6034
6852
|
}
|
|
6035
6853
|
if (!receiver && member) {
|
|
6036
6854
|
const local = csharpLocalBinding(member, node, name);
|
|
@@ -6062,11 +6880,9 @@ function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
|
6062
6880
|
trees.set(file, tree);
|
|
6063
6881
|
return tree;
|
|
6064
6882
|
};
|
|
6065
|
-
//
|
|
6066
|
-
//
|
|
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)));
|
|
6883
|
+
// An explicit receiver can name the method too. Unresolved receivers
|
|
6884
|
+
// remain review items instead of falling into simple-name lookup.
|
|
6885
|
+
const candidateRecord = record => record.usageType === 'reference' && !record.isDefinition;
|
|
6070
6886
|
for (const first of usages) {
|
|
6071
6887
|
if (first.usageType !== 'reference' || first.isDefinition) continue;
|
|
6072
6888
|
const abs = first.file;
|
|
@@ -6111,7 +6927,7 @@ function _methodGroupPass(index, name, renameTo, renamedDefs, changes, usages) {
|
|
|
6111
6927
|
line: first.line,
|
|
6112
6928
|
expression: (first.content || index.getLineContent(abs, first.line) || '').trim(),
|
|
6113
6929
|
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`,
|
|
6930
|
+
`(an unresolved receiver, an overload chosen by the delegate type, or a local of that name) and rename it to ${renameTo} if it does`,
|
|
6115
6931
|
needsReview: true,
|
|
6116
6932
|
reviewReason: 'method-group',
|
|
6117
6933
|
editKind: 'reference',
|