ucn 5.5.1 → 5.5.3

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.
Files changed (97) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +40 -18
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +32 -21
  6. package/cli/index.js +2 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/bridge.js +15 -65
  12. package/core/brief.js +8 -4
  13. package/core/build-worker.js +2 -1
  14. package/core/cache.js +137 -6
  15. package/core/call-name-cache.js +134 -0
  16. package/core/callable-scope.js +21 -0
  17. package/core/callers.js +4726 -940
  18. package/core/check.js +22 -5
  19. package/core/class-identity.js +399 -12
  20. package/core/client-receivers.js +189 -0
  21. package/core/constructor-calls.js +831 -0
  22. package/core/constructor-hierarchy.js +147 -0
  23. package/core/constructor-protocol.js +196 -0
  24. package/core/contract-membership.js +346 -140
  25. package/core/cpp-conversion-constraints.js +123 -0
  26. package/core/cpp-conversion-syntax.js +216 -0
  27. package/core/cpp-conversions.js +414 -0
  28. package/core/cpp-scope.js +245 -44
  29. package/core/deadcode.js +61 -16
  30. package/core/decorator-binding.js +137 -3
  31. package/core/enum-reference-syntax.js +134 -0
  32. package/core/execute.js +17 -1
  33. package/core/external-macros.js +42 -10
  34. package/core/go-packages.js +171 -0
  35. package/core/graph-build.js +2 -0
  36. package/core/graph.js +75 -59
  37. package/core/ignore-rules.js +2 -1
  38. package/core/imports.js +145 -19
  39. package/core/index-ir.js +41 -4
  40. package/core/inprocess-clients.js +41 -15
  41. package/core/ir.js +27 -6
  42. package/core/macro-expansion.js +38 -2
  43. package/core/out-of-class-members.js +182 -0
  44. package/core/output/analysis-ext.js +4 -2
  45. package/core/output/analysis.js +89 -20
  46. package/core/output/graph.js +12 -3
  47. package/core/output/lines.js +4 -3
  48. package/core/output/public.js +2 -0
  49. package/core/output/reporting.js +12 -1
  50. package/core/output/shared.js +2 -0
  51. package/core/output/tracing.js +28 -22
  52. package/core/output-budget.js +2 -2
  53. package/core/parallel-build.js +52 -1
  54. package/core/project.js +217 -112
  55. package/core/provenance-constructor-base.js +161 -0
  56. package/core/provenance-facts.js +257 -27
  57. package/core/provenance-overload.js +458 -54
  58. package/core/provenance-type.js +487 -0
  59. package/core/provenance.js +234 -15
  60. package/core/public-command.js +17 -2
  61. package/core/query-bodies.js +159 -0
  62. package/core/reflection.js +270 -2
  63. package/core/rename-token-syntax.js +111 -0
  64. package/core/reporting.js +13 -7
  65. package/core/request-helpers.js +63 -7
  66. package/core/rust-macro-expansion.js +151 -19
  67. package/core/rust-modules.js +1 -1
  68. package/core/rust-names.js +607 -0
  69. package/core/search.js +70 -17
  70. package/core/shared.js +98 -1
  71. package/core/source-memo.js +201 -0
  72. package/core/text-usage-syntax.js +38 -0
  73. package/core/tracing.js +7 -1
  74. package/core/type-denotation.js +124 -37
  75. package/core/type-references.js +615 -246
  76. package/core/value-references.js +2777 -0
  77. package/core/verify.js +1097 -281
  78. package/languages/adapter.js +28 -4
  79. package/languages/c-family.js +1118 -164
  80. package/languages/constructor-bases.js +105 -0
  81. package/languages/cpp-platform.js +26 -0
  82. package/languages/cpp-site-syntax.js +83 -0
  83. package/languages/csharp-scope.js +439 -0
  84. package/languages/csharp.js +428 -68
  85. package/languages/go.js +385 -207
  86. package/languages/html.js +4 -2
  87. package/languages/index.js +397 -2
  88. package/languages/java-platform.js +226 -0
  89. package/languages/java-unicode.js +269 -0
  90. package/languages/java.js +461 -117
  91. package/languages/javascript.js +418 -66
  92. package/languages/lexical-scope.js +190 -23
  93. package/languages/python.js +404 -54
  94. package/languages/rust.js +510 -82
  95. package/languages/utils.js +67 -8
  96. package/mcp/server.js +8 -2
  97. package/package.json +3 -3
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 } = require('./shared');
11
- const { classDefsNamed, distinctOwnerDefinitions } = require('./class-identity');
12
- const { isTypeRenamePin } = require('./type-references');
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
- const tree = parser && (index._getParsedTree?.(absolute, content, language) ||
159
- safeParse(parser, content));
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 language = index.files.get(absolute)?.language;
260
- const parser = language && getParser(language);
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
- const tree = index._getParsedTree?.(absolute, content, language) || safeParse(parser, content);
265
- if (!tree) return null;
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 isName = n => n && n.text === name && /identifier$/.test(n.type) &&
370
+ const isPlainName = n => n && n.text === name && /identifier$/.test(n.type) &&
277
371
  n.startPosition.row === line - 1;
278
- if (isName(node)) return node.startPosition.column;
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.startPosition.column;
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.startPosition.column;
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
- let renamed = prior.rawSource;
308
- for (const column of columns) {
309
- if (renamed.slice(column, column + prior.oldName.length) !== prior.oldName) continue;
310
- renamed = renamed.slice(0, column) + prior.newName + renamed.slice(column + prior.oldName.length);
311
- }
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
- const paramTypes = def.paramTypes || {};
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 = detectLanguage(def.file);
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 (constructors are deliberately not
857
- * indexed as members there). Returns an array of paramsStructured lists —
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)) lists.push(d.paramsStructured);
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
- lists.push(d.paramsStructured);
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 siteKey = site => `${site.file}\0${site.line}`;
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 => [siteKey(entry), entry.reason]));
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(key)) continue;
1423
+ if (unverifiedKeys.has(key) ? boundBefore !== 'confirmed' : !reasonAt.has(lineKey(site))) continue;
1271
1424
  seen.add(key);
1272
- const calls = (getCachedCalls(index, site.file) || []).filter(call =>
1273
- call.line === site.line && (call.name === name || call.resolvedName === name));
1274
- const call = calls.find(c => site.column == null || c.column === site.column) || calls[0];
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(key) || null,
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
- return {
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
- * BUG-BW: Build the list of call sites for `plan` using the SAME sweep verify
1340
- * uses. This guarantees plan and verify agree on which sites need updating.
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(..)` spells no name (fix #400): a rename never
1409
- // edits it, so an undecided one is no rename candidate either.
1410
- const renameRelevant = u => u.macroExpansion?.origin !== 'template' && !u.targetTyped;
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))) return null;
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 = detectLanguage(call.file);
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 callNode = findCallNode(tree.rootNode, callTypes, targetRow, spelling, occurrence,
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 = callNode.childForFieldName('arguments');
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 = detectLanguage(filePath);
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
- function importStatementLine(index, filePath, line, column) {
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
- const content = index._readFile(filePath);
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
- let node = tree?.rootNode?.descendantForPosition({ row: line - 1, column });
2477
- while (node && !IMPORT_STATEMENT_TYPES.has(node.type)) node = node.parent;
2478
- return node ? node.startPosition.row + 1 : line;
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
- const lineEdit = (raw, columns) => {
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
- let renamed = raw;
2581
- for (const column of sorted) {
2582
- if (renamed.slice(column, column + name.length) !== name) continue;
2583
- renamed = renamed.slice(0, column) + renameTo + renamed.slice(column + name.length);
2584
- }
2919
+ const aliased = aliases.filter(column => sorted.includes(column));
2585
2920
  return {
2586
- source: raw.trim(), renamed: renamed.trim(), count: sorted.length,
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' : 'Update type reference'}: ${edit.renamed}`,
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 type reference: ${edit.renamed}${reviewNote}`,
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
- if (options.renameTo && def.className === def.name && !isTypeRenamePin(def) &&
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
- const owner = definitions.find(d => isTypeRenamePin(d) && d.file === def.file &&
2937
- d.startLine <= def.startLine && d.endLine >= def.endLine);
2938
- if (owner) def = owner;
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 } = computePlanCallSites(index, name, def,
2965
- { analyzeArgs: !options.renameTo });
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
- const selfNames = ['self', 'cls', '&self', '&mut self', 'mut self'];
3047
- const minIdx = (newParams.length > 0 && selfNames.includes(newParams[0].name)) ? 1 : 0;
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.map(formatTypedParam).filter(Boolean).join(', ');
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 && site.keywordArgNames?.length > 0;
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.map(formatTypedParam).filter(Boolean).join(', ');
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(..)` spells no name: the type written at
3249
- // its declaration site is what the rename edits (fix #393).
3250
- if (incoming.targetTyped) continue;
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: true,
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)) return false;
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
- const importKeepsItemName = (filePath, line) => {
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
- return itemsNamed(index, filePath, name).some(item =>
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, imp.file && importKeepsItemName(imp.file, imp.line)
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
- changes.push(tagTokenEdit({
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
- changes.push({
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 { _cFamilySignatureKey, _isCrossFileConfigurationMember, _sameOwnerPath,
3738
- _isConfigurationAlternative } = require('./callers');
3739
- const hasCfg = d => (d.attributesWithArgs || []).some(a => a.name === 'cfg') ||
3740
- (d.modifiers || []).includes('cfg');
3741
- const alternatives = definitions.filter(other => other !== def && ((
3742
- // Runtime configuration items (fix #398F, Python): the
3743
- // parser-recorded alternatives of the item, whatever their
3744
- // parameters.
3745
- conditionalStyle === 'branch' ? _isConfigurationAlternative(index, def, other) :
3746
- other.file === def.file && !other.isSignature &&
3747
- !NON_CALLABLE_TYPES.has(other.type) &&
3748
- (other.className || null) === (def.className || null) &&
3749
- (other.traitName || null) === (def.traitName || null) &&
3750
- (other.namespace || null) === (def.namespace || null) &&
3751
- (other.explicitInterface || null) === (def.explicitInterface || null) &&
3752
- !(other.startLine === def.startLine && other.endLine === def.endLine) &&
3753
- // Members of two same-name classes declared in different
3754
- // scopes (a function-local class) are two items (fix #389).
3755
- !distinctOwnerDefinitions(index, other, def) &&
3756
- // One full owner path (fix #395): same-name nested types of
3757
- // two outer classes are two items.
3758
- _sameOwnerPath(index, other, def) &&
3759
- _cFamilySignatureKey(other) === _cFamilySignatureKey(def) &&
3760
- (conditionalStyle !== 'attribute' || hasCfg(def) || hasCfg(other))) ||
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 changedLines = new Set(changes.map(change => `${change.file}:${change.line}`));
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 { adapter = getLanguageAdapter(planLang); } catch { adapter = null; }
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
- changes.push(tagTokenEdit({
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
- `${site.file}:${site.line}:${Number.isInteger(site.column) ? site.column : '*'}`;
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
- !changedLines.has(`${site.file}:${site.line}`));
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 changedLines = new Set(changes.map(change => `${change.file}:${change.line}`));
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) continue;
4502
- if (!best || (range.end - range.start) < (best.end - best.start)) {
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
- const pinIdentity = new Set(require('./callers')._closeCallableIdentityGroup(
4512
- index, [def], definitions));
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
- return Array.isArray(ref.scopeDefLines) &&
4609
- ref.scopeDefLines.includes(pinNameLine) ? 'edit' : null;
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
- if (!winner || size < winner.size) {
4622
- winner = { def: binder.def, size };
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
- if (verdict === 'edit' && ref.file === def.file && def.className) {
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
- changes.push({
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
- const accessorReferences = findAccessorReferences(index, name, def, {
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) Object.assign(existing, concrete);
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 = renameIdentifierTokens(index, def.file,
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 = renameIdentifierTokens(index, def.file,
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 = detectLanguage(filePath);
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
- // `Host.Build`: a static method group of the named class.
6029
- if (!/^[A-Z]/.test(receiver)) return null;
6030
- const typeRef = resolveClassRef(index, receiver, ref.file, { line: ref.line });
6031
- if (!typeRef?.key) return typeRef?.external ? null : 'review';
6032
- return verdictOf(findDeclaringClass(index, typeRef, members)) === 'edit' ? 'edit'
6033
- : verdictOf(findDeclaringClass(index, typeRef, members)) === 'review' ? 'review' : null;
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
- // Only simple names and `this.`/`base.`/type-qualified accesses can name
6066
- // the method group; other member accesses are never parsed for.
6067
- const candidateRecord = record => record.usageType === 'reference' && !record.isDefinition &&
6068
- (!record.receiver || record.receiver === 'this' || record.receiver === 'base' ||
6069
- /^[A-Z]/.test(String(record.receiver)));
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',