ucn 5.4.1 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +42 -16
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. package/package.json +1 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
9
+ const { referenceScope, scopeFields } = require('./lexical-scope');
9
10
 
10
11
 
11
12
  const {
@@ -19,6 +20,7 @@ const {
19
20
  buildTypeAnnotations,
20
21
  visitNameNodes,
21
22
  sameNode,
23
+ parseErrorRegions,
22
24
  } = require('./utils');
23
25
  const { PARSE_OPTIONS, safeParse } = require('./index');
24
26
 
@@ -65,6 +67,23 @@ function aliasBaseTypeName(typeNode) {
65
67
  return null;
66
68
  }
67
69
 
70
+ /** Whether a call's parent, through parentheses, is of `parentType` (fix #402). */
71
+ function directlyAwaited(node, awaitType) {
72
+ let parent = node.parent;
73
+ while (parent?.type === 'parenthesized_expression') parent = parent.parent;
74
+ return parent?.type === awaitType;
75
+ }
76
+
77
+ /** Whether a function/arrow node carries the `async` keyword token (fix #402). */
78
+ function hasAsyncToken(node) {
79
+ for (let i = 0; i < (node?.childCount || 0); i++) {
80
+ const child = node.child(i);
81
+ if (child.type === 'async') return true;
82
+ if (child.isNamed) return false;
83
+ }
84
+ return false;
85
+ }
86
+
68
87
  /**
69
88
  * Check if function is a generator
70
89
  * @param {object} node - Function node
@@ -103,6 +122,287 @@ function getAssignmentName(leftNode) {
103
122
  return null;
104
123
  }
105
124
 
125
+ // Names of the global object itself: a property assigned on one of them
126
+ // (unshadowed) is a global binding that bare names resolve to.
127
+ const JS_GLOBAL_OBJECT_NAMES = new Set(['globalThis', 'window', 'self', 'global']);
128
+
129
+ const FUNCTION_NODE_TYPES = new Set([
130
+ 'function_declaration', 'function_expression', 'function', 'arrow_function',
131
+ 'method_definition', 'generator_function', 'generator_function_declaration',
132
+ ]);
133
+
134
+ function unwrapValueExpression(node) {
135
+ while (node && (node.type === 'parenthesized_expression' || node.type === 'as_expression' ||
136
+ node.type === 'non_null_expression' || node.type === 'satisfies_expression' ||
137
+ node.type === 'type_assertion')) {
138
+ node = node.type === 'type_assertion'
139
+ ? node.namedChild(node.namedChildCount - 1) : node.namedChild(0);
140
+ }
141
+ return node;
142
+ }
143
+
144
+ /** Names a binding pattern declares (identifiers, shorthand, rest, defaults' targets). */
145
+ function collectPatternNames(pattern, out) {
146
+ if (!pattern) return;
147
+ if (pattern.type === 'required_parameter' || pattern.type === 'optional_parameter') {
148
+ collectPatternNames(pattern.childForFieldName('pattern') || pattern.childForFieldName('name'), out);
149
+ return;
150
+ }
151
+ if (pattern.type === 'identifier' || pattern.type === 'shorthand_property_identifier_pattern') {
152
+ out.push(pattern.text);
153
+ return;
154
+ }
155
+ if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
156
+ collectPatternNames(pattern.childForFieldName('value'), out);
157
+ return;
158
+ }
159
+ if (pattern.type === 'assignment_pattern') {
160
+ collectPatternNames(pattern.childForFieldName('left') || pattern.childForFieldName('pattern'), out);
161
+ return;
162
+ }
163
+ if (pattern.type === 'type_annotation') return;
164
+ for (let i = 0; i < pattern.namedChildCount; i++) collectPatternNames(pattern.namedChild(i), out);
165
+ }
166
+
167
+ // Module-level alias: findCallsInCode declares its own one-argument
168
+ // collectPatternNames for module value bindings.
169
+ const collectBoundPatternNames = collectPatternNames;
170
+
171
+ // Function kinds whose parameters bind in their own range (fix #397).
172
+ const JS_SHADOW_FUNCTIONS = new Set(['arrow_function', 'function_expression', 'function_declaration',
173
+ 'function', 'method_definition', 'generator_function', 'generator_function_declaration']);
174
+
175
+ /** A declarator value that is a require()/import() (through await, parens, `.member`). */
176
+ function isImportInitializerNode(value) {
177
+ let v = value;
178
+ for (;;) {
179
+ if (!v) return false;
180
+ if (v.type === 'await_expression' || v.type === 'parenthesized_expression') {
181
+ v = v.namedChild(0);
182
+ continue;
183
+ }
184
+ if (v.type === 'member_expression' || v.type === 'subscript_expression') {
185
+ v = v.childForFieldName('object');
186
+ continue;
187
+ }
188
+ break;
189
+ }
190
+ if (v.type !== 'call_expression') return false;
191
+ const fn = v.childForFieldName('function');
192
+ return !!fn && (fn.type === 'import' || (fn.type === 'identifier' && fn.text === 'require'));
193
+ }
194
+
195
+ /** Record the bindings one statement introduces into `table` (first wins). */
196
+ function collectStatementBindings(stmt, table) {
197
+ const add = (name, binding) => { if (name && !table.has(name)) table.set(name, binding); };
198
+ if (stmt.type === 'export_statement') {
199
+ const declaration = stmt.childForFieldName('declaration');
200
+ if (declaration) collectStatementBindings(declaration, table);
201
+ return;
202
+ }
203
+ if (stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') {
204
+ for (let i = 0; i < stmt.namedChildCount; i++) {
205
+ const declarator = stmt.namedChild(i);
206
+ if (declarator.type !== 'variable_declarator') continue;
207
+ const target = declarator.childForFieldName('name');
208
+ const start = stmt.startIndex;
209
+ const line = stmt.startPosition.row + 1;
210
+ if (target?.type === 'identifier') {
211
+ add(target.text, { kind: 'declarator', value: declarator.childForFieldName('value'), declarator,
212
+ start, line });
213
+ } else {
214
+ const names = [];
215
+ collectPatternNames(target, names);
216
+ for (const name of names) add(name, { kind: 'pattern', declarator, start, line });
217
+ }
218
+ }
219
+ return;
220
+ }
221
+ if (stmt.type === 'function_declaration' || stmt.type === 'generator_function_declaration' ||
222
+ stmt.type === 'class_declaration') {
223
+ add(stmt.childForFieldName('name')?.text,
224
+ { kind: stmt.type === 'class_declaration' ? 'class' : 'function', line: stmt.startPosition.row + 1 });
225
+ return;
226
+ }
227
+ if (stmt.type === 'import_statement') {
228
+ for (const clause of stmt.namedChildren) {
229
+ if (clause.type !== 'import_clause') continue;
230
+ for (const part of clause.namedChildren) {
231
+ if (part.type === 'identifier') add(part.text, { kind: 'import' });
232
+ else if (part.type === 'namespace_import') {
233
+ for (const child of part.namedChildren) {
234
+ if (child.type === 'identifier') add(child.text, { kind: 'import' });
235
+ }
236
+ } else if (part.type === 'named_imports') {
237
+ for (const spec of part.namedChildren) {
238
+ const local = spec.childForFieldName('alias') || spec.childForFieldName('name');
239
+ if (local) add(local.text, { kind: 'import' });
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ // Statement-list binding tables per tree, by node id: one scan per list
248
+ // however many member assignments look names up in it.
249
+ const statementListTables = new WeakMap();
250
+
251
+ function statementListBindings(listNode) {
252
+ let byId = statementListTables.get(listNode.tree);
253
+ if (!byId) {
254
+ byId = new Map();
255
+ statementListTables.set(listNode.tree, byId);
256
+ }
257
+ let table = byId.get(listNode.id);
258
+ if (!table) {
259
+ table = new Map();
260
+ for (let i = 0; i < listNode.namedChildCount; i++) {
261
+ collectStatementBindings(listNode.namedChild(i), table);
262
+ }
263
+ byId.set(listNode.id, table);
264
+ }
265
+ return table;
266
+ }
267
+
268
+ /**
269
+ * The declaration of `name` visible at `refNode`: statement-list bindings
270
+ * (hoisted declarations anywhere in the list), loop and catch bindings,
271
+ * parameters and a function expression's own name; null when free.
272
+ */
273
+ function declarationVisibleAt(refNode, name) {
274
+ for (let p = refNode.parent; p; p = p.parent) {
275
+ if (p.type === 'statement_block' || p.type === 'program' || p.type === 'class_static_block') {
276
+ const binding = statementListBindings(p).get(name);
277
+ if (binding) return binding;
278
+ } else if (p.type === 'for_statement' || p.type === 'for_in_statement') {
279
+ const head = p.childForFieldName('initializer') || p.childForFieldName('left');
280
+ if (head) {
281
+ const names = [];
282
+ if (head.type === 'lexical_declaration' || head.type === 'variable_declaration') {
283
+ const table = new Map();
284
+ collectStatementBindings(head, table);
285
+ if (table.has(name)) return { kind: 'loop' };
286
+ } else {
287
+ collectPatternNames(head, names);
288
+ if (names.includes(name)) return { kind: 'loop' };
289
+ }
290
+ }
291
+ } else if (p.type === 'catch_clause') {
292
+ const names = [];
293
+ collectPatternNames(p.childForFieldName('parameter'), names);
294
+ if (names.includes(name)) return { kind: 'param' };
295
+ } else if (FUNCTION_NODE_TYPES.has(p.type)) {
296
+ const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
297
+ if (params) {
298
+ const names = [];
299
+ collectPatternNames(params, names);
300
+ if (names.includes(name)) return { kind: 'param' };
301
+ }
302
+ if ((p.type === 'function_expression' || p.type === 'function' ||
303
+ p.type === 'generator_function') && p.childForFieldName('name')?.text === name) {
304
+ return { kind: 'function' };
305
+ }
306
+ } else if (p.type === 'class' && p.childForFieldName('name')?.text === name) {
307
+ return { kind: 'class' };
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+
313
+ /** A value that is a fresh or module object, never the global object. */
314
+ function provablyNonGlobalValue(valueNode) {
315
+ let value = unwrapValueExpression(valueNode);
316
+ while (value?.type === 'assignment_expression') {
317
+ value = unwrapValueExpression(value.childForFieldName('right'));
318
+ }
319
+ if (!value) return false;
320
+ if (['new_expression', 'object', 'array', 'class', 'function_expression', 'function',
321
+ 'arrow_function', 'generator_function'].includes(value.type)) return true;
322
+ const callee = value.type === 'call_expression' ? value.childForFieldName('function') : null;
323
+ return callee?.type === 'identifier' && callee.text === 'require';
324
+ }
325
+
326
+ /**
327
+ * Whether `this` at a node is an object other than the global object: a
328
+ * class member body, or a function installed as a method (object literal
329
+ * value, member assignment). A plain function's `this` can be the global
330
+ * object (a sloppy-mode plain call), as can module-level `this` in a script.
331
+ */
332
+ function thisIsNonGlobalObject(thisNode) {
333
+ for (let p = thisNode.parent; p; p = p.parent) {
334
+ if (p.type === 'arrow_function') continue;
335
+ if (p.type === 'method_definition' || p.type === 'class_body' ||
336
+ p.type === 'class_static_block') return true;
337
+ if (p.type === 'function_expression' || p.type === 'function' ||
338
+ p.type === 'generator_function') {
339
+ const holder = p.parent;
340
+ if (holder?.type === 'pair' && sameNode(holder.childForFieldName('value'), p)) return true;
341
+ return holder?.type === 'assignment_expression' &&
342
+ sameNode(holder.childForFieldName('right'), p) &&
343
+ holder.childForFieldName('left')?.type === 'member_expression';
344
+ }
345
+ if (p.type === 'function_declaration' || p.type === 'generator_function_declaration' ||
346
+ p.type === 'program') return false;
347
+ }
348
+ return false;
349
+ }
350
+
351
+ /**
352
+ * What the object of a member assignment (`obj.f = function () {}`) is for
353
+ * bare-name lookup: 'global' when it is the global object (the member is a
354
+ * global binding), 'object' when it is provably another object (the member
355
+ * creates no name a bare call can reach), null when it may be either (a
356
+ * parameter, a plain function's `this`, a value UCN does not follow).
357
+ */
358
+ function classifyAssignedObject(objectNode) {
359
+ const node = unwrapValueExpression(objectNode);
360
+ if (!node) return null;
361
+ if (node.type === 'this') return thisIsNonGlobalObject(node) ? 'object' : null;
362
+ if (node.type === 'identifier') {
363
+ const declaration = declarationVisibleAt(node, node.text);
364
+ if (!declaration) return JS_GLOBAL_OBJECT_NAMES.has(node.text) ? 'global' : 'object';
365
+ if (declaration.kind === 'function' || declaration.kind === 'class' ||
366
+ declaration.kind === 'import') return 'object';
367
+ if (declaration.kind === 'declarator' && provablyNonGlobalValue(declaration.value)) return 'object';
368
+ // `var global = globalThis` (fix #397, handlebars-measured): a
369
+ // binding of the global object itself, never assigned again, names
370
+ // the global object.
371
+ if (declaration.kind === 'declarator' && declaration.declarator &&
372
+ globalObjectAlias(declaration.declarator, node.text)) return 'global';
373
+ return null;
374
+ }
375
+ if (node.type === 'member_expression' || node.type === 'subscript_expression') {
376
+ // A property value is the global object only through a global
377
+ // alias of the global object itself (`window.self`).
378
+ const property = node.type === 'member_expression'
379
+ ? node.childForFieldName('property')?.text : null;
380
+ if (property && JS_GLOBAL_OBJECT_NAMES.has(property) &&
381
+ classifyAssignedObject(node.childForFieldName('object')) === 'global') return 'global';
382
+ return 'object';
383
+ }
384
+ return null;
385
+ }
386
+
387
+ /**
388
+ * Whether a declarator binds the global object and nothing assigns the name
389
+ * again in its scope: `var global = globalThis`, `const root = window`.
390
+ */
391
+ function globalObjectAlias(declarator, name) {
392
+ const value = unwrapValueExpression(declarator.childForFieldName('value'));
393
+ if (value?.type !== 'identifier' || !JS_GLOBAL_OBJECT_NAMES.has(value.text) ||
394
+ declarationVisibleAt(value, value.text)) return false;
395
+ const statement = declarator.parent;
396
+ const scope = statement?.parent;
397
+ if (!scope) return false;
398
+ if (statement.type === 'lexical_declaration' && statement.child(0)?.type === 'const') return true;
399
+ for (const assignment of scope.descendantsOfType(['assignment_expression', 'augmented_assignment_expression'])) {
400
+ const left = assignment.childForFieldName('left');
401
+ if (left?.type === 'identifier' && left.text === name) return false;
402
+ }
403
+ return true;
404
+ }
405
+
106
406
  /** True when a declaration is outside every function/class body. */
107
407
  function isModuleScope(node) {
108
408
  let current = node && node.parent;
@@ -174,6 +474,16 @@ function appendObjectFunctionMembers(objectNode, functions, lines, extraFields =
174
474
  }
175
475
  const name = objectPropertyName(nameNode);
176
476
  if (!name || !fnNode) continue;
477
+ // `get name() {}` / `set name(v) {}` in the literal: one property
478
+ // accessed by reads and writes (fix #397).
479
+ let accessorKind = null;
480
+ if (prop.type === 'method_definition') {
481
+ for (let c = 0; c < prop.childCount; c++) {
482
+ const token = prop.child(c);
483
+ if (nameNode && token.startIndex >= nameNode.startIndex) break;
484
+ if (token.type === 'get' || token.type === 'set') accessorKind = token.type;
485
+ }
486
+ }
177
487
 
178
488
  const paramsNode = fnNode.childForFieldName('parameters');
179
489
  const { startLine, endLine, indent } = nodeToLocation(prop, lines);
@@ -196,6 +506,16 @@ function appendObjectFunctionMembers(objectNode, functions, lines, extraFields =
196
506
  modifiers,
197
507
  memberAssigned: true,
198
508
  registryMember: true,
509
+ // The literal's own line: members of one object literal share it
510
+ // (fix #397: `this` in a member names that object).
511
+ objectLiteralLine: objectNode.startPosition.row + 1,
512
+ ...(accessorKind && { memberType: accessorKind }),
513
+ // An object literal member is a property of a fresh object: no
514
+ // bare name reaches it, except a self-named function
515
+ // expression's own name inside its body (fix #384).
516
+ assignedObject: 'object',
517
+ ...(fnNode.type !== 'method_definition' &&
518
+ fnNode.childForFieldName('name')?.text === name && { selfNamed: true }),
199
519
  ...extraFields,
200
520
  ...typeAnno,
201
521
  ...(generics && { generics }),
@@ -769,8 +1089,16 @@ function _processFunction(node, functions, processedRanges, lines) {
769
1089
  if (!isArrow && !isFnExpr && isModuleScope(node)) {
770
1090
  const registryObject = unwrapObjectRegistry(valueNode);
771
1091
  if (registryObject) {
1092
+ // The container's declared type (`const h: Handler =
1093
+ // {...}`) makes each member a structural contract
1094
+ // member of that type (fix #360: plan rename
1095
+ // closure over interface slots).
1096
+ const containerTypeNode = declarator.childForFieldName('type');
1097
+ const containerType = containerTypeNode
1098
+ ? containerTypeNode.text.replace(/^:\s*/, '').trim() : null;
772
1099
  const added = appendObjectFunctionMembers(registryObject, functions, lines, {
773
1100
  registryContainer: nameNode.text,
1101
+ ...(containerType && { registryContainerType: containerType }),
774
1102
  });
775
1103
  if (added > 0) processedRanges.add(rangeKey);
776
1104
  }
@@ -849,6 +1177,7 @@ function _processFunction(node, functions, processedRanges, lines) {
849
1177
  const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
850
1178
  const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
851
1179
 
1180
+ const isAsync = hasAsyncToken(rightNode);
852
1181
  functions.push({
853
1182
  name,
854
1183
  params: extractParams(paramsNode),
@@ -858,7 +1187,8 @@ function _processFunction(node, functions, processedRanges, lines) {
858
1187
  indent,
859
1188
  isArrow,
860
1189
  isGenerator: isGen,
861
- modifiers: isCommonJsDefault ? ['export'] : [],
1190
+ ...(isAsync && { isAsync }),
1191
+ modifiers: [...(isCommonJsDefault ? ['export'] : []), ...(isAsync ? ['async'] : [])],
862
1192
  // A property-assignment def (Reply.prototype.serialize
863
1193
  // = function, exports.h = () => ...) creates NO
864
1194
  // lexical name — a bare call in the file can never
@@ -875,6 +1205,18 @@ function _processFunction(node, functions, processedRanges, lines) {
875
1205
  ...(leftNode.type === 'member_expression' &&
876
1206
  leftNode.childForFieldName('object')?.type === 'identifier' &&
877
1207
  { assignedReceiver: leftNode.childForFieldName('object').text }),
1208
+ // Bare-name reachability of the member (fix #384): a
1209
+ // bare call resolves lexically, then globally, so it
1210
+ // reaches the member only when the object is the
1211
+ // global object, or inside a self-named function
1212
+ // expression's own body (ECMA-262 binds that name
1213
+ // there).
1214
+ ...(leftNode.type === 'member_expression' && (() => {
1215
+ const kind = classifyAssignedObject(leftNode.childForFieldName('object'));
1216
+ return kind ? { assignedObject: kind } : {};
1217
+ })()),
1218
+ ...(leftNode.type === 'member_expression' && expressionName === name &&
1219
+ { selfNamed: true }),
878
1220
  ...(prototypeOwner && { className: prototypeOwner, isMethod: true }),
879
1221
  ...typeAnno,
880
1222
  ...(generics && { generics }),
@@ -920,6 +1262,7 @@ function _processFunction(node, functions, processedRanges, lines) {
920
1262
  const paramsStructured = parseStructuredParams(paramsNode, 'javascript');
921
1263
  const typeAnno = buildTypeAnnotations(paramsStructured, returnType, lines, startLine, true);
922
1264
 
1265
+ const isAsync = hasAsyncToken(child);
923
1266
  functions.push({
924
1267
  name: 'default',
925
1268
  params: extractParams(paramsNode),
@@ -929,7 +1272,8 @@ function _processFunction(node, functions, processedRanges, lines) {
929
1272
  indent,
930
1273
  isArrow: child.type === 'arrow_function',
931
1274
  isGenerator: isGen,
932
- modifiers: ['export', 'default'],
1275
+ ...(isAsync && { isAsync }),
1276
+ modifiers: isAsync ? ['export', 'async', 'default'] : ['export', 'default'],
933
1277
  ...typeAnno,
934
1278
  ...(generics && { generics }),
935
1279
  ...(docstring && { docstring })
@@ -994,7 +1338,10 @@ function _processClass(node, classes, processedRanges, lines) {
994
1338
  ...(extendsInfo && { extends: extendsInfo }),
995
1339
  ...(implementsInfo.length > 0 && { implements: implementsInfo }),
996
1340
  ...(decorators.length > 0 && { decorators }),
997
- ...(decoratorsWithArgs.some(d => d.firstStringArg) && { decoratorsWithArgs })
1341
+ ...(decoratorsWithArgs.some(d => d.firstStringArg) && { decoratorsWithArgs }),
1342
+ // A class declared in a function body is visible only there
1343
+ // (fix #378).
1344
+ ...(node.type !== 'class' && lexicalOwnerRange(node)),
998
1345
  });
999
1346
  }
1000
1347
  return true;
@@ -1473,6 +1820,10 @@ function extractClassMembers(classNode, codeOrLines) {
1473
1820
  if (nameNode) {
1474
1821
  const { startLine, endLine } = nodeToLocation(child, code);
1475
1822
  const name = nameNode.text;
1823
+ // fix #390: a TS decorator is part of the field node; the
1824
+ // name's line is where definition edits go.
1825
+ const fieldNameLine = nameNode.startPosition.row + 1 !== startLine
1826
+ ? { nameLine: nameNode.startPosition.row + 1 } : {};
1476
1827
  const valueNode = child.childForFieldName('value');
1477
1828
  const isArrow = valueNode && valueNode.type === 'arrow_function';
1478
1829
  const isStatic = Array.from({ length: child.childCount }, (_, ci) => child.child(ci))
@@ -1514,6 +1865,7 @@ function extractClassMembers(classNode, codeOrLines) {
1514
1865
  paramsStructured,
1515
1866
  startLine,
1516
1867
  endLine,
1868
+ ...fieldNameLine,
1517
1869
  memberType: name.startsWith('#') ? 'private' : 'field',
1518
1870
  ...(isStatic && { modifiers: ['static'] }),
1519
1871
  isArrow: true,
@@ -1540,6 +1892,7 @@ function extractClassMembers(classNode, codeOrLines) {
1540
1892
  name,
1541
1893
  startLine,
1542
1894
  endLine,
1895
+ ...fieldNameLine,
1543
1896
  memberType: name.startsWith('#') ? 'private field' : 'field',
1544
1897
  ...(isStatic && { modifiers: ['static'] }),
1545
1898
  ...(fieldType && { fieldType }),
@@ -1663,6 +2016,70 @@ function findStateObjects(code, parser) {
1663
2016
  * @param {object} parser - Tree-sitter parser instance
1664
2017
  * @returns {ParseResult}
1665
2018
  */
2019
+ /**
2020
+ * Module-level `const` bindings of another name (fix #389): `const Alias =
2021
+ * Box;`, `export const Alias = models.Box;`. A const is bound once; query
2022
+ * time decides whether the target is a class (then `new Alias()` makes a
2023
+ * Box).
2024
+ */
2025
+ function jsModuleValueAliases(root) {
2026
+ const aliases = [];
2027
+ const mutable = [];
2028
+ const declared = new Map();
2029
+ for (let statement of root.namedChildren) {
2030
+ if (statement.type === 'export_statement') {
2031
+ statement = statement.childForFieldName('declaration') ||
2032
+ statement.namedChildren.find(child => child.type === 'lexical_declaration');
2033
+ }
2034
+ if (statement?.type !== 'lexical_declaration' && statement?.type !== 'variable_declaration') continue;
2035
+ const isConst = statement.children.some(child => child.type === 'const');
2036
+ for (const declarator of statement.namedChildren) {
2037
+ if (declarator.type !== 'variable_declarator') continue;
2038
+ const nameNode = declarator.childForFieldName('name');
2039
+ if (nameNode?.type === 'identifier') declared.set(nameNode.text, (declared.get(nameNode.text) || 0) + 1);
2040
+ let value = declarator.childForFieldName('value');
2041
+ if (declarator.childForFieldName('type')) continue;
2042
+ while (value?.type === 'parenthesized_expression') value = value.namedChild(0);
2043
+ if (nameNode?.type !== 'identifier' || !value) continue;
2044
+ if ((value.type === 'identifier' || value.type === 'member_expression') &&
2045
+ /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(value.text) &&
2046
+ value.text !== nameNode.text) {
2047
+ const alias = { name: nameNode.text, target: value.text,
2048
+ line: declarator.startPosition.row + 1 };
2049
+ if (isConst) aliases.push(alias);
2050
+ else mutable.push(alias);
2051
+ }
2052
+ }
2053
+ }
2054
+ if (mutable.length > 0) {
2055
+ // A `let`/`var` alias is the class only while nothing assigns the
2056
+ // name again (fix #392): any write to that identifier anywhere in the
2057
+ // module, or a second declaration, leaves it unbound.
2058
+ const written = new Set();
2059
+ const writeTargets = (node) => {
2060
+ if (!node) return;
2061
+ if (node.type === 'identifier') { written.add(node.text); return; }
2062
+ if (/pattern$/.test(node.type) || node.type === 'pair_pattern' ||
2063
+ node.type === 'shorthand_property_identifier_pattern') {
2064
+ if (node.type === 'shorthand_property_identifier_pattern') written.add(node.text);
2065
+ for (const child of node.namedChildren) writeTargets(child);
2066
+ }
2067
+ };
2068
+ for (const node of root.descendantsOfType(['assignment_expression',
2069
+ 'augmented_assignment_expression', 'update_expression', 'for_in_statement'])) {
2070
+ if (node.type === 'update_expression') writeTargets(node.namedChild(0));
2071
+ else if (node.type === 'for_in_statement') {
2072
+ if (!node.childForFieldName('kind')) writeTargets(node.childForFieldName('left'));
2073
+ } else writeTargets(node.childForFieldName('left'));
2074
+ }
2075
+ for (const alias of mutable) {
2076
+ if (!written.has(alias.name) && declared.get(alias.name) === 1) aliases.push(alias);
2077
+ }
2078
+ aliases.sort((a, b) => a.line - b.line);
2079
+ }
2080
+ return aliases;
2081
+ }
2082
+
1666
2083
  function parse(code, parser) {
1667
2084
  const tree = parseTree(parser, code);
1668
2085
  const lines = code.split('\n');
@@ -1774,6 +2191,7 @@ function parse(code, parser) {
1774
2191
  classes.sort((a, b) => a.startLine - b.startLine);
1775
2192
  stateObjects.sort((a, b) => a.startLine - b.startLine);
1776
2193
  callableAliases.sort((a, b) => a.startLine - b.startLine);
2194
+ const moduleValueAliases = jsModuleValueAliases(tree.rootNode);
1777
2195
 
1778
2196
  return {
1779
2197
  language: 'javascript',
@@ -1782,7 +2200,8 @@ function parse(code, parser) {
1782
2200
  classes,
1783
2201
  stateObjects,
1784
2202
  callableAliases,
1785
- ...(tree.rootNode.hasError && { parseRecovery: true }),
2203
+ ...(moduleValueAliases.length > 0 && { moduleValueAliases }),
2204
+ ...(tree.rootNode.hasError && { parseRecovery: true, parseErrorRegions: parseErrorRegions(tree.rootNode) }),
1786
2205
  imports: [], // Handled by core/imports.js
1787
2206
  exports: [] // Handled by core/imports.js
1788
2207
  };
@@ -1912,11 +2331,36 @@ function tsArrayElement(node) {
1912
2331
  !(child.type === 'literal_type' && ['null', 'undefined'].includes(child.text)));
1913
2332
  return present.length === 1 ? tsArrayElement(present[0]) : null;
1914
2333
  }
1915
- if (node.type !== 'array_type') return null;
1916
- const item = node.namedChild(0);
2334
+ let item = null;
2335
+ let keyed = false;
2336
+ if (node.type === 'array_type') {
2337
+ item = node.namedChild(0);
2338
+ } else if (node.type === 'generic_type') {
2339
+ // Declared container slots (fix #359): Array<T> / ReadonlyArray<T>
2340
+ // index to T; Record<K, V> keys to V.
2341
+ const base = node.childForFieldName('name') || node.namedChild(0);
2342
+ const argsNode = node.childForFieldName('type_arguments') ||
2343
+ node.namedChildren.find(child => child.type === 'type_arguments');
2344
+ const args = argsNode?.namedChildren || [];
2345
+ if (base?.type === 'type_identifier' &&
2346
+ ['Array', 'ReadonlyArray'].includes(base.text) && args.length === 1) {
2347
+ item = args[0];
2348
+ } else if (base?.type === 'type_identifier' && base.text === 'Record' &&
2349
+ args.length === 2) {
2350
+ item = args[1];
2351
+ keyed = true;
2352
+ }
2353
+ } else if (node.type === 'object_type' && node.namedChildCount === 1 &&
2354
+ node.namedChild(0).type === 'index_signature') {
2355
+ // `{ [key: string]: T }` keys to T.
2356
+ const signature = node.namedChild(0);
2357
+ const valueType = signature.childForFieldName('type');
2358
+ item = valueType?.type === 'type_annotation' ? valueType.namedChild(0) : valueType;
2359
+ keyed = true;
2360
+ }
1917
2361
  if (!['type_identifier', 'nested_type_identifier', 'generic_type', 'predefined_type'].includes(item?.type)) return null;
1918
2362
  const type = tsTypeName(item);
1919
- return type ? { type, qualifier: tsTypeQualifier(item), node: item } : null;
2363
+ return type ? { type, qualifier: tsTypeQualifier(item), node: item, ...(keyed && { keyed: true }) } : null;
1920
2364
  }
1921
2365
 
1922
2366
  /**
@@ -2020,7 +2464,82 @@ function findCallsInCode(code, parser) {
2020
2464
  }
2021
2465
  return current?.type === 'identifier' ? current.text : undefined;
2022
2466
  };
2467
+ // Local binding scopes by name (fix #397): every function-level
2468
+ // declaration, parameter, catch and loop binding with the source range it
2469
+ // binds in, so a reference's shadowing binding is found by range instead
2470
+ // of a parent walk; and the names object patterns bind.
2471
+ const bindingScopes = new Map(); // name -> [{ start, end, declStart, line }]
2472
+ const destructuredNames = new Set();
2473
+ const addScope = (pattern, scope, declStart, line) => {
2474
+ const names = [];
2475
+ collectBoundPatternNames(pattern, names);
2476
+ for (const n of names) {
2477
+ let list = bindingScopes.get(n);
2478
+ if (!list) { list = []; bindingScopes.set(n, list); }
2479
+ list.push({ start: scope.startIndex, end: scope.endIndex, declStart, line });
2480
+ }
2481
+ };
2023
2482
  traverseTreeCached(tree.rootNode, node => {
2483
+ switch (node.type) {
2484
+ case 'variable_declarator': {
2485
+ const statement = node.parent;
2486
+ const host = statement?.parent;
2487
+ if (host?.type === 'statement_block') {
2488
+ // A declarator initialized from require()/import() is the
2489
+ // module's binding reaching the scope, not a shadow (#337).
2490
+ if (!isImportInitializerNode(node.childForFieldName('value'))) {
2491
+ addScope(node.childForFieldName('name'), host, statement.startIndex,
2492
+ statement.startPosition.row + 1);
2493
+ }
2494
+ } else if (host?.type === 'for_statement' || host?.type === 'for_in_statement') {
2495
+ addScope(node.childForFieldName('name'), host, null, -1);
2496
+ }
2497
+ break;
2498
+ }
2499
+ case 'formal_parameters':
2500
+ if (node.parent && JS_SHADOW_FUNCTIONS.has(node.parent.type)) addScope(node, node.parent, null, -1);
2501
+ break;
2502
+ case 'arrow_function': {
2503
+ const single = node.childForFieldName('parameter');
2504
+ if (single) addScope(single, node, null, -1);
2505
+ break;
2506
+ }
2507
+ case 'catch_clause': {
2508
+ const param = node.childForFieldName('parameter');
2509
+ if (param) addScope(param, node, null, -1);
2510
+ break;
2511
+ }
2512
+ case 'for_in_statement': {
2513
+ const left = node.childForFieldName('left');
2514
+ if (left && left.type !== 'lexical_declaration' && left.type !== 'variable_declaration') {
2515
+ addScope(left, node, null, -1);
2516
+ }
2517
+ break;
2518
+ }
2519
+ case 'function_declaration': case 'generator_function_declaration': case 'class_declaration':
2520
+ if (node.parent?.type === 'statement_block') {
2521
+ const nameNode = node.childForFieldName('name');
2522
+ // Hoisted within the block: shadows at any position.
2523
+ if (nameNode) addScope(nameNode, node.parent, null, node.startPosition.row + 1);
2524
+ }
2525
+ break;
2526
+ case 'object_pattern':
2527
+ for (let i = 0; i < node.namedChildCount; i++) {
2528
+ const prop = node.namedChild(i);
2529
+ if (prop.type === 'shorthand_property_identifier_pattern') destructuredNames.add(prop.text);
2530
+ else if (prop.type === 'object_assignment_pattern') {
2531
+ const left = prop.childForFieldName('left');
2532
+ if (left) destructuredNames.add(left.text);
2533
+ } else if (prop.type === 'pair_pattern') {
2534
+ let value = prop.childForFieldName('value');
2535
+ if (value?.type === 'assignment_pattern') value = value.childForFieldName('left');
2536
+ if (value?.type === 'identifier') destructuredNames.add(value.text);
2537
+ }
2538
+ }
2539
+ break;
2540
+ default:
2541
+ break;
2542
+ }
2024
2543
  if (['variable_declarator', 'required_parameter', 'optional_parameter'].includes(node.type)) {
2025
2544
  const name = node.childForFieldName('name') || node.childForFieldName('pattern');
2026
2545
  if (name?.type === 'identifier' && tsArrayElement(node.childForFieldName('type'))) {
@@ -2136,6 +2655,10 @@ function findCallsInCode(code, parser) {
2136
2655
  const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
2137
2656
  const nonCallableNames = new Set(); // Track names assigned non-callable values
2138
2657
  const localVarTypes = new ReceiverTypeMap(); // Track local variable types: varName -> typeName (for receiverType inference)
2658
+ // One-hop `const` aliases of member paths (fix #381):
2659
+ // `const c = this.config; c.load()` receives through `this.config`.
2660
+ // name -> { node: the aliased member_expression, scope: declaring block }.
2661
+ const localFieldAliases = new Map();
2139
2662
  const localVarTypeQualifiers = new Map(); // qualifier provenance for new ns.Type()
2140
2663
  // Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
2141
2664
  // params). The compiler enforces assignability for these, so reassignment
@@ -2177,6 +2700,176 @@ function findCallsInCode(code, parser) {
2177
2700
  return count;
2178
2701
  };
2179
2702
 
2703
+ // fix #366: request-config calls — `request(cfg, { method: 'POST',
2704
+ // url: '/api/users' })`, `client.post({ url: '/api/items' })` (generated
2705
+ // OpenAPI SDKs). Returns { url, interp, method? } for the first object
2706
+ // literal argument carrying a string `url`/`path` property. Whether the
2707
+ // callee really performs HTTP is decided at query time from the callee's
2708
+ // definition, never from this record.
2709
+ const getRequestConfig = (callNode) => {
2710
+ const argsNode = callNode.childForFieldName('arguments');
2711
+ if (!argsNode) return null;
2712
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
2713
+ const arg = argsNode.namedChild(i);
2714
+ if (arg.type !== 'object') continue;
2715
+ let url = null;
2716
+ let method = null;
2717
+ let handler = null;
2718
+ for (let j = 0; j < arg.namedChildCount; j++) {
2719
+ const prop = arg.namedChild(j);
2720
+ if (prop.type !== 'pair') continue;
2721
+ const keyNode = prop.childForFieldName('key');
2722
+ const valNode = prop.childForFieldName('value');
2723
+ if (!keyNode || !valNode) continue;
2724
+ const keyName = keyNode.type === 'string'
2725
+ ? keyNode.text.replace(/^['"`]|['"`]$/g, '') : keyNode.text;
2726
+ if ((keyName === 'url' || keyName === 'path') && !url) {
2727
+ const v = _extractStringArg(valNode);
2728
+ if (v && typeof v.value === 'string' && v.value.length > 0) {
2729
+ url = { value: v.value, interp: !!v.interp, key: keyName };
2730
+ }
2731
+ } else if (keyName === 'method') {
2732
+ const v = _extractStringArg(valNode);
2733
+ if (v && !v.interp && typeof v.value === 'string' && v.value.length > 0) {
2734
+ method = v.value.toUpperCase();
2735
+ }
2736
+ } else if (keyName === 'handler') {
2737
+ // fix #383: `route({ method, url, handler })` names its handler.
2738
+ handler = handlerNameOf(valNode);
2739
+ }
2740
+ }
2741
+ if (url) {
2742
+ return { url: url.value, key: url.key, ...(url.interp && { interp: true }),
2743
+ ...(method && { method }), ...(handler && { handler }) };
2744
+ }
2745
+ }
2746
+ return null;
2747
+ };
2748
+
2749
+ // fix #383: the arguments of a route-shaped registration
2750
+ // (`app.get('/x', mw, handler)`) after its path: each argument's name,
2751
+ // '<anonymous>' for an inline function, the callee's name for a call
2752
+ // (`wrap(handler)`), null for any other expression.
2753
+ const ROUTE_VERB_METHODS = /^(get|post|put|delete|patch|options|head|all)$/;
2754
+ const handlerNameOf = (node) => {
2755
+ if (!node) return null;
2756
+ if (node.type === 'identifier') return node.text;
2757
+ if (node.type === 'member_expression') return node.childForFieldName('property')?.text || null;
2758
+ if (/function|arrow/.test(node.type)) return '<anonymous>';
2759
+ if (node.type === 'call_expression') {
2760
+ const fn = node.childForFieldName('function');
2761
+ if (fn?.type === 'identifier') return fn.text;
2762
+ if (fn?.type === 'member_expression') return fn.childForFieldName('property')?.text || null;
2763
+ }
2764
+ return null;
2765
+ };
2766
+ // The names of a registration's arguments after its path (null for an
2767
+ // expression with no name); the router decides which one is the handler.
2768
+ const getHandlerArgs = (callNode) => {
2769
+ const argsNode = callNode.childForFieldName('arguments');
2770
+ if (!argsNode) return null;
2771
+ const out = [];
2772
+ let first = true;
2773
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
2774
+ const arg = argsNode.namedChild(i);
2775
+ if (arg.type.endsWith('comment')) continue;
2776
+ if (first) { first = false; continue; }
2777
+ out.push(handlerNameOf(arg));
2778
+ }
2779
+ return out.length > 0 ? out : null;
2780
+ };
2781
+
2782
+ const hasObjectKeyArg = (callNode, key) => {
2783
+ const argsNode = callNode.childForFieldName('arguments');
2784
+ if (!argsNode) return false;
2785
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
2786
+ const arg = argsNode.namedChild(i);
2787
+ if (arg.type !== 'object') continue;
2788
+ for (let j = 0; j < arg.namedChildCount; j++) {
2789
+ const prop = arg.namedChild(j);
2790
+ const keyNode = prop.type === 'pair' ? prop.childForFieldName('key') : prop;
2791
+ if (keyNode && keyNode.text.replace(/^['"`]|['"`]$/g, '') === key) return true;
2792
+ }
2793
+ }
2794
+ return false;
2795
+ };
2796
+
2797
+ const getMountArgs = (callNode) => {
2798
+ const argsNode = callNode.childForFieldName('arguments');
2799
+ if (!argsNode) return null;
2800
+ const out = [];
2801
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
2802
+ const arg = argsNode.namedChild(i);
2803
+ if (arg.type.endsWith('comment')) continue;
2804
+ if (arg.type === 'identifier') out.push(arg.text);
2805
+ else if (arg.type === 'string' || (arg.type === 'template_string' && arg.namedChildCount <= 1)) out.push('""');
2806
+ else if (arg.type.includes('function')) out.push('fn');
2807
+ else out.push('()');
2808
+ }
2809
+ return out;
2810
+ };
2811
+
2812
+ // fix #366: `x.register(plugin, { prefix: '/v1' })` (Fastify plugin
2813
+ // mounts). Records the prefix option (literal, or `prefixDynamic` for an
2814
+ // expression) and, for an inline plugin function, its span and first
2815
+ // parameter - the router the plugin registers routes on. Returns null
2816
+ // when the call carries neither.
2817
+ const getRegisterMount = (callNode) => {
2818
+ const argsNode = callNode.childForFieldName('arguments');
2819
+ if (!argsNode) return null;
2820
+ const args = [];
2821
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
2822
+ const arg = argsNode.namedChild(i);
2823
+ if (!arg.type.endsWith('comment')) args.push(arg);
2824
+ }
2825
+ if (args.length === 0) return null;
2826
+ const out = {};
2827
+ const opts = args[1];
2828
+ if (opts && opts.type === 'object') {
2829
+ for (let j = 0; j < opts.namedChildCount; j++) {
2830
+ const prop = opts.namedChild(j);
2831
+ const keyNode = prop.type === 'pair' ? prop.childForFieldName('key') : prop;
2832
+ if (!keyNode || keyNode.text.replace(/^['"`]|['"`]$/g, '') !== 'prefix') continue;
2833
+ const v = prop.type === 'pair' ? _extractStringArg(prop.childForFieldName('value')) : null;
2834
+ if (v && !v.interp && typeof v.value === 'string') out.prefix = v.value;
2835
+ else out.prefixDynamic = true;
2836
+ }
2837
+ } else if (opts) {
2838
+ out.prefixDynamic = true;
2839
+ }
2840
+ let plugin = args[0];
2841
+ // `register(fp(function (instance) {...}))`: a wrapper call around an
2842
+ // inline plugin function registers that function.
2843
+ if (plugin.type === 'call_expression') {
2844
+ const inner = plugin.childForFieldName('arguments')?.namedChild(0);
2845
+ if (inner && ['arrow_function', 'function_expression', 'function'].includes(inner.type)) plugin = inner;
2846
+ }
2847
+ if (['arrow_function', 'function_expression', 'function'].includes(plugin.type)) {
2848
+ const params = plugin.childForFieldName('parameters') || plugin.childForFieldName('parameter');
2849
+ let first = params && params.type === 'identifier' ? params : null;
2850
+ if (!first && params) {
2851
+ for (let i = 0; i < params.namedChildCount; i++) {
2852
+ const p = params.namedChild(i);
2853
+ if (p.type.endsWith('comment')) continue;
2854
+ const pat = p.type === 'identifier' ? p : p.childForFieldName('pattern');
2855
+ first = pat && pat.type === 'identifier' ? pat : null;
2856
+ break;
2857
+ }
2858
+ }
2859
+ if (first) {
2860
+ out.plugin = { start: plugin.startIndex, end: plugin.endIndex, param: first.text };
2861
+ }
2862
+ } else if (plugin.type === 'identifier') {
2863
+ // A named plugin: a same-file function composes from the index;
2864
+ // anything else is resolved by the endpoints graph.
2865
+ out.pluginName = plugin.text;
2866
+ } else if (plugin.type === 'member_expression' || plugin.type === 'call_expression') {
2867
+ out.pluginRef = true;
2868
+ }
2869
+ return (out.prefix != null || out.prefixDynamic || out.plugin || out.pluginRef || out.pluginName)
2870
+ ? out : null;
2871
+ };
2872
+
2180
2873
  // MEDIUM-5: extract HTTP method from `fetch(url, { method: 'POST' })`
2181
2874
  // and similar XHR/Request-init shapes. Returns the upper-cased method
2182
2875
  // string or null. Looks at argument index `argIdx` (default 1, the
@@ -2437,8 +3130,12 @@ function findCallsInCode(code, parser) {
2437
3130
  const root = object.childForFieldName('object');
2438
3131
  const offset = object.childForFieldName('index');
2439
3132
  if (root?.type !== 'identifier' || !arrayAnnotationNames.has(root.text) || !offset) return null;
2440
- let numeric = offset.type === 'number';
2441
- if (offset.type === 'identifier') {
3133
+ const binding = indexedBinding(root.text, object);
3134
+ const element = tsArrayElement(binding?.type);
3135
+ // A keyed container (Record / index signature) accepts any key; an
3136
+ // array index must be numeric (a string key reads a property).
3137
+ let numeric = offset.type === 'number' || !!element?.keyed;
3138
+ if (offset.type === 'identifier' && !element?.keyed) {
2442
3139
  const indexBinding = indexedBinding(offset.text, object);
2443
3140
  const annotation = indexBinding?.type?.namedChild(0);
2444
3141
  numeric = annotation?.type === 'predefined_type' && annotation.text === 'number';
@@ -2447,8 +3144,6 @@ function findCallsInCode(code, parser) {
2447
3144
  }
2448
3145
  }
2449
3146
  if (!numeric) return null;
2450
- const binding = indexedBinding(root.text, object);
2451
- const element = tsArrayElement(binding?.type);
2452
3147
  if (element && !element.qualifier) {
2453
3148
  for (let scope = binding.declaration.parent; scope; scope = scope.parent) {
2454
3149
  const parameters = scope.childForFieldName('type_parameters');
@@ -2496,16 +3191,6 @@ function findCallsInCode(code, parser) {
2496
3191
  const fn = v.childForFieldName('function');
2497
3192
  return !!fn && (fn.type === 'import' || (fn.type === 'identifier' && fn.text === 'require'));
2498
3193
  };
2499
- const _declaresLocalShadow = (declNode, name) => {
2500
- for (let i = 0; i < declNode.namedChildCount; i++) {
2501
- const d = declNode.namedChild(i);
2502
- if (d.type !== 'variable_declarator') continue;
2503
- if (!_patternDeclaresName(d.childForFieldName('name'), name)) continue;
2504
- if (_isImportBindingInitializer(d.childForFieldName('value'))) continue;
2505
- return true;
2506
- }
2507
- return false;
2508
- };
2509
3194
 
2510
3195
  // Bare callback references need to distinguish a module-owned VALUE from
2511
3196
  // an unbound name. File-level import reachability cannot prove the value's
@@ -2568,54 +3253,438 @@ function findCallsInCode(code, parser) {
2568
3253
  // findCallers — let locals and non-symbol arrow params are only
2569
3254
  // visible here. Module-level (program) declarations are NOT shadows:
2570
3255
  // that's the module binding itself, owned by binding resolution.
2571
- const isShadowedByLocal = (refNode, name) => {
2572
- for (let p = refNode.parent; p; p = p.parent) {
3256
+ // Returns the 1-based line of the shadowing declaration statement, -1
3257
+ // for a parameter / catch / loop binding (never an indexed definition),
3258
+ // 0 when the name is not shadowed. The declaration line lets the query
3259
+ // tell a shadow that IS the pinned definition (declared in any
3260
+ // enclosing function of the reference, fix #397) from an unrelated
3261
+ // local. Statement lists read their memoized binding tables: hoisted
3262
+ // function/class declarations shadow anywhere in the block, lexical and
3263
+ // var declarations from their statement on; a declarator initialized
3264
+ // from require()/import() is the module's binding, not a shadow (#337).
3265
+ // The innermost binding scope containing the reference: a hoisted
3266
+ // function/class declaration anywhere in its block, a lexical or var
3267
+ // declaration from its statement on, a parameter / catch / loop binding
3268
+ // anywhere in its function or statement.
3269
+ const shadowingLineOf = (refNode, name) => {
3270
+ const scopes = bindingScopes.get(name);
3271
+ if (!scopes) return 0;
3272
+ const at = refNode.startIndex;
3273
+ let best = null;
3274
+ for (const scope of scopes) {
3275
+ if (at < scope.start || at >= scope.end) continue;
3276
+ if (scope.declStart != null && scope.declStart >= at) continue;
3277
+ if (!best || scope.end - scope.start < best.end - best.start) best = scope;
3278
+ }
3279
+ return best ? best.line : 0;
3280
+ };
3281
+ const isShadowedByLocal = (refNode, name) => shadowingLineOf(refNode, name) !== 0;
3282
+ // `localShadow` holds the shadowing declaration's line (-1 for a
3283
+ // parameter / catch / loop binding): truthy like the other languages'
3284
+ // `true`, and the query compares the line with the pinned definition.
3285
+ const localShadowFields = (refNode, name) => {
3286
+ const line = shadowingLineOf(refNode, name);
3287
+ return line !== 0 ? { localShadow: line } : null;
3288
+ };
3289
+
3290
+ // The aliased member expression when `refNode` names a live one-hop
3291
+ // const alias: inside the declaring block, after the declaration, and
3292
+ // not shadowed by a parameter or declaration in between (fix #381).
3293
+ const localFieldAliasAt = (refNode) => {
3294
+ const alias = localFieldAliases.get(refNode.text);
3295
+ if (!alias || refNode.startIndex < alias.node.endIndex ||
3296
+ refNode.startIndex < alias.scope.startIndex ||
3297
+ refNode.endIndex > alias.scope.endIndex) return null;
3298
+ const name = refNode.text;
3299
+ for (let p = refNode.parent; p && p.id !== alias.scope.id; p = p.parent) {
2573
3300
  if (p.type === 'statement_block') {
2574
3301
  for (let i = 0; i < p.namedChildCount; i++) {
2575
3302
  const stmt = p.namedChild(i);
2576
- // Nested function/class declarations are hoisted within
2577
- // their block — they shadow regardless of position
2578
- // (fix #218: `function getStyle() {}` after the ref).
3303
+ if ((stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') &&
3304
+ _declaresName(stmt, name)) return null;
3305
+ if ((stmt.type === 'function_declaration' || stmt.type === 'class_declaration' ||
3306
+ stmt.type === 'generator_function_declaration') &&
3307
+ stmt.childForFieldName('name')?.text === name) return null;
3308
+ }
3309
+ } else if (p.type === 'for_statement' || p.type === 'for_in_statement') {
3310
+ const left = p.childForFieldName('initializer') || p.childForFieldName('left');
3311
+ if (left && (_patternDeclaresName(left, name) ||
3312
+ ((left.type === 'lexical_declaration' || left.type === 'variable_declaration') &&
3313
+ _declaresName(left, name)))) return null;
3314
+ } else if (p.type === 'catch_clause') {
3315
+ if (_patternDeclaresName(p.childForFieldName('parameter'), name)) return null;
3316
+ } else if (isFunctionNode(p)) {
3317
+ const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
3318
+ if (params && (_patternDeclaresName(params, name) ||
3319
+ params.namedChildren.some(prm => _patternDeclaresName(prm, name)))) return null;
3320
+ }
3321
+ }
3322
+ return alias.node;
3323
+ };
3324
+
3325
+ // Receiver facts of a member access `objNode.propName` (the receiver
3326
+ // half of a method-call record). Shared by method calls and by bare
3327
+ // calls of names destructured from an object (fix #397), which read the
3328
+ // member of the destructuring source.
3329
+ const memberReceiverFacts = (objNode, propName) => {
3330
+ // Extract receiver: handles identifiers (obj), this, super
3331
+ let receiver = undefined;
3332
+ // fix #381: `c.load()` after `const c = this.config`
3333
+ // receives exactly like `this.config.load()` while the
3334
+ // const is the binding in scope.
3335
+ const aliasedReceiver = objNode?.type === 'identifier' &&
3336
+ !localVarTypes.has(objNode.text)
3337
+ ? localFieldAliasAt(objNode) : null;
3338
+ if (aliasedReceiver) objNode = aliasedReceiver;
3339
+ if (objNode) {
3340
+ if (objNode.type === 'identifier' || objNode.type === 'this' || objNode.type === 'super') {
3341
+ receiver = objNode.text;
3342
+ }
3343
+ }
3344
+ // One-hop field receiver (fix #219 — #202's shape for
3345
+ // structural): this._map.has(x) / def.cache.get(k) —
3346
+ // receiverRoot/Field let findCallers hop to the
3347
+ // field's DECLARED type annotation. `this`-rooted hops
3348
+ // resolve their root type query-side (the enclosing
3349
+ // class); identifier roots type from local annotations.
3350
+ let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
3351
+ let receiverDeepPath = false;
3352
+ if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
3353
+ if (!receiver && objNode && objNode.type === 'member_expression') {
3354
+ const rootNode = objNode.childForFieldName('object');
3355
+ const fldNode = objNode.childForFieldName('property');
3356
+ if (fldNode && rootNode &&
3357
+ (rootNode.type === 'identifier' || rootNode.type === 'this')) {
3358
+ receiverRoot = rootNode.text;
3359
+ receiverBindingNode = rootNode.type === 'identifier' ? rootNode : undefined;
3360
+ receiverFieldName = fldNode.text;
3361
+ if (rootNode.type === 'identifier') {
3362
+ receiverRootType = localVarTypes.get(rootNode.text);
3363
+ }
3364
+ } else {
3365
+ // Preserve unresolved deeper member chains
3366
+ // (`client.req.query()`). Their terminal name
3367
+ // must not borrow a same-file method binding
3368
+ // while the root object's type is unknown.
3369
+ receiverDeepPath = true;
3370
+ }
3371
+ }
3372
+ // Chained receiver (fix #219): the receiver IS a call —
3373
+ // parseAsync(args).catch(...) — record the producer so
3374
+ // findCallers can type the receiver from its declared
3375
+ // return annotation (Promise<...> → Promise).
3376
+ let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
3377
+ let receiverCallStart, receiverCallEnd;
3378
+ {
3379
+ let recvNode = objNode;
3380
+ if (recvNode && recvNode.type === 'parenthesized_expression') {
3381
+ recvNode = recvNode.namedChild(0);
3382
+ }
3383
+ if (recvNode && recvNode.type === 'await_expression') {
3384
+ receiverCallAwaited = true;
3385
+ recvNode = recvNode.namedChild(0);
3386
+ }
3387
+ if (recvNode && recvNode.type === 'call_expression') {
3388
+ const prodFunc = recvNode.childForFieldName('function');
3389
+ if (prodFunc?.type === 'identifier') {
3390
+ receiverCall = prodFunc.text;
3391
+ // Producer link (fix #258): plain-call
3392
+ // records carry the call node's start line
3393
+ receiverCallLine = recvNode.startPosition.row + 1;
3394
+ receiverCallStart = recvNode.startIndex;
3395
+ receiverCallEnd = recvNode.endIndex;
3396
+ } else if (prodFunc?.type === 'member_expression') {
3397
+ const prodProp = prodFunc.childForFieldName('property');
3398
+ if (prodProp) {
3399
+ receiverCall = prodProp.text;
3400
+ receiverCallIsMethod = true;
3401
+ // Method records report the property
3402
+ // node's own line
3403
+ receiverCallLine = prodProp.startPosition.row + 1;
3404
+ receiverCallStart = recvNode.startIndex;
3405
+ receiverCallEnd = recvNode.endIndex;
3406
+ }
3407
+ }
3408
+ }
3409
+ if (!receiverCall) receiverCallAwaited = undefined;
3410
+ }
3411
+ // Literal receivers carry their builtin type: [].map() can
3412
+ // never be a project class method
3413
+ // A freshly constructed receiver has an exact runtime
3414
+ // type as well: new Service().start(). Recording it here
3415
+ // avoids treating the call as an untyped method dispatch.
3416
+ const constructedReceiverType = objNode?.type === 'new_expression'
3417
+ ? jsConstructorTypeName(objNode.childForFieldName('constructor'))
3418
+ : undefined;
3419
+ const constructedReceiverQualifier = objNode?.type === 'new_expression'
3420
+ ? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
3421
+ : undefined;
3422
+ const indexedReceiver = indexedArrayReceiver(objNode);
3423
+ const receiverType = indexedReceiver?.type || (receiver
3424
+ ? localVarTypes.get(receiver)
3425
+ : (constructedReceiverType ||
3426
+ (objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined)));
3427
+ // Module receiver (ns.helper()) — unless locally shadowed
3428
+ // by a typed instance binding
3429
+ const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
3430
+ const receiverIsModule = !!receiverModuleSpecifier ||
3431
+ (!!receiver && moduleAliases.has(receiver) &&
3432
+ !localVarTypes.has(receiver));
3433
+ const receiverModuleComposition = receiver &&
3434
+ !unsafeModuleCompositions.has(receiver)
3435
+ ? moduleCompositions.get(receiver) : undefined;
3436
+ return {
3437
+ receiver,
3438
+ ...(receiverType && { receiverType,
3439
+ ...(indexedReceiver ? { receiverTypeSource: 'annotation',
3440
+ receiverTypeEvidence: indexedReceiver.evidence }
3441
+ : receiver ? localVarTypes.fields(receiver, receiverType) : {
3442
+ receiverTypeSource: constructedReceiverType ? 'constructor' : 'literal',
3443
+ receiverTypeEvidence: typeOrigin(constructedReceiverType ? 'constructor' : 'literal', objNode),
3444
+ }),
3445
+ }),
3446
+ ...((indexedReceiver?.qualifier || constructedReceiverQualifier ||
3447
+ (receiver && localVarTypeQualifiers.get(receiver))) && {
3448
+ receiverTypeQualifier: indexedReceiver?.qualifier || constructedReceiverQualifier ||
3449
+ localVarTypeQualifiers.get(receiver),
3450
+ }),
3451
+ ...(receiverIsModule && { receiverIsModule: true }),
3452
+ ...(receiverModuleSpecifier && { receiverModuleSpecifier }),
3453
+ ...(receiverModuleComposition && {
3454
+ receiverModuleComposition,
3455
+ }),
3456
+ ...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
3457
+ receiverMemberAssigned: true,
3458
+ }),
3459
+ ...(receiverBindingNode &&
3460
+ isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
3461
+ { receiverLocalBinding: true }),
3462
+ ...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
3463
+ ...(receiverFieldName && receiverRootType && { receiverRootType }),
3464
+ ...(receiverDeepPath && { receiverDeepPath: true }),
3465
+ ...(receiverCall && { receiverCall }),
3466
+ ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
3467
+ ...(receiverCallAwaited && { receiverCallAwaited: true }),
3468
+ ...(receiverCallLine && { receiverCallLine }),
3469
+ ...(receiverCallStart != null && { receiverCallStart }),
3470
+ ...(receiverCallEnd != null && { receiverCallEnd }),
3471
+ };
3472
+ };
3473
+
3474
+ // Destructured member bindings (fix #397, immer-measured: `const {
3475
+ // produce } = createPatchedImmer()` then `produce(...)` lost every
3476
+ // caller). An object-pattern binding holds the MEMBER of its source
3477
+ // object: `const { run } = make(); run()` reads `make().run`, so the bare
3478
+ // call is that member access. Names some object pattern binds, so the
3479
+ // lexical lookup below runs only for them.
3480
+ // The node declaring `name` in a declaration / parameter pattern.
3481
+ const patternBindingNode = (pattern, name) => {
3482
+ if (!pattern) return null;
3483
+ if (pattern.type === 'required_parameter' || pattern.type === 'optional_parameter') {
3484
+ return patternBindingNode(pattern.childForFieldName('pattern') ||
3485
+ pattern.childForFieldName('name'), name);
3486
+ }
3487
+ if (pattern.type === 'identifier' || pattern.type === 'shorthand_property_identifier_pattern') {
3488
+ return pattern.text === name ? pattern : null;
3489
+ }
3490
+ if (pattern.type === 'pair_pattern' || pattern.type === 'pair') {
3491
+ return patternBindingNode(pattern.childForFieldName('value'), name);
3492
+ }
3493
+ if (pattern.type === 'assignment_pattern' || pattern.type === 'object_assignment_pattern') {
3494
+ return patternBindingNode(pattern.childForFieldName('left') ||
3495
+ pattern.childForFieldName('pattern'), name);
3496
+ }
3497
+ if (pattern.type === 'type_annotation' || pattern.type === 'predefined_type') return null;
3498
+ for (let i = 0; i < pattern.namedChildCount; i++) {
3499
+ const found = patternBindingNode(pattern.namedChild(i), name);
3500
+ if (found) return found;
3501
+ }
3502
+ return null;
3503
+ };
3504
+ const declarationBindingNode = (declaration, name) => {
3505
+ for (let i = 0; i < declaration.namedChildCount; i++) {
3506
+ const declarator = declaration.namedChild(i);
3507
+ if (declarator.type !== 'variable_declarator') continue;
3508
+ const found = patternBindingNode(declarator.childForFieldName('name'), name);
3509
+ if (found) return found;
3510
+ }
3511
+ return null;
3512
+ };
3513
+ // The innermost lexical binding of `name` visible at `refNode`: the
3514
+ // declaring identifier / shorthand pattern node, or a marker for a
3515
+ // binding that is not a variable pattern (function, class, import,
3516
+ // named function expression). Within the declaring block a let/const
3517
+ // must precede the reference; from a nested function any position of
3518
+ // the block counts (the closure runs after the block is initialized).
3519
+ const innermostBinding = (refNode, name) => {
3520
+ let crossedFunction = false;
3521
+ for (let p = refNode.parent; p; p = p.parent) {
3522
+ if (p.type === 'statement_block' || p.type === 'program') {
3523
+ for (let i = 0; i < p.namedChildCount; i++) {
3524
+ let stmt = p.namedChild(i);
3525
+ if (stmt.type === 'export_statement') {
3526
+ stmt = stmt.childForFieldName('declaration') || stmt.namedChild(0);
3527
+ if (!stmt) continue;
3528
+ }
2579
3529
  if ((stmt.type === 'function_declaration' ||
2580
3530
  stmt.type === 'generator_function_declaration' ||
2581
3531
  stmt.type === 'class_declaration') &&
2582
- stmt.childForFieldName('name')?.text === name) return true;
2583
- if (stmt.startIndex >= refNode.startIndex) continue; // declaration-before-use
2584
- if ((stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') &&
2585
- _declaresLocalShadow(stmt, name)) return true;
3532
+ stmt.childForFieldName('name')?.text === name) return { other: true };
3533
+ if (stmt.type === 'import_statement' && p.type === 'program') {
3534
+ const clause = stmt.namedChildren.find(c => c.type === 'import_clause');
3535
+ if (clause && clause.descendantsOfType(['identifier']).some(id => id.text === name)) {
3536
+ return { other: true };
3537
+ }
3538
+ continue;
3539
+ }
3540
+ if (stmt.type !== 'lexical_declaration' && stmt.type !== 'variable_declaration') continue;
3541
+ if (!crossedFunction && stmt.startIndex >= refNode.startIndex) continue;
3542
+ const found = declarationBindingNode(stmt, name);
3543
+ if (found) return { node: found };
2586
3544
  }
2587
3545
  } else if (p.type === 'for_statement') {
2588
3546
  const init = p.childForFieldName('initializer');
2589
- if (init && (init.type === 'lexical_declaration' || init.type === 'variable_declaration') &&
2590
- _declaresName(init, name)) return true;
3547
+ if (init && (init.type === 'lexical_declaration' || init.type === 'variable_declaration')) {
3548
+ const found = declarationBindingNode(init, name);
3549
+ if (found) return { node: found };
3550
+ }
2591
3551
  } else if (p.type === 'for_in_statement') {
2592
3552
  const left = p.childForFieldName('left');
2593
- if (_patternDeclaresName(left, name)) return true;
2594
- if (left && (left.type === 'lexical_declaration' || left.type === 'variable_declaration') &&
2595
- _declaresName(left, name)) return true;
3553
+ const found = left && (patternBindingNode(left, name) ||
3554
+ ((left.type === 'lexical_declaration' || left.type === 'variable_declaration')
3555
+ ? declarationBindingNode(left, name) : null));
3556
+ if (found) return { node: found };
2596
3557
  } else if (p.type === 'catch_clause') {
2597
- const param = p.childForFieldName('parameter');
2598
- if (_patternDeclaresName(param, name)) return true;
2599
- } else if (p.type === 'arrow_function' || p.type === 'function_expression' ||
2600
- p.type === 'function_declaration' || p.type === 'function' ||
2601
- p.type === 'method_definition' || p.type === 'generator_function' ||
2602
- p.type === 'generator_function_declaration') {
3558
+ const found = patternBindingNode(p.childForFieldName('parameter'), name);
3559
+ if (found) return { node: found };
3560
+ } else if (isFunctionNode(p)) {
2603
3561
  const params = p.childForFieldName('parameters') || p.childForFieldName('parameter');
2604
- if (params) {
2605
- if (_patternDeclaresName(params, name)) return true;
2606
- for (let i = 0; i < params.namedChildCount; i++) {
2607
- const prm = params.namedChild(i);
2608
- if (_patternDeclaresName(prm, name)) return true;
2609
- }
3562
+ const found = params && patternBindingNode(params, name);
3563
+ if (found) return { node: found };
3564
+ if ((p.type === 'function_expression' || p.type === 'function') &&
3565
+ p.childForFieldName('name')?.text === name) return { other: true };
3566
+ crossedFunction = true;
3567
+ } else if (p.type === 'class' && p.childForFieldName('name')?.text === name) {
3568
+ return { other: true };
3569
+ }
3570
+ }
3571
+ return null;
3572
+ };
3573
+ // The destructuring that binds `name` at `refNode`, when the innermost
3574
+ // binding is an object-pattern property whose source is not an import
3575
+ // (`const { f } = require('./m')` stays an import binding): the property
3576
+ // key, its position, and the receiver facts of the source object read as
3577
+ // `source.key`. A parameter pattern's source is the argument (typed by a
3578
+ // plain TS annotation when present); nested patterns, loop and catch
3579
+ // bindings and defaulted properties have an unknown source.
3580
+ const destructuredBindingOf = (refNode, name) => {
3581
+ if (!destructuredNames.has(name)) return null;
3582
+ const binding = innermostBinding(refNode, name);
3583
+ const node = binding?.node;
3584
+ if (!node) return null;
3585
+ let keyNode = null;
3586
+ let shorthand = false;
3587
+ let hasDefault = false;
3588
+ let property = node;
3589
+ if (node.type === 'shorthand_property_identifier_pattern') {
3590
+ keyNode = node;
3591
+ shorthand = true;
3592
+ if (node.parent?.type === 'object_assignment_pattern') {
3593
+ hasDefault = true;
3594
+ property = node.parent;
3595
+ }
3596
+ } else if (node.type === 'identifier') {
3597
+ let holder = node.parent;
3598
+ if (holder?.type === 'assignment_pattern' &&
3599
+ sameNode(holder.childForFieldName('left'), node)) {
3600
+ hasDefault = true;
3601
+ holder = holder.parent;
3602
+ }
3603
+ if (holder?.type !== 'pair_pattern') return null;
3604
+ const key = holder.childForFieldName('key');
3605
+ if (!key || (key.type !== 'property_identifier' && key.type !== 'identifier')) return null;
3606
+ keyNode = key;
3607
+ property = holder;
3608
+ }
3609
+ const pattern = property.parent;
3610
+ if (!keyNode || pattern?.type !== 'object_pattern') return null;
3611
+ let holder = pattern.parent;
3612
+ let facts = null;
3613
+ let sourceKind = 'unknown';
3614
+ if (holder?.type === 'variable_declarator' &&
3615
+ sameNode(holder.childForFieldName('name'), pattern)) {
3616
+ const value = holder.childForFieldName('value');
3617
+ if (!value || _isImportBindingInitializer(value)) return null;
3618
+ if (!hasDefault) {
3619
+ facts = memberReceiverFacts(value, keyNode.text);
3620
+ sourceKind = 'value';
3621
+ }
3622
+ } else {
3623
+ let typeNode = null;
3624
+ if (holder?.type === 'assignment_pattern' && sameNode(holder.childForFieldName('left'), pattern)) {
3625
+ holder = holder.parent;
3626
+ }
3627
+ if (holder?.type === 'required_parameter' || holder?.type === 'optional_parameter') {
3628
+ typeNode = holder.childForFieldName('type');
3629
+ holder = holder.parent;
3630
+ }
3631
+ if (holder?.type === 'formal_parameters') {
3632
+ sourceKind = 'parameter';
3633
+ const written = typeNode?.namedChild(0);
3634
+ if (!hasDefault && written?.type === 'type_identifier') {
3635
+ facts = { receiverType: written.text, receiverTypeSource: 'annotation',
3636
+ receiverTypeEvidence: typeOrigin('annotation', written) };
2610
3637
  }
2611
3638
  }
2612
3639
  }
2613
- return false;
3640
+ return {
3641
+ key: keyNode.text,
3642
+ line: keyNode.startPosition.row + 1,
3643
+ column: keyNode.startPosition.column,
3644
+ ...(shorthand && { shorthand: true }),
3645
+ source: sourceKind,
3646
+ receiver: facts || { receiverDeepPath: true },
3647
+ };
3648
+ };
3649
+ // A method taken as a value (fix #397, immer-measured: `export const
3650
+ // produce = immer.produce`): a paren-less member access stored by a
3651
+ // declaration or assignment names the member exactly like a call on the
3652
+ // same receiver. Recorded when the receiver has typing evidence (a typed
3653
+ // local or `this`); an untyped `a.b` read stays a plain reference, since
3654
+ // most such reads are data.
3655
+ const memberValueReference = (valueNode) => {
3656
+ let value = valueNode;
3657
+ while (value && (value.type === 'parenthesized_expression' || value.type === 'as_expression' ||
3658
+ value.type === 'satisfies_expression' || value.type === 'non_null_expression')) {
3659
+ value = value.namedChild(0);
3660
+ }
3661
+ if (value?.type !== 'member_expression') return;
3662
+ const prop = value.childForFieldName('property');
3663
+ const obj = value.childForFieldName('object');
3664
+ if (prop?.type !== 'property_identifier' || !obj ||
3665
+ (obj.type !== 'identifier' && obj.type !== 'this') || SKIP_IDENTS.has(prop.text)) return;
3666
+ const facts = memberReceiverFacts(obj, prop.text);
3667
+ if (!facts.receiverType && facts.receiver !== 'this') return;
3668
+ calls.push({
3669
+ name: prop.text,
3670
+ line: prop.startPosition.row + 1,
3671
+ column: prop.startPosition.column,
3672
+ isMethod: true,
3673
+ ...facts,
3674
+ isFunctionReference: true,
3675
+ memberValue: true,
3676
+ enclosingFunction: getCurrentEnclosingFunction(),
3677
+ });
3678
+ };
3679
+ const destructuredFields = (refNode) => {
3680
+ const destructured = destructuredBindingOf(refNode, refNode.text);
3681
+ return destructured ? { destructured } : null;
2614
3682
  };
2615
3683
 
2616
3684
  const bareReferenceBindingFields = (refNode) => ({
2617
- ...(isShadowedByLocal(refNode, refNode.text) && { localShadow: true }),
3685
+ ...localShadowFields(refNode, refNode.text),
2618
3686
  ...(moduleValueBindings.has(refNode.text) && { moduleLocalBinding: true }),
3687
+ ...destructuredFields(refNode),
2619
3688
  });
2620
3689
 
2621
3690
  const isConditionalReassignment = node => {
@@ -2659,6 +3728,7 @@ function findCallsInCode(code, parser) {
2659
3728
  if (node.type === 'variable_declarator') {
2660
3729
  const nameNode = node.childForFieldName('name');
2661
3730
  const initNode = node.childForFieldName('value');
3731
+ memberValueReference(initNode);
2662
3732
  // const pkg = require("./lib") — pkg is a module namespace
2663
3733
  if (nameNode?.type === 'identifier' && initNode?.type === 'call_expression') {
2664
3734
  const fn = initNode.childForFieldName('function');
@@ -2670,6 +3740,29 @@ function findCallsInCode(code, parser) {
2670
3740
  // Simple alias: const p = parse
2671
3741
  recordAlias(nameNode.text, initNode.text, node);
2672
3742
  }
3743
+ // One-hop const alias (fix #381): `const c = cfg` with a typed
3744
+ // cfg carries cfg's type; `const c = this.config` receives like
3745
+ // `this.config`. Only `const` (one binding, never reassigned).
3746
+ if (nameNode?.type === 'identifier') localFieldAliases.delete(nameNode.text);
3747
+ if (nameNode?.type === 'identifier' && !node.childForFieldName('type') &&
3748
+ node.parent?.type === 'lexical_declaration' &&
3749
+ node.parent.child(0)?.type === 'const' && initNode &&
3750
+ initNode.text !== nameNode.text) {
3751
+ if (initNode.type === 'identifier' && localVarTypes.has(initNode.text)) {
3752
+ localVarTypes.set(nameNode.text, localVarTypes.get(initNode.text),
3753
+ localVarTypes.origins.get(initNode.text) || 'flow');
3754
+ if (localVarTypeQualifiers.has(initNode.text)) {
3755
+ localVarTypeQualifiers.set(nameNode.text, localVarTypeQualifiers.get(initNode.text));
3756
+ } else {
3757
+ localVarTypeQualifiers.delete(nameNode.text);
3758
+ }
3759
+ } else if (initNode.type === 'member_expression' &&
3760
+ initNode.childForFieldName('property')?.type === 'property_identifier' &&
3761
+ ['this', 'identifier'].includes(initNode.childForFieldName('object')?.type) &&
3762
+ node.parent.parent) {
3763
+ localFieldAliases.set(nameNode.text, { node: initNode, scope: node.parent.parent });
3764
+ }
3765
+ }
2673
3766
  // Ternary alias: const fn = cond ? parseCSV : parseJSON → both targets
2674
3767
  if (nameNode?.type === 'identifier' && initNode?.type === 'ternary_expression') {
2675
3768
  const consequence = initNode.childForFieldName('consequence');
@@ -2729,6 +3822,17 @@ function findCallsInCode(code, parser) {
2729
3822
  localVarTypeQualifiers.delete(nameNode.text);
2730
3823
  }
2731
3824
  }
3825
+ } else if (initNode?.type === 'subscript_expression' &&
3826
+ indexedArrayReceiver(initNode)) {
3827
+ // Declared container element (fix #359): `const c =
3828
+ // items[0]` with `items: Conv[]` binds a Conv.
3829
+ const element = indexedArrayReceiver(initNode);
3830
+ localVarTypes.set(nameNode.text, element.type, 'annotation', element.node);
3831
+ if (element.qualifier) {
3832
+ localVarTypeQualifiers.set(nameNode.text, element.qualifier);
3833
+ } else {
3834
+ localVarTypeQualifiers.delete(nameNode.text);
3835
+ }
2732
3836
  } else if (initNode && JS_LITERAL_ASSIGN_TYPES[initNode.type]) {
2733
3837
  // Literal declaration types the variable (fix #262):
2734
3838
  // `const lines = []` → Array. Annotation, when present,
@@ -2762,6 +3866,7 @@ function findCallsInCode(code, parser) {
2762
3866
  if (node.type === 'assignment_expression') {
2763
3867
  const left = node.childForFieldName('left');
2764
3868
  const right = node.childForFieldName('right');
3869
+ memberValueReference(right);
2765
3870
  if (left?.type === 'identifier') {
2766
3871
  if (right?.type === 'new_expression') {
2767
3872
  nonCallableNames.add(left.text);
@@ -2884,6 +3989,7 @@ function findCallsInCode(code, parser) {
2884
3989
  const optionsMethod = funcNode.text === 'fetch'
2885
3990
  ? getOptionsMethod(node, 1)
2886
3991
  : null;
3992
+ const requestConfig = getRequestConfig(node);
2887
3993
  calls.push({
2888
3994
  name: funcNode.text,
2889
3995
  ...(resolvedName && { resolvedName }),
@@ -2892,12 +3998,19 @@ function findCallsInCode(code, parser) {
2892
3998
  callStart: node.startIndex,
2893
3999
  callEnd: node.endIndex,
2894
4000
  isMethod: false,
4001
+ // A local binding of the name (fix #397): the call runs
4002
+ // the local's value, not a same-name outer definition.
4003
+ ...localShadowFields(funcNode, funcNode.text),
4004
+ ...destructuredFields(funcNode),
2895
4005
  ...(assignedTo && { assignedTo }),
2896
4006
  ...assignedIterFields,
2897
4007
  enclosingFunction,
2898
- uncertain,
4008
+ // Recorded only when true (fix #397: a `false` on every
4009
+ // record was a twelfth of a JS calls cache).
4010
+ ...(uncertain && { uncertain: true }),
2899
4011
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
2900
- ...(optionsMethod && { optionsMethod })
4012
+ ...(optionsMethod && { optionsMethod }),
4013
+ ...(requestConfig && { requestConfig })
2901
4014
  });
2902
4015
  } else if (funcNode.type === 'super') {
2903
4016
  // super(config) — the subclass constructor invoking the
@@ -2914,12 +4027,11 @@ function findCallsInCode(code, parser) {
2914
4027
  receiver: 'super',
2915
4028
  argCount: node.childForFieldName('arguments')?.namedChildCount ?? 0,
2916
4029
  enclosingFunction,
2917
- uncertain: false,
2918
4030
  });
2919
4031
  } else if (funcNode.type === 'member_expression') {
2920
4032
  // Method call: obj.foo() or foo.call/apply/bind()
2921
4033
  const propNode = funcNode.childForFieldName('property');
2922
- const objNode = funcNode.childForFieldName('object');
4034
+ let objNode = funcNode.childForFieldName('object');
2923
4035
 
2924
4036
  if (propNode) {
2925
4037
  const propName = propNode.text;
@@ -2938,6 +4050,7 @@ function findCallsInCode(code, parser) {
2938
4050
  line: node.startPosition.row + 1,
2939
4051
  isMethod: false,
2940
4052
  boundCall: true,
4053
+ ...destructuredFields(objNode),
2941
4054
  enclosingFunction
2942
4055
  });
2943
4056
  } else if (objNode.type === 'member_expression') {
@@ -2976,112 +4089,25 @@ function findCallsInCode(code, parser) {
2976
4089
  receiverLocalBinding: true,
2977
4090
  }),
2978
4091
  enclosingFunction,
2979
- uncertain
4092
+ ...(uncertain && { uncertain: true }),
2980
4093
  });
2981
4094
  }
2982
4095
  }
2983
4096
  } else {
2984
4097
  // Regular method call: obj.foo()
2985
- // Extract receiver: handles identifiers (obj), this, super
2986
- let receiver = undefined;
2987
- if (objNode) {
2988
- if (objNode.type === 'identifier' || objNode.type === 'this' || objNode.type === 'super') {
2989
- receiver = objNode.text;
2990
- }
2991
- }
2992
- // One-hop field receiver (fix #219 — #202's shape for
2993
- // structural): this._map.has(x) / def.cache.get(k) —
2994
- // receiverRoot/Field let findCallers hop to the
2995
- // field's DECLARED type annotation. `this`-rooted hops
2996
- // resolve their root type query-side (the enclosing
2997
- // class); identifier roots type from local annotations.
2998
- let receiverRoot, receiverFieldName, receiverRootType, receiverBindingNode;
2999
- let receiverDeepPath = false;
3000
- if (receiver && objNode?.type === 'identifier') receiverBindingNode = objNode;
3001
- if (!receiver && objNode && objNode.type === 'member_expression') {
3002
- const rootNode = objNode.childForFieldName('object');
3003
- const fldNode = objNode.childForFieldName('property');
3004
- if (fldNode && rootNode &&
3005
- (rootNode.type === 'identifier' || rootNode.type === 'this')) {
3006
- receiverRoot = rootNode.text;
3007
- receiverBindingNode = rootNode.type === 'identifier' ? rootNode : undefined;
3008
- receiverFieldName = fldNode.text;
3009
- if (rootNode.type === 'identifier') {
3010
- receiverRootType = localVarTypes.get(rootNode.text);
3011
- }
3012
- } else {
3013
- // Preserve unresolved deeper member chains
3014
- // (`client.req.query()`). Their terminal name
3015
- // must not borrow a same-file method binding
3016
- // while the root object's type is unknown.
3017
- receiverDeepPath = true;
3018
- }
3019
- }
3020
- // Chained receiver (fix #219): the receiver IS a call —
3021
- // parseAsync(args).catch(...) — record the producer so
3022
- // findCallers can type the receiver from its declared
3023
- // return annotation (Promise<...> → Promise).
3024
- let receiverCall, receiverCallIsMethod, receiverCallAwaited, receiverCallLine;
3025
- let receiverCallStart, receiverCallEnd;
3026
- {
3027
- let recvNode = objNode;
3028
- if (recvNode && recvNode.type === 'parenthesized_expression') {
3029
- recvNode = recvNode.namedChild(0);
3030
- }
3031
- if (recvNode && recvNode.type === 'await_expression') {
3032
- receiverCallAwaited = true;
3033
- recvNode = recvNode.namedChild(0);
3034
- }
3035
- if (recvNode && recvNode.type === 'call_expression') {
3036
- const prodFunc = recvNode.childForFieldName('function');
3037
- if (prodFunc?.type === 'identifier') {
3038
- receiverCall = prodFunc.text;
3039
- // Producer link (fix #258): plain-call
3040
- // records carry the call node's start line
3041
- receiverCallLine = recvNode.startPosition.row + 1;
3042
- receiverCallStart = recvNode.startIndex;
3043
- receiverCallEnd = recvNode.endIndex;
3044
- } else if (prodFunc?.type === 'member_expression') {
3045
- const prodProp = prodFunc.childForFieldName('property');
3046
- if (prodProp) {
3047
- receiverCall = prodProp.text;
3048
- receiverCallIsMethod = true;
3049
- // Method records report the property
3050
- // node's own line
3051
- receiverCallLine = prodProp.startPosition.row + 1;
3052
- receiverCallStart = recvNode.startIndex;
3053
- receiverCallEnd = recvNode.endIndex;
3054
- }
3055
- }
3056
- }
3057
- if (!receiverCall) receiverCallAwaited = undefined;
3058
- }
3059
- // Literal receivers carry their builtin type: [].map() can
3060
- // never be a project class method
3061
- // A freshly constructed receiver has an exact runtime
3062
- // type as well: new Service().start(). Recording it here
3063
- // avoids treating the call as an untyped method dispatch.
3064
- const constructedReceiverType = objNode?.type === 'new_expression'
3065
- ? jsConstructorTypeName(objNode.childForFieldName('constructor'))
3066
- : undefined;
3067
- const constructedReceiverQualifier = objNode?.type === 'new_expression'
3068
- ? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
3069
- : undefined;
3070
- const indexedReceiver = indexedArrayReceiver(objNode);
3071
- const receiverType = indexedReceiver?.type || (receiver
3072
- ? localVarTypes.get(receiver)
3073
- : (constructedReceiverType ||
3074
- (objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined)));
3075
- // Module receiver (ns.helper()) — unless locally shadowed
3076
- // by a typed instance binding
3077
- const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
3078
- const receiverIsModule = !!receiverModuleSpecifier ||
3079
- (!!receiver && moduleAliases.has(receiver) &&
3080
- !localVarTypes.has(receiver));
3081
- const receiverModuleComposition = receiver &&
3082
- !unsafeModuleCompositions.has(receiver)
3083
- ? moduleCompositions.get(receiver) : undefined;
4098
+ const receiverFacts = memberReceiverFacts(objNode, propName);
3084
4099
  const firstArg = getFirstStringArg(node);
4100
+ const requestConfig = getRequestConfig(node);
4101
+ const handlerArgs = firstArg && ROUTE_VERB_METHODS.test(propNode.text) &&
4102
+ getArgCount(node) >= 2 ? getHandlerArgs(node) : null;
4103
+ // fix #366: `server.register(plugin, { prefix })` mounts
4104
+ // the plugin's routes under the prefix.
4105
+ const registerMount = propNode.text === 'register' ? getRegisterMount(node) : null;
4106
+ // fix #366: the argument shapes of a router mount
4107
+ // (`app.use('/api', router)`, `app.route('/v2', api)`):
4108
+ // identifier names, or '()' for any other expression.
4109
+ const mountArgs = (propNode.text === 'use' || propNode.text === 'route') &&
4110
+ getArgCount(node) >= 2 ? getMountArgs(node) : null;
3085
4111
  const argCount = getArgCount(node);
3086
4112
  let assignedTo = jsAssignmentTargetOf(node);
3087
4113
  let assignedIterFields = {};
@@ -3106,45 +4132,20 @@ function findCallsInCode(code, parser) {
3106
4132
  callStart: node.startIndex,
3107
4133
  callEnd: node.endIndex,
3108
4134
  isMethod: true,
3109
- receiver,
3110
- ...(receiverType && { receiverType,
3111
- ...(indexedReceiver ? { receiverTypeSource: 'annotation',
3112
- receiverTypeEvidence: indexedReceiver.evidence }
3113
- : receiver ? localVarTypes.fields(receiver, receiverType) : {
3114
- receiverTypeSource: constructedReceiverType ? 'constructor' : 'literal',
3115
- receiverTypeEvidence: typeOrigin(constructedReceiverType ? 'constructor' : 'literal', objNode),
3116
- }),
3117
- }),
3118
- ...((indexedReceiver?.qualifier || constructedReceiverQualifier ||
3119
- (receiver && localVarTypeQualifiers.get(receiver))) && {
3120
- receiverTypeQualifier: indexedReceiver?.qualifier || constructedReceiverQualifier ||
3121
- localVarTypeQualifiers.get(receiver),
3122
- }),
3123
- ...(receiverIsModule && { receiverIsModule: true }),
3124
- ...(receiverModuleSpecifier && { receiverModuleSpecifier }),
3125
- ...(receiverModuleComposition && {
3126
- receiverModuleComposition,
3127
- }),
3128
- ...(receiver && assignedMembers.has(`${receiver}.${propName}`) && {
3129
- receiverMemberAssigned: true,
3130
- }),
3131
- ...(receiverBindingNode &&
3132
- isShadowedByLocal(receiverBindingNode, receiverBindingNode.text) &&
3133
- { receiverLocalBinding: true }),
3134
- ...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
3135
- ...(receiverFieldName && receiverRootType && { receiverRootType }),
3136
- ...(receiverDeepPath && { receiverDeepPath: true }),
3137
- ...(receiverCall && { receiverCall }),
3138
- ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
3139
- ...(receiverCallAwaited && { receiverCallAwaited: true }),
3140
- ...(receiverCallLine && { receiverCallLine }),
3141
- ...(receiverCallStart != null && { receiverCallStart }),
3142
- ...(receiverCallEnd != null && { receiverCallEnd }),
4135
+ // `await x.m()` and `f(x.m())` (fix #402): the
4136
+ // value is awaited or passed on; audit-async skips them.
4137
+ ...(directlyAwaited(node, 'await_expression') && { awaited: true }),
4138
+ ...(directlyAwaited(node, 'arguments') && { passedAsArgument: true }),
4139
+ ...receiverFacts,
3143
4140
  ...(assignedTo && { assignedTo }),
3144
4141
  ...assignedIterFields,
3145
4142
  enclosingFunction,
3146
- uncertain,
4143
+ ...(uncertain && { uncertain: true }),
3147
4144
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
4145
+ ...(requestConfig && { requestConfig }),
4146
+ ...(registerMount && { registerMount }),
4147
+ ...(mountArgs && { mountArgs }),
4148
+ ...(handlerArgs && { handlerArgs }),
3148
4149
  argCount
3149
4150
  });
3150
4151
  }
@@ -3288,13 +4289,17 @@ function findCallsInCode(code, parser) {
3288
4289
  if (ctorNode) {
3289
4290
  const enclosingFunction = getCurrentEnclosingFunction();
3290
4291
 
4292
+ // fix #366: `new Router({ prefix: '/api' })` (koa-router)
4293
+ // carries a constructor mount prefix.
4294
+ const ctorPrefix = hasObjectKeyArg(node, 'prefix');
3291
4295
  if (ctorNode.type === 'identifier') {
3292
4296
  calls.push({
3293
4297
  name: ctorNode.text,
3294
4298
  line: node.startPosition.row + 1,
3295
4299
  isMethod: false,
3296
4300
  isConstructor: true,
3297
- ...(isShadowedByLocal(ctorNode, ctorNode.text) && { localShadow: true }),
4301
+ ...localShadowFields(ctorNode, ctorNode.text),
4302
+ ...(ctorPrefix && { prefixOption: true }),
3298
4303
  enclosingFunction
3299
4304
  });
3300
4305
  } else if (ctorNode.type === 'member_expression') {
@@ -3306,6 +4311,7 @@ function findCallsInCode(code, parser) {
3306
4311
  line: node.startPosition.row + 1,
3307
4312
  isMethod: true,
3308
4313
  isConstructor: true,
4314
+ ...(ctorPrefix && { prefixOption: true }),
3309
4315
  enclosingFunction
3310
4316
  });
3311
4317
  }
@@ -3730,6 +4736,7 @@ function findImportsInCode(code, parser) {
3730
4736
  const names = [];
3731
4737
  const esmRenames = [];
3732
4738
  let importType = 'named';
4739
+ let defaultName = null;
3733
4740
  let typeOnly = hasTypeKeyword(node);
3734
4741
  let specifierCount = 0;
3735
4742
  let typeSpecifierCount = 0;
@@ -3764,9 +4771,11 @@ function findImportsInCode(code, parser) {
3764
4771
  for (let j = 0; j < child.namedChildCount; j++) {
3765
4772
  const clauseChild = child.namedChild(j);
3766
4773
  if (clauseChild.type === 'identifier') {
3767
- // Default import: import foo from 'x'
3768
- names.push(clauseChild.text);
3769
- importType = 'default';
4774
+ // Default import: import foo from 'x'. Beside
4775
+ // named or namespace imports it is its own
4776
+ // record, so its binding stays a default one
4777
+ // (fix #402).
4778
+ defaultName = clauseChild.text;
3770
4779
  hasValueBinding = true;
3771
4780
  } else if (clauseChild.type === 'named_imports') {
3772
4781
  // Named imports: import { a, b } from 'x'
@@ -3800,6 +4809,13 @@ function findImportsInCode(code, parser) {
3800
4809
  }
3801
4810
 
3802
4811
  if (modulePath) {
4812
+ if (defaultName && names.length === 0) {
4813
+ names.push(defaultName);
4814
+ importType = 'default';
4815
+ } else if (defaultName) {
4816
+ imports.push({ module: modulePath, names: [defaultName], type: 'default', line,
4817
+ ...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
4818
+ }
3803
4819
  if (names.length === 0) {
3804
4820
  // Side-effect import: import 'x'
3805
4821
  importType = 'side-effect';
@@ -3808,9 +4824,11 @@ function findImportsInCode(code, parser) {
3808
4824
  importType === 'named' && !hasValueBinding) {
3809
4825
  typeOnly = true;
3810
4826
  }
3811
- imports.push({ module: modulePath, names, type: importType, line,
3812
- ...(esmRenames.length > 0 && { renames: esmRenames }),
3813
- ...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
4827
+ if (names.length > 0 || !defaultName) {
4828
+ imports.push({ module: modulePath, names, type: importType, line,
4829
+ ...(esmRenames.length > 0 && { renames: esmRenames }),
4830
+ ...(typeOnly && { deferred: true, deferredReason: 'type-only' }) });
4831
+ }
3814
4832
  }
3815
4833
  return true;
3816
4834
  }
@@ -3885,6 +4903,28 @@ function findImportsInCode(code, parser) {
3885
4903
  // Check parent for variable name
3886
4904
  let parent = node.parent;
3887
4905
  let defaultLike = false;
4906
+ // `const stringify = require('url').format` binds the
4907
+ // module's member `format` under the local name (fix
4908
+ // #397, koa-measured): a renamed named import, never the
4909
+ // module value nor a binding of `stringify` itself.
4910
+ const memberOfRequire = parent?.type === 'member_expression' &&
4911
+ sameNode(parent.childForFieldName('object'), node) &&
4912
+ parent.childForFieldName('property')?.type === 'property_identifier' &&
4913
+ parent.parent?.type === 'variable_declarator' &&
4914
+ sameNode(parent.parent.childForFieldName('value'), parent)
4915
+ ? parent.childForFieldName('property').text : null;
4916
+ if (memberOfRequire) {
4917
+ const nameNode = parent.parent.childForFieldName('name');
4918
+ if (nameNode?.type === 'identifier') {
4919
+ names.push(memberOfRequire);
4920
+ if (nameNode.text !== memberOfRequire) {
4921
+ if (!importAliases) importAliases = [];
4922
+ importAliases.push({ original: memberOfRequire, local: nameNode.text });
4923
+ renames.push({ original: memberOfRequire, local: nameNode.text });
4924
+ }
4925
+ }
4926
+ parent = null;
4927
+ }
3888
4928
  if (parent && parent.type === 'variable_declarator') {
3889
4929
  const nameNode = parent.childForFieldName('name');
3890
4930
  if (nameNode) {
@@ -4281,9 +5321,24 @@ function findExportsInCode(code, parser) {
4281
5321
  * @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
4282
5322
  * @returns {Array<{line: number, column: number, usageType: string}>}
4283
5323
  */
4284
- function findUsagesInCode(code, name, parser, tree) {
5324
+ /** A shorthand property of a module export object (`module.exports = { f }`, `export default { f }`). */
5325
+ function jsExportObjectShorthand(node) {
5326
+ const object = node.parent;
5327
+ if (object?.type !== 'object') return false;
5328
+ const holder = object.parent;
5329
+ if (holder?.type === 'export_statement') return true;
5330
+ if (holder?.type !== 'assignment_expression' ||
5331
+ !sameNode(holder.childForFieldName('right'), object)) return false;
5332
+ const left = holder.childForFieldName('left')?.text?.replace(/\s+/g, '') || '';
5333
+ return left === 'module.exports' || left === 'exports' ||
5334
+ left.startsWith('module.exports.') || left.startsWith('exports.');
5335
+ }
5336
+
5337
+ function findUsagesInCode(code, name, parser, tree, options = {}) {
4285
5338
  tree = tree || parseTree(parser, code);
4286
5339
  const usages = [];
5340
+ // Lexical scope verdicts (fix #392) only for refactoring internals.
5341
+ const scopeMemo = options.lexicalScopes ? new Map() : null;
4287
5342
 
4288
5343
  visitNameNodes(tree, code, name, (node) => {
4289
5344
  // Look for identifier, property_identifier (method names in obj.method() calls),
@@ -4307,6 +5362,7 @@ function findUsagesInCode(code, name, parser, tree) {
4307
5362
 
4308
5363
  // Classify based on parent node
4309
5364
  let usageType = 'reference';
5365
+ let importAliasLocal = false;
4310
5366
 
4311
5367
  if (parent) {
4312
5368
  // Import: identifier inside import_specifier or import_clause
@@ -4314,6 +5370,11 @@ function findUsagesInCode(code, name, parser, tree) {
4314
5370
  parent.type === 'import_clause' ||
4315
5371
  parent.type === 'namespace_import') {
4316
5372
  usageType = 'import';
5373
+ // `import { g as f }`: f is the importer's local alias.
5374
+ const imported = parent.type === 'import_specifier' ? parent.childForFieldName('name') : null;
5375
+ if (imported && sameNode(parent.childForFieldName('alias'), node) && imported.text !== node.text) {
5376
+ importAliasLocal = true;
5377
+ }
4317
5378
  }
4318
5379
  // Call: identifier is function in call_expression
4319
5380
  else if (parent.type === 'call_expression' &&
@@ -4344,6 +5405,8 @@ function findUsagesInCode(code, name, parser, tree) {
4344
5405
  }
4345
5406
  // Unwrap require('./x').member — still an import binding
4346
5407
  if (value && value.type === 'member_expression') {
5408
+ // `const f = require('m').g`: f is a local alias of g.
5409
+ if (value.childForFieldName('property')?.text !== node.text) importAliasLocal = true;
4347
5410
  value = value.childForFieldName('object');
4348
5411
  }
4349
5412
  if (value && value.type === 'call_expression') {
@@ -4422,7 +5485,17 @@ function findUsagesInCode(code, name, parser, tree) {
4422
5485
  }
4423
5486
  }
4424
5487
 
4425
- usages.push({ line, column, usageType });
5488
+ // Where a bare reference resolves (fix #392).
5489
+ const scope = scopeMemo && usageType === 'reference'
5490
+ ? scopeFields(referenceScope(node, 'javascript', scopeMemo)) : null;
5491
+ // An object-literal shorthand `{ name }` is a key AND a reference to
5492
+ // the binding (fix #397): a rename of the binding keeps the key
5493
+ // (`{ name: renamed }`), except in a module's export object, whose
5494
+ // keys are the export names a rename follows into every importer.
5495
+ const shorthandProperty = scopeMemo && node.type === 'shorthand_property_identifier' &&
5496
+ !jsExportObjectShorthand(node);
5497
+ usages.push({ line, column, usageType, ...scope, ...(shorthandProperty && { shorthandProperty: true }),
5498
+ ...(importAliasLocal && usageType === 'import' && { importAlias: true }) });
4426
5499
  return true;
4427
5500
  });
4428
5501