ucn 5.4.2 → 5.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) 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 +27 -16
  6. package/cli/index.js +45 -14
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +62 -11
  9. package/core/analysis.js +517 -266
  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 +529 -31
  16. package/core/callers.js +12228 -921
  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.js +18 -2
  38. package/core/output/brief.js +3 -0
  39. package/core/output/doctor.js +4 -0
  40. package/core/output/endpoints.js +15 -6
  41. package/core/output/find.js +7 -2
  42. package/core/output/refactoring.js +121 -14
  43. package/core/output/reporting.js +31 -3
  44. package/core/output/shared.js +14 -0
  45. package/core/output-budget.js +305 -122
  46. package/core/parallel-build.js +124 -47
  47. package/core/parser.js +3 -2
  48. package/core/project.js +472 -66
  49. package/core/provenance-facts.js +156 -12
  50. package/core/provenance-overload.js +30 -3
  51. package/core/provenance.js +104 -30
  52. package/core/pytest-fixtures.js +434 -0
  53. package/core/python-fixture-flow.js +17 -3
  54. package/core/python-modules.js +173 -0
  55. package/core/reflection.js +877 -0
  56. package/core/registry.js +1 -1
  57. package/core/reporting.js +234 -52
  58. package/core/request-helpers.js +561 -0
  59. package/core/route-graph.js +1922 -0
  60. package/core/route-lists.js +2414 -0
  61. package/core/rust-macro-expansion.js +3026 -0
  62. package/core/rust-modules.js +203 -0
  63. package/core/search.js +121 -29
  64. package/core/shared.js +19 -1
  65. package/core/test-name-bindings.js +256 -0
  66. package/core/type-denotation.js +1185 -0
  67. package/core/type-references.js +3229 -0
  68. package/core/verify.js +3360 -672
  69. package/languages/adapter.js +31 -5
  70. package/languages/c-family.js +4061 -212
  71. package/languages/c-macro-invocations.js +380 -0
  72. package/languages/c-preprocessor.js +254 -0
  73. package/languages/csharp.js +1406 -49
  74. package/languages/go.js +1243 -70
  75. package/languages/index.js +728 -6
  76. package/languages/java.js +473 -61
  77. package/languages/javascript.js +1270 -197
  78. package/languages/lexical-scope.js +1280 -0
  79. package/languages/python.js +1557 -72
  80. package/languages/rust-macro-rules.js +1020 -0
  81. package/languages/rust-value-flow.js +416 -0
  82. package/languages/rust.js +2265 -112
  83. package/languages/utils.js +298 -4
  84. package/mcp/server.js +22 -8
  85. package/package.json +1 -1
@@ -0,0 +1,1729 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * core/async-producers.js - what calling an async definition RETURNS (fix #364).
5
+ *
6
+ * audit-async flags a call as "missing await" only when the callee returns an
7
+ * awaitable. The `async` keyword alone does not decide that:
8
+ * - coroutine `async def` / `async function` / C# `async Task`: awaitable
9
+ * - iterator async generators (`async def` with an own-body `yield`,
10
+ * `async function*`, C# `async IAsyncEnumerable` iterators):
11
+ * consumed by `async for` / `for await` / `await foreach`,
12
+ * never awaited
13
+ * - context-manager a decorator resolving to the language's async
14
+ * context-manager factory: entered by `async with`
15
+ * - void C# `async void`: nothing to await
16
+ * - unknown a decorator whose effect is not known replaces the
17
+ * callable, or same-name definitions disagree: not audited
18
+ * The callee definition is resolved lexically first (nearest enclosing scope
19
+ * that binds the name: a local def, a parameter, a variable), then by the
20
+ * call site's proven import identity. A same-name function elsewhere in the
21
+ * project is only a candidate, not evidence of an async producer.
22
+ */
23
+
24
+ const path = require('path');
25
+ const { langTraits } = require('../languages');
26
+ const { sameNode } = require('../languages/utils');
27
+
28
+ const CALLABLE_TYPES = new Set(['function', 'method', 'constructor', 'arrow']);
29
+
30
+ function isCallableDef(def) {
31
+ return !!def && (CALLABLE_TYPES.has(def.type) || def.params != null || def.paramsStructured != null);
32
+ }
33
+
34
+ function isDefAsync(def) {
35
+ return def.isAsync === true || (Array.isArray(def.modifiers) && def.modifiers.includes('async'));
36
+ }
37
+
38
+ /**
39
+ * Resolve a decorator expression to its qualified identity through the
40
+ * defining file's import bindings. Returns { name, called } or null.
41
+ */
42
+ function resolveDecorator(text, fileEntry, vocab) {
43
+ let expr = String(text || '').trim();
44
+ let called = false;
45
+ const paren = expr.indexOf('(');
46
+ if (paren >= 0) {
47
+ called = true;
48
+ expr = expr.slice(0, paren).trim();
49
+ }
50
+ if (!/^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*$/.test(expr)) return null;
51
+ const dot = expr.indexOf('.');
52
+ const head = dot < 0 ? expr : expr.slice(0, dot);
53
+ const rest = dot < 0 ? '' : expr.slice(dot);
54
+ const binding = (fileEntry?.importBindings || []).find(b =>
55
+ (b.alias || b.name) === head && b.module && (b.kind === 'from' || b.kind === 'import'));
56
+ let name = null;
57
+ if (binding) {
58
+ name = binding.kind === 'from'
59
+ ? `${binding.module}.${binding.name}${rest}`
60
+ : `${binding.module}${rest}`;
61
+ } else if (!rest && (vocab.builtins || []).includes(head)) {
62
+ name = `builtins.${head}`;
63
+ }
64
+ return name ? { name, called } : null;
65
+ }
66
+
67
+ /**
68
+ * Classify one definition. Memoized per definition object in `memo`.
69
+ */
70
+ function producerKind(index, def, memo) {
71
+ if (memo.has(def)) return memo.get(def);
72
+ let kind;
73
+ if (!isDefAsync(def)) {
74
+ kind = 'sync';
75
+ } else if (typeof def.returnType === 'string' && def.returnType.trim() === 'void') {
76
+ kind = 'void';
77
+ } else {
78
+ // The body decides first; a wrapping decorator then replaces it.
79
+ kind = def.isGenerator ? 'iterator' : 'coroutine';
80
+ const decorators = Array.isArray(def.decorators) ? def.decorators : [];
81
+ const fileEntry = decorators.length > 0 ? index.files.get(def.file) : null;
82
+ const traits = fileEntry ? langTraits(fileEntry.language) : null;
83
+ if (traits?.decoratorsWrapCallables) {
84
+ const vocab = traits.callableDecorators || {};
85
+ let contextManager = false;
86
+ for (const decorator of decorators) {
87
+ const resolved = resolveDecorator(decorator, fileEntry, vocab);
88
+ const key = resolved ? resolved.name + (resolved.called ? '()' : '') : null;
89
+ if (key && !contextManager && (vocab.asyncContextManagers || []).includes(key)) {
90
+ contextManager = true;
91
+ } else if (!key || !(vocab.transparent || []).includes(key)) {
92
+ kind = 'unknown';
93
+ break;
94
+ }
95
+ }
96
+ if (contextManager && kind !== 'unknown') kind = 'context-manager';
97
+ }
98
+ }
99
+ memo.set(def, kind);
100
+ return kind;
101
+ }
102
+
103
+ /**
104
+ * Collapse the kinds of the definitions a call may reach.
105
+ * `requireAllAsync`: a sync definition anywhere makes the name ambiguous
106
+ * (project-wide lookup); otherwise the async definitions win (file-local).
107
+ * Returns null (not an async producer), a kind, or 'unknown' (disagreeing).
108
+ */
109
+ function collapseKinds(index, defs, memo, requireAllAsync) {
110
+ const kinds = new Set();
111
+ let sync = false;
112
+ for (const def of defs) {
113
+ if (!isCallableDef(def)) continue;
114
+ const kind = producerKind(index, def, memo);
115
+ if (kind === 'sync') sync = true;
116
+ else kinds.add(kind);
117
+ }
118
+ if (kinds.size === 0 || (sync && requireAllAsync)) return null;
119
+ return kinds.size === 1 ? [...kinds][0] : 'unknown';
120
+ }
121
+
122
+ // ── Lexical resolution (structural languages resolve bare calls by scope) ──
123
+
124
+ const PY_SCOPES = new Set(['function_definition', 'lambda', 'module', 'class_definition',
125
+ 'list_comprehension', 'set_comprehension', 'dictionary_comprehension', 'generator_expression']);
126
+ const PY_NESTED = new Set(['function_definition', 'lambda', 'class_definition',
127
+ 'list_comprehension', 'set_comprehension', 'dictionary_comprehension', 'generator_expression']);
128
+ const JS_FUNCTIONS = new Set(['function_declaration', 'function_expression', 'arrow_function',
129
+ 'method_definition', 'generator_function', 'generator_function_declaration']);
130
+ const JS_FUNCTION_VALUES = new Set(['function_expression', 'arrow_function', 'generator_function']);
131
+ const JS_SCOPES = new Set([...JS_FUNCTIONS, 'statement_block', 'program', 'for_statement',
132
+ 'for_in_statement', 'catch_clause']);
133
+
134
+ const LEXICAL_FAMILY = {
135
+ python: 'python',
136
+ javascript: 'js', typescript: 'js', tsx: 'js', html: 'js',
137
+ };
138
+
139
+ function addBinding(map, name, entry) {
140
+ if (!name) return;
141
+ if (!map.has(name)) map.set(name, []);
142
+ map.get(name).push(entry);
143
+ }
144
+
145
+ function collectPatternNames(node, out, skipFields) {
146
+ if (!node) return;
147
+ if (node.type === 'identifier' || node.type === 'shorthand_property_identifier_pattern') {
148
+ out.push(node.text);
149
+ return;
150
+ }
151
+ for (let i = 0; i < node.namedChildCount; i++) {
152
+ const child = node.namedChild(i);
153
+ if (skipFields.some(field => sameNode(node.childForFieldName(field), child))) continue;
154
+ if (child.type === 'type_annotation' || child.type === 'type') continue;
155
+ collectPatternNames(child, out, skipFields);
156
+ }
157
+ }
158
+
159
+ function pythonScopeBindings(scope) {
160
+ const map = new Map();
161
+ const bindTarget = (target, entry) => {
162
+ const names = [];
163
+ collectPatternNames(target, names, ['value', 'subscript', 'attribute', 'object']);
164
+ if (target && (target.type === 'attribute' || target.type === 'subscript')) return;
165
+ for (const name of names) addBinding(map, name, entry);
166
+ };
167
+ if (scope.type === 'function_definition' || scope.type === 'lambda') {
168
+ const params = scope.childForFieldName('parameters');
169
+ for (const param of params?.namedChildren || []) {
170
+ const names = [];
171
+ const nameNode = param.childForFieldName('name');
172
+ if (nameNode) names.push(nameNode.text);
173
+ else collectPatternNames(param, names, ['value', 'type']);
174
+ for (const name of names) addBinding(map, name, { kind: 'binding' });
175
+ }
176
+ }
177
+ if (PY_NESTED.has(scope.type) && scope.type !== 'function_definition' &&
178
+ scope.type !== 'lambda' && scope.type !== 'class_definition') {
179
+ for (const clause of scope.namedChildren) {
180
+ if (clause.type === 'for_in_clause') bindTarget(clause.childForFieldName('left'), { kind: 'binding' });
181
+ }
182
+ return map;
183
+ }
184
+ const body = scope.type === 'module' ? scope : scope.childForFieldName('body');
185
+ const stack = body ? [body] : [];
186
+ while (stack.length > 0) {
187
+ const node = stack.pop();
188
+ for (let i = 0; i < node.namedChildCount; i++) {
189
+ const child = node.namedChild(i);
190
+ switch (child.type) {
191
+ case 'function_definition':
192
+ addBinding(map, child.childForFieldName('name')?.text, { kind: 'def', node: child });
193
+ continue;
194
+ case 'class_definition':
195
+ addBinding(map, child.childForFieldName('name')?.text, { kind: 'binding' });
196
+ continue;
197
+ case 'lambda': case 'list_comprehension': case 'set_comprehension':
198
+ case 'dictionary_comprehension': case 'generator_expression':
199
+ continue;
200
+ case 'import_statement': case 'import_from_statement':
201
+ case 'future_import_statement': case 'global_statement':
202
+ case 'nonlocal_statement': {
203
+ // Resolved through import identity, or not ours. The
204
+ // module a from-import reads is not bound by it.
205
+ const source = child.type === 'import_from_statement'
206
+ ? child.childForFieldName('module_name') : null;
207
+ for (const name of child.namedChildren) {
208
+ if (source && sameNode(name, source)) continue;
209
+ if (name.type === 'wildcard_import') {
210
+ // `from m import *` binds whichever names m
211
+ // exports; only a module-level one is known.
212
+ addBinding(map, '*', { kind: 'external', module: source?.text || null,
213
+ topLevel: child.parent?.type === 'module' });
214
+ continue;
215
+ }
216
+ const alias = name.childForFieldName('alias') || name.childForFieldName('name') || name;
217
+ const text = alias.type === 'identifier' ? alias.text : alias.text.split('.')[0];
218
+ // A from-import names the module and the name it reads there.
219
+ const imported = source ? (name.type === 'aliased_import'
220
+ ? name.childForFieldName('name') : name)?.text : null;
221
+ addBinding(map, text, imported
222
+ ? { kind: 'external', module: source.text, imported } : { kind: 'external' });
223
+ }
224
+ continue;
225
+ }
226
+ case 'assignment': case 'augmented_assignment':
227
+ bindTarget(child.childForFieldName('left'), { kind: 'binding' });
228
+ break;
229
+ case 'named_expression':
230
+ addBinding(map, child.childForFieldName('name')?.text, { kind: 'binding' });
231
+ break;
232
+ case 'for_statement':
233
+ bindTarget(child.childForFieldName('left'), { kind: 'binding' });
234
+ break;
235
+ case 'as_pattern_target':
236
+ bindTarget(child, { kind: 'binding' });
237
+ continue;
238
+ case 'except_clause':
239
+ for (const part of child.namedChildren) {
240
+ if (part.type === 'as_pattern') bindTarget(part.childForFieldName('alias'), { kind: 'binding' });
241
+ }
242
+ break;
243
+ default:
244
+ break;
245
+ }
246
+ stack.push(child);
247
+ }
248
+ }
249
+ return map;
250
+ }
251
+
252
+ function jsScopeBindings(scope) {
253
+ const map = new Map();
254
+ if (JS_FUNCTIONS.has(scope.type)) {
255
+ const params = scope.childForFieldName('parameters') || scope.childForFieldName('parameter');
256
+ const names = [];
257
+ if (params?.type === 'identifier') names.push(params.text);
258
+ else collectPatternNames(params, names, ['right', 'value', 'type']);
259
+ for (const name of names) addBinding(map, name, { kind: 'binding' });
260
+ return map;
261
+ }
262
+ if (scope.type === 'catch_clause') {
263
+ const names = [];
264
+ collectPatternNames(scope.childForFieldName('parameter'), names, ['right', 'value']);
265
+ for (const name of names) addBinding(map, name, { kind: 'binding' });
266
+ return map;
267
+ }
268
+ if (scope.type === 'for_statement' || scope.type === 'for_in_statement') {
269
+ const init = scope.childForFieldName('initializer') || scope.childForFieldName('left');
270
+ const names = [];
271
+ if (init?.type === 'lexical_declaration' || init?.type === 'variable_declaration') {
272
+ for (const decl of init.namedChildren) collectPatternNames(decl.childForFieldName('name'), names, []);
273
+ } else if (scope.type === 'for_in_statement' && init) {
274
+ collectPatternNames(init, names, ['right', 'value']);
275
+ }
276
+ for (const name of names) addBinding(map, name, { kind: 'binding' });
277
+ return map;
278
+ }
279
+ for (const child of scope.namedChildren) {
280
+ let statement = child;
281
+ if (statement.type === 'export_statement') {
282
+ statement = statement.childForFieldName('declaration') || statement;
283
+ }
284
+ switch (statement.type) {
285
+ case 'function_declaration': case 'generator_function_declaration':
286
+ addBinding(map, statement.childForFieldName('name')?.text, { kind: 'def', node: statement });
287
+ break;
288
+ case 'class_declaration':
289
+ addBinding(map, statement.childForFieldName('name')?.text, { kind: 'binding' });
290
+ break;
291
+ case 'lexical_declaration': case 'variable_declaration':
292
+ for (const decl of statement.namedChildren) {
293
+ if (decl.type !== 'variable_declarator') continue;
294
+ const nameNode = decl.childForFieldName('name');
295
+ const value = decl.childForFieldName('value');
296
+ if (nameNode?.type === 'identifier' && value && JS_FUNCTION_VALUES.has(value.type)) {
297
+ addBinding(map, nameNode.text, { kind: 'def', node: decl });
298
+ } else {
299
+ const names = [];
300
+ collectPatternNames(nameNode, names, []);
301
+ for (const name of names) addBinding(map, name, { kind: 'binding' });
302
+ }
303
+ }
304
+ break;
305
+ case 'import_statement': {
306
+ // Each import records the module and the name it reads
307
+ // there (a default import reads `default`; a namespace
308
+ // import binds the module object itself).
309
+ const source = statement.childForFieldName('source');
310
+ const module = source?.namedChildren.find(part => part.type === 'string_fragment')?.text || null;
311
+ for (const clause of statement.namedChildren) {
312
+ if (clause.type !== 'import_clause') continue;
313
+ const bind = (local, imported) => addBinding(map, local.text,
314
+ module && imported ? { kind: 'external', module, imported } : { kind: 'external' });
315
+ for (const part of clause.namedChildren) {
316
+ if (part.type === 'identifier') bind(part, 'default');
317
+ else if (part.type === 'named_imports') {
318
+ for (const specifier of part.namedChildren) {
319
+ if (specifier.type !== 'import_specifier') continue;
320
+ const name = specifier.childForFieldName('name');
321
+ bind(specifier.childForFieldName('alias') || name, name?.text);
322
+ }
323
+ } else if (part.type === 'namespace_import') {
324
+ const local = part.namedChildren.find(child => child.type === 'identifier');
325
+ if (local) bind(local, null);
326
+ }
327
+ }
328
+ }
329
+ break;
330
+ }
331
+ default:
332
+ break;
333
+ }
334
+ }
335
+ return map;
336
+ }
337
+
338
+ /** Callable symbols of a file, grouped by name. */
339
+ function callableSymbolsByName(fileEntry) {
340
+ const byName = new Map();
341
+ for (const symbol of fileEntry?.symbols || []) {
342
+ if (!symbol?.name || !isCallableDef(symbol)) continue;
343
+ if (!byName.has(symbol.name)) byName.set(symbol.name, []);
344
+ byName.get(symbol.name).push(symbol);
345
+ }
346
+ return byName;
347
+ }
348
+
349
+ /** The indexed symbol of a definition whose name token is on `line`. */
350
+ function symbolAtLine(symbolsByName, name, line) {
351
+ let best = null;
352
+ for (const symbol of symbolsByName.get(name) || []) {
353
+ const anchor = symbol.nameLine || symbol.startLine;
354
+ if (anchor === line) return symbol;
355
+ if (symbol.startLine <= line && symbol.endLine >= line &&
356
+ (!best || symbol.startLine > best.startLine)) best = symbol;
357
+ }
358
+ return best;
359
+ }
360
+
361
+ /**
362
+ * Per-file lexical resolver. `resolve(callNode, name)` returns:
363
+ * { defs: [symbol...] } the name binds to local definitions
364
+ * { shadowed: true } a parameter/variable binds the name (not provably async)
365
+ * { external: true } an import or global/nonlocal declaration binds it
366
+ * (`imports`: Python from-imports, when only those bind it)
367
+ * { star: [binding...] } only a Python module's `from m import *` can bind it
368
+ * null no enclosing scope binds it
369
+ */
370
+ function createLexicalResolver(fileEntry, language) {
371
+ const family = LEXICAL_FAMILY[language];
372
+ if (!family) return null;
373
+ const scopes = new Map();
374
+ const bindingsOf = scope => {
375
+ let map = scopes.get(scope.id);
376
+ if (!map) {
377
+ map = family === 'python' ? pythonScopeBindings(scope) : jsScopeBindings(scope);
378
+ scopes.set(scope.id, map);
379
+ }
380
+ return map;
381
+ };
382
+ const symbolsByName = callableSymbolsByName(fileEntry);
383
+ const symbolFor = (entry, name) => {
384
+ const nameNode = entry.node.childForFieldName('name');
385
+ return symbolAtLine(symbolsByName, name, (nameNode || entry.node).startPosition.row + 1);
386
+ };
387
+ return function resolve(callNode, name) {
388
+ let passedFunction = false;
389
+ let star = null;
390
+ for (let scope = callNode.parent; scope; scope = scope.parent) {
391
+ const isScope = family === 'python' ? PY_SCOPES.has(scope.type) : JS_SCOPES.has(scope.type);
392
+ if (!isScope) continue;
393
+ // A class body is not an enclosing scope for the methods it holds.
394
+ if (family === 'python' && scope.type === 'class_definition' && passedFunction) continue;
395
+ if (family === 'python' && (scope.type === 'function_definition' || scope.type === 'lambda')) {
396
+ passedFunction = true;
397
+ }
398
+ const bindings = bindingsOf(scope);
399
+ if (family === 'python' && scope.type === 'module') star = bindings.get('*') || null;
400
+ const entries = bindings.get(name);
401
+ if (!entries) continue;
402
+ if (entries.some(entry => entry.kind === 'external')) {
403
+ return entries.every(entry => entry.kind === 'external' && entry.imported)
404
+ ? { external: true, imports: entries } : { external: true };
405
+ }
406
+ if (entries.some(entry => entry.kind === 'binding')) return { shadowed: true };
407
+ const defs = [];
408
+ for (const entry of entries) {
409
+ const symbol = symbolFor(entry, name);
410
+ // A local definition the index does not hold still binds
411
+ // the name: nothing else can be its producer.
412
+ if (!symbol) return { shadowed: true };
413
+ defs.push(symbol);
414
+ }
415
+ return { defs };
416
+ }
417
+ return star ? { star } : null;
418
+ };
419
+ }
420
+
421
+ /** Program-scope bindings of a JS/TS module, memoized in `memo`. */
422
+ function jsProgramScope(index, file, entry, memo) {
423
+ const key = 'js\0' + file;
424
+ if (!memo.has(key)) {
425
+ let names = null;
426
+ try {
427
+ const tree = index._getParsedTree(file, index._readFile(file), entry.language);
428
+ if (tree) names = jsScopeBindings(tree.rootNode);
429
+ } catch (_) { names = null; }
430
+ memo.set(key, names);
431
+ }
432
+ return memo.get(key);
433
+ }
434
+
435
+ // A module's export of `name` (`default` for the default export), read
436
+ // through re-exports (`export { f as g } from`, `export { default } from`,
437
+ // `export * from`) to the module that declares the function (fix #402).
438
+ // With `want`, only whether some definition satisfies it is kept: a module
439
+ // with no wanted callable of the exported name answers null unread.
440
+ function jsModuleExportDefinitions(index, file, name, memo = new Map(), depth = 0, seen = new Set(), want = null) {
441
+ const key = `${file}\0${name}`;
442
+ if (depth > 8 || seen.has(key)) return null;
443
+ const memoKey = (want ? 'jswant\0' : 'jsexport\0') + key;
444
+ if (depth === 0 && memo.has(memoKey)) return memo.get(memoKey);
445
+ seen.add(key);
446
+ let result = jsModuleExportDefinitionsUncached(index, file, name, memo, depth, seen, want);
447
+ // 'absent' (the module exports no such name) only steers `export *`
448
+ // lookups; callers get null, as for an undecided export.
449
+ if (depth === 0) {
450
+ if (result === ABSENT) result = null;
451
+ memo.set(memoKey, result);
452
+ }
453
+ return result;
454
+ }
455
+ const ABSENT = Object.freeze([]);
456
+
457
+ function jsModuleExportDefinitionsUncached(index, file, name, memo, depth, seen, want) {
458
+ const entry = file ? index.files.get(file) : null;
459
+ if (!entry || LEXICAL_FAMILY[entry.language] !== 'js' || entry.language === 'html') return null;
460
+ const details = (entry.exportDetails || []).filter(detail => !detail.isTypeExport);
461
+ const isDefault = name === 'default';
462
+ const local = details.filter(detail => !detail.source &&
463
+ (isDefault ? detail.type === 'default' : detail.type === 'named' && (detail.alias || detail.name) === name));
464
+ const forwarded = details.filter(detail => detail.source && detail.type === 're-export' &&
465
+ (detail.alias || detail.name) === name);
466
+ const stars = isDefault ? [] : details.filter(detail => detail.source && detail.type === 're-export-all' && !detail.alias);
467
+ if (forwarded.length > 1 || (forwarded.length === 1 && local.length > 0)) return null;
468
+ const resolveSource = specifier => {
469
+ const relative = entry.moduleResolved?.[specifier];
470
+ return relative ? path.resolve(index.root, relative) : null;
471
+ };
472
+ if (forwarded.length === 1) {
473
+ const source = resolveSource(forwarded[0].source);
474
+ const defs = source ? jsModuleExportDefinitions(index, source, forwarded[0].name, memo, depth + 1, seen, want) : null;
475
+ return defs === ABSENT ? null : defs;
476
+ }
477
+ if (local.length === 0) {
478
+ // `export * from` re-exports every named export: exactly one
479
+ // star source may supply the name.
480
+ let found = null;
481
+ for (const star of stars) {
482
+ const source = resolveSource(star.source);
483
+ if (!source) return null;
484
+ const defs = jsModuleExportDefinitions(index, source, name, memo, depth + 1, new Set(seen), want);
485
+ if (defs === ABSENT) continue;
486
+ if (!defs || found) return null;
487
+ found = defs;
488
+ }
489
+ return found || ABSENT;
490
+ }
491
+ const symbols = callableSymbolsByName(entry);
492
+ if (want && !local.some(detail => (symbols.get(detail.name) || []).some(want))) return null;
493
+ const defs = [];
494
+ for (const detail of local) {
495
+ const declared = (symbols.get(detail.name) || []).filter(symbol =>
496
+ !symbol.className && (symbol.startLine === detail.line || symbol.nameLine === detail.line));
497
+ if (declared.length === 1) {
498
+ defs.push(declared[0]);
499
+ continue;
500
+ }
501
+ // No callable of that name in the module: no binding can map to one.
502
+ if (declared.length > 1 || !symbols.has(detail.name)) return null;
503
+ const bound = jsProgramScope(index, file, entry, memo)?.get(detail.name);
504
+ if (!bound || bound.some(binding => binding.kind !== 'def')) return null;
505
+ for (const def of bound) {
506
+ const nameNode = def.node.childForFieldName('name');
507
+ const symbol = symbolAtLine(symbols, detail.name, (nameNode || def.node).startPosition.row + 1);
508
+ if (!symbol || symbol.className) return null;
509
+ defs.push(symbol);
510
+ }
511
+ }
512
+ return defs;
513
+ }
514
+
515
+ /**
516
+ * The function definitions an import binds, read from the imported project
517
+ * module itself (null when that does not prove them):
518
+ * fromImports(entries) the import bindings of one name. Python
519
+ * `from m import f [as g]`: m's module scope must bind f
520
+ * only by function definitions; JS/TS
521
+ * `import { f as g }` / `import g`: m must export f (or
522
+ * its default) by declaring the function. A re-export,
523
+ * alias, value or class is left to the engine.
524
+ * star(name, bindings) a name no scope binds, reached only through the
525
+ * module's `from m import *` imports: m's closed literal
526
+ * `__all__`, or without one every public name m's module
527
+ * scope binds; a source m that star-imports itself and
528
+ * declares no `__all__` exports unknown names. Every source
529
+ * must be an unconditional import of a project module and
530
+ * exactly one may export the name.
531
+ * Anything else (a builtin, an external or conditional source) is not a
532
+ * provable producer. `memo` (per source file) may be shared across files.
533
+ */
534
+ function createModuleImportResolver(index, fileEntry, memo = new Map()) {
535
+ const path = require('path');
536
+ const { moduleEvidence } = require('./python-fixture-flow');
537
+ const moduleOf = module => {
538
+ const relative = module ? fileEntry.moduleResolved?.[module] : null;
539
+ const file = relative ? path.resolve(index.root, relative) : null;
540
+ const entry = file ? index.files.get(file) : null;
541
+ if (entry?.language !== 'python') return null;
542
+ if (!memo.has(file)) {
543
+ let scope = null;
544
+ try {
545
+ const tree = index._getParsedTree(file, index._readFile(file), 'python');
546
+ if (tree) {
547
+ const names = new Map();
548
+ for (const [name, entries] of pythonScopeBindings(tree.rootNode)) {
549
+ names.set(name, {
550
+ other: entries.some(binding => binding.kind !== 'def'),
551
+ defLines: entries.filter(binding => binding.kind === 'def').map(binding =>
552
+ (binding.node.childForFieldName('name') || binding.node).startPosition.row + 1),
553
+ });
554
+ }
555
+ scope = { file, entry, names, exports: moduleEvidence(index, file)?.exports || null };
556
+ }
557
+ } catch (_) { scope = null; }
558
+ memo.set(file, scope);
559
+ }
560
+ return memo.get(file);
561
+ };
562
+ const definitions = (scope, name) => {
563
+ const bound = scope.names.get(name);
564
+ if (!bound || bound.other || bound.defLines.length === 0) return null;
565
+ const symbols = callableSymbolsByName(scope.entry);
566
+ const defs = [];
567
+ for (const line of bound.defLines) {
568
+ const symbol = symbolAtLine(symbols, name, line);
569
+ if (!symbol) return null;
570
+ defs.push(symbol);
571
+ }
572
+ return defs;
573
+ };
574
+ // 'yes' | 'no' | 'unknown'
575
+ const exports = (scope, name) => {
576
+ if (scope.names.has('__all__')) {
577
+ if (!scope.exports) return 'unknown';
578
+ return scope.exports.literals.some(literal => literal.value === name) ? 'yes' : 'no';
579
+ }
580
+ if (name.startsWith('_')) return 'no';
581
+ if (scope.names.has(name)) return 'yes';
582
+ return scope.names.has('*') ? 'unknown' : 'no';
583
+ };
584
+ // JS/TS: the module's export of that name must be its own function: an
585
+ // exported declaration on the export's line, or a name (`export { f }`,
586
+ // `export default f`) its module scope binds only by function
587
+ // definitions, read through re-exports (jsModuleExportDefinitions). Any
588
+ // other export (an alias of a value, a class) is left to the engine.
589
+ const exportedDefinitions = binding => {
590
+ const relative = fileEntry.moduleResolved?.[binding.module];
591
+ const file = relative ? path.resolve(index.root, relative) : null;
592
+ return file ? jsModuleExportDefinitions(index, file, binding.imported, memo) : null;
593
+ };
594
+ return {
595
+ fromImports(entries) {
596
+ const defs = [];
597
+ for (const binding of entries) {
598
+ let found;
599
+ if (LEXICAL_FAMILY[fileEntry.language] === 'python') {
600
+ const scope = moduleOf(binding.module);
601
+ found = scope && definitions(scope, binding.imported);
602
+ } else {
603
+ found = exportedDefinitions(binding);
604
+ }
605
+ if (!found) return null;
606
+ defs.push(...found);
607
+ }
608
+ return defs;
609
+ },
610
+ star(name, bindings) {
611
+ let supplier = null;
612
+ for (const binding of bindings) {
613
+ const scope = binding.topLevel ? moduleOf(binding.module) : null;
614
+ if (!scope) return null;
615
+ const verdict = exports(scope, name);
616
+ if (verdict === 'unknown') return null;
617
+ if (verdict === 'yes') {
618
+ if (supplier && supplier !== scope) return null;
619
+ supplier = scope;
620
+ }
621
+ }
622
+ return supplier ? definitions(supplier, name) : null;
623
+ },
624
+ };
625
+ }
626
+
627
+ /**
628
+ * The producers an import-bound bare call reaches: the engine's confirmed
629
+ * callees at that call site whose evidence establishes the target, from one
630
+ * restricted findCallees run per enclosing callable (module-level calls run
631
+ * as a synthetic module owner). A same-name definition the import does not
632
+ * reach is never borrowed.
633
+ */
634
+ function createImportedProducerResolver(index, filePath, fileEntry, calls) {
635
+ const symbols = (fileEntry.symbols || []).filter(isCallableDef);
636
+ const moduleOwner = { file: filePath, startLine: 1, endLine: Infinity, name: '<module>' };
637
+ const ownerOf = line => rustSiteOwner(symbols, line) || moduleOwner;
638
+ const byOwner = new Map();
639
+ for (const call of calls) {
640
+ const start = call.callStart ?? call.callSite?.start;
641
+ if (!Number.isInteger(start)) continue;
642
+ const owner = ownerOf(call.line);
643
+ if (!byOwner.has(owner)) byOwner.set(owner, new Set());
644
+ byOwner.get(owner).add(start);
645
+ }
646
+ const memo = new Map();
647
+ // `start` is the call record's site start: the call node's for most
648
+ // records, the method name token's for a record that keeps only the
649
+ // token (Python method calls).
650
+ return (callNode, start = callNode.startIndex) => {
651
+ const owner = ownerOf(callNode.startPosition.row + 1);
652
+ if (!byOwner.has(owner)) return [];
653
+ if (!memo.has(owner)) {
654
+ memo.set(owner, index.findCallees(owner, {
655
+ collectAccount: true, siteStarts: byOwner.get(owner),
656
+ }) || []);
657
+ }
658
+ return memo.get(owner).filter(def => (def.siteProvenance || []).some(site =>
659
+ site.start === start && site.provenance?.validation === 'establishes-target'));
660
+ };
661
+ }
662
+
663
+ // ── Consumer positions: a call there is never a missing await ──
664
+
665
+ function hasChildToken(node, tokenType) {
666
+ for (let i = 0; i < node.childCount; i++) {
667
+ if (node.child(i).type === tokenType) return true;
668
+ }
669
+ return false;
670
+ }
671
+
672
+ /**
673
+ * The async consumer a call feeds directly, or null:
674
+ * 'async-for' (Python `async for` / async comprehension, JS `for await`,
675
+ * C# `await foreach`) or 'async-with' (Python `async with`, C# `await using`).
676
+ */
677
+ function asyncConsumerRole(callNode) {
678
+ let expr = callNode;
679
+ while (expr.parent?.type === 'parenthesized_expression') expr = expr.parent;
680
+ let parent = expr.parent;
681
+ if (!parent) return null;
682
+ switch (parent.type) {
683
+ case 'for_statement':
684
+ case 'for_in_clause':
685
+ return sameNode(parent.childForFieldName('right'), expr) && hasChildToken(parent, 'async')
686
+ ? 'async-for' : null;
687
+ case 'for_in_statement':
688
+ return sameNode(parent.childForFieldName('right'), expr) && hasChildToken(parent, 'await')
689
+ ? 'async-for' : null;
690
+ case 'foreach_statement':
691
+ return sameNode(parent.childForFieldName('right'), expr) && hasChildToken(parent, 'await')
692
+ ? 'async-for' : null;
693
+ case 'using_statement':
694
+ return !sameNode(parent.childForFieldName('body'), expr) && hasChildToken(parent, 'await')
695
+ ? 'async-with' : null;
696
+ case 'as_pattern':
697
+ if (!sameNode(parent.namedChild(0), expr)) return null;
698
+ parent = parent.parent;
699
+ if (parent?.type !== 'with_item') return null;
700
+ // falls through
701
+ case 'with_item': {
702
+ const statement = parent.parent?.parent;
703
+ return statement?.type === 'with_statement' && hasChildToken(statement, 'async')
704
+ ? 'async-with' : null;
705
+ }
706
+ default:
707
+ return null;
708
+ }
709
+ }
710
+
711
+ /** Whether the call's value is discarded (a bare expression statement). */
712
+ function isDiscardedCall(callNode) {
713
+ let expr = callNode;
714
+ while (expr.parent?.type === 'parenthesized_expression') expr = expr.parent;
715
+ return expr.parent?.type === 'expression_statement';
716
+ }
717
+
718
+ // fix #367c: where an unawaited coroutine/promise value GOES. Only a lost
719
+ // value (discarded, or stored in a local that is never read) or a value used
720
+ // as if it were already resolved is a missing await; any other destination
721
+ // (an argument, a collection/generator/spread element, a returned or yielded
722
+ // value, a conditional/logical branch that itself flows, a stored field)
723
+ // hands the awaitable on.
724
+ const VALUE_TRANSPARENT = new Set([
725
+ 'parenthesized_expression', 'as_expression', 'satisfies_expression',
726
+ 'non_null_expression', 'type_assertion',
727
+ ]);
728
+ const VALUE_FLOW_PARENTS = new Set([
729
+ // arguments
730
+ 'arguments', 'argument_list', 'argument', 'keyword_argument',
731
+ // collections, spreads, generators, comprehensions
732
+ 'array', 'list', 'tuple', 'set', 'dictionary', 'pair', 'object',
733
+ 'spread_element', 'list_splat', 'dictionary_splat', 'expression_list',
734
+ 'generator_expression', 'list_comprehension', 'set_comprehension',
735
+ 'dictionary_comprehension', 'initializer_expression',
736
+ 'collection_expression', 'spread_expression',
737
+ // handed to the caller
738
+ 'return_statement', 'yield_expression', 'yield', 'arrow_function', 'lambda',
739
+ 'lambda_expression', 'arrow_expression_clause', 'export_statement',
740
+ // stored somewhere other than a plain local
741
+ 'assignment_pattern', 'default_parameter', 'public_field_definition',
742
+ 'field_definition', 'jsx_expression', 'sequence_expression',
743
+ ]);
744
+ const RESOLVED_OPERATORS = new Set(['+', '-', '*', '/', '%', '**', '//', '@', '<', '>',
745
+ '<=', '>=', '==', '!=', '===', '!==', '|', '&', '^', '<<', '>>', '>>>', 'in', 'instanceof']);
746
+ const AWAITABLE_MEMBERS = {
747
+ // Promise protocol and Object.prototype members.
748
+ promise: new Set(['then', 'catch', 'finally', 'constructor', 'toString', 'toLocaleString',
749
+ 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable']),
750
+ // Coroutine object protocol and attributes, and members every object has.
751
+ coroutine: new Set(['send', 'throw', 'close', '__await__', 'cr_await', 'cr_frame',
752
+ 'cr_running', 'cr_code', 'cr_origin', 'cr_suspended', '__name__', '__qualname__',
753
+ '__class__', '__doc__', '__repr__', '__str__', '__format__', '__hash__', '__eq__', '__ne__',
754
+ '__sizeof__', '__dir__', '__reduce__', '__reduce_ex__']),
755
+ };
756
+
757
+ function fieldIs(parent, field, node) {
758
+ const child = parent.childForFieldName(field);
759
+ return !!child && sameNode(child, node);
760
+ }
761
+
762
+ function conditionOf(parent) {
763
+ // Python conditional_expression has no field names: body `if` cond `else` alt.
764
+ return parent.childForFieldName('condition') ||
765
+ (parent.type === 'conditional_expression' && !parent.childForFieldName('consequence')
766
+ ? parent.namedChild(1) : null);
767
+ }
768
+
769
+ /**
770
+ * Classify an unawaited call's value. Returns
771
+ * { kind: 'flow' } handed on (no finding)
772
+ * { kind: 'discarded' } bare expression statement
773
+ * { kind: 'stored', binding, scopeNode } plain local; caller checks reads
774
+ * { kind: 'used-as-value' } treated as the resolved value
775
+ * { kind: 'intentional' } explicit discard (`void`, C# `_ =`)
776
+ */
777
+ function valueFlow(callNode, language) {
778
+ let expr = callNode;
779
+ for (let depth = 0; depth < 32; depth++) {
780
+ const parent = expr.parent;
781
+ if (!parent) return { kind: 'flow' };
782
+ const type = parent.type;
783
+ if (VALUE_TRANSPARENT.has(type)) { expr = parent; continue; }
784
+ if (type === 'expression_statement') return { kind: 'discarded' };
785
+ if (VALUE_FLOW_PARENTS.has(type)) return { kind: 'flow' };
786
+ switch (type) {
787
+ case 'ternary_expression':
788
+ case 'conditional_expression': {
789
+ const condition = conditionOf(parent);
790
+ if (condition && sameNode(condition, expr)) return { kind: 'used-as-value' };
791
+ expr = parent;
792
+ continue;
793
+ }
794
+ case 'binary_expression':
795
+ case 'boolean_operator': {
796
+ const operator = parent.childForFieldName('operator')?.text ||
797
+ parent.children.find(child => !child.isNamed)?.text;
798
+ if (operator === '??' || operator === '||' || operator === '&&' ||
799
+ operator === 'or' || operator === 'and') {
800
+ // The left operand is tested for truthiness (an
801
+ // awaitable is always truthy / never null); the right
802
+ // operand is the expression's value.
803
+ if (fieldIs(parent, 'left', expr)) return { kind: 'used-as-value' };
804
+ expr = parent;
805
+ continue;
806
+ }
807
+ return RESOLVED_OPERATORS.has(operator) ? { kind: 'used-as-value' } : { kind: 'flow' };
808
+ }
809
+ case 'comparison_operator':
810
+ case 'binary_operator':
811
+ case 'not_operator':
812
+ case 'update_expression':
813
+ case 'augmented_assignment_expression':
814
+ case 'augmented_assignment':
815
+ case 'template_substitution':
816
+ case 'interpolation':
817
+ case 'subscript_expression':
818
+ case 'element_access_expression':
819
+ return { kind: 'used-as-value' };
820
+ case 'unary_expression':
821
+ case 'unary_operator':
822
+ case 'prefix_unary_expression': {
823
+ const operator = parent.childForFieldName('operator')?.text ||
824
+ parent.children.find(child => !child.isNamed)?.text;
825
+ if (operator === 'void') return { kind: 'intentional' };
826
+ return operator === 'typeof' ? { kind: 'flow' } : { kind: 'used-as-value' };
827
+ }
828
+ case 'subscript':
829
+ return fieldIs(parent, 'value', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
830
+ case 'member_expression':
831
+ case 'attribute': {
832
+ if (!fieldIs(parent, 'object', expr)) return { kind: 'flow' };
833
+ const member = (parent.childForFieldName('property') ||
834
+ parent.childForFieldName('attribute'))?.text;
835
+ const members = type === 'attribute' ? AWAITABLE_MEMBERS.coroutine : AWAITABLE_MEMBERS.promise;
836
+ return members.has(member) ? { kind: 'flow' } : { kind: 'used-as-value' };
837
+ }
838
+ case 'member_access_expression':
839
+ // Task members (ConfigureAwait, Result, ContinueWith, ...).
840
+ return { kind: 'flow' };
841
+ case 'call_expression':
842
+ case 'call':
843
+ // `fn()(...)` calls the awaitable itself.
844
+ return fieldIs(parent, 'function', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
845
+ case 'if_statement':
846
+ case 'while_statement':
847
+ case 'do_statement':
848
+ case 'elif_clause':
849
+ case 'assert_statement':
850
+ return fieldIs(parent, 'condition', expr) || type === 'assert_statement'
851
+ ? { kind: 'used-as-value' } : { kind: 'flow' };
852
+ case 'for_statement':
853
+ case 'for_in_statement':
854
+ case 'for_in_clause':
855
+ case 'foreach_statement':
856
+ // A synchronous loop over the awaitable (async loops were
857
+ // classified as consumers before this point).
858
+ return fieldIs(parent, 'right', expr) ? { kind: 'used-as-value' } : { kind: 'flow' };
859
+ case 'with_item':
860
+ case 'as_pattern':
861
+ return { kind: 'used-as-value' };
862
+ case 'variable_declarator':
863
+ case 'assignment_expression':
864
+ case 'assignment':
865
+ case 'named_expression':
866
+ case 'equals_value_clause': {
867
+ let holder = parent;
868
+ if (type === 'equals_value_clause') holder = parent.parent;
869
+ const target = holder?.childForFieldName('name') || holder?.childForFieldName('left');
870
+ if (!target || (holder.type !== 'variable_declarator' && fieldIs(holder, 'left', expr)) ||
871
+ (target && sameNode(target, expr))) return { kind: 'flow' };
872
+ if (['object_pattern', 'array_pattern', 'pattern_list', 'tuple_pattern', 'list_pattern']
873
+ .includes(target.type)) return { kind: 'used-as-value' };
874
+ if (target.type !== 'identifier') return { kind: 'flow' };
875
+ if (language === 'csharp' && target.text === '_') return { kind: 'intentional' };
876
+ // fix #380: a plain assignment (not a declaration) whose
877
+ // name is not a local of the enclosing function stores into
878
+ // a field/property (implicit this) or an outer variable:
879
+ // the value outlives the function and flows on.
880
+ if ((holder.type === 'assignment_expression' || holder.type === 'assignment') &&
881
+ !assignmentTargetIsLocal(holder, target.text, language)) return { kind: 'flow' };
882
+ return { kind: 'stored', binding: target.text, holder };
883
+ }
884
+ default:
885
+ return { kind: 'flow' };
886
+ }
887
+ }
888
+ return { kind: 'flow' };
889
+ }
890
+
891
+ // Function-like nodes that open a local scope, across the audited grammars.
892
+ const LOCAL_SCOPE_TYPES = new Set([
893
+ 'function_declaration', 'function_expression', 'arrow_function', 'method_definition',
894
+ 'generator_function', 'generator_function_declaration', 'function_definition',
895
+ 'async_function_definition', 'lambda', 'method_declaration', 'local_function_statement',
896
+ 'anonymous_method_expression', 'lambda_expression', 'constructor_declaration',
897
+ 'accessor_declaration', 'operator_declaration', 'conversion_operator_declaration',
898
+ 'destructor_declaration',
899
+ ]);
900
+ // Declaration parents whose `name`/`left`/`pattern` child binds a local.
901
+ const LOCAL_DECLARATION_PARENTS = new Set([
902
+ 'variable_declarator', 'parameter', 'required_parameter', 'optional_parameter',
903
+ 'rest_pattern', 'catch_declaration', 'catch_clause', 'foreach_statement', 'for_in_statement',
904
+ 'declaration_expression', 'typed_parameter', 'default_parameter', 'typed_default_parameter',
905
+ 'formal_parameters', 'parameters', 'lambda_parameters', 'implicit_parameter',
906
+ 'single_variable_designation', 'for_each_statement', 'using_statement',
907
+ ]);
908
+
909
+ /**
910
+ * fix #380: whether the name assigned by `holder` is a local of its enclosing
911
+ * function. Languages whose plain assignment declares a local (Python, unless
912
+ * a `global`/`nonlocal` statement names it) answer from the statement; the
913
+ * others (JS/TS: outer variables, C#: implicit-this fields and properties)
914
+ * need a declaration or parameter of that name inside the function.
915
+ */
916
+ function assignmentTargetIsLocal(holder, name, language) {
917
+ const declaresLocal = !!langTraits(language)?.assignmentDeclaresLocal;
918
+ let scope = holder.parent;
919
+ while (scope && !LOCAL_SCOPE_TYPES.has(scope.type)) {
920
+ // An assignment in a class body sets a class attribute.
921
+ if (declaresLocal && scope.type === 'class_definition') return false;
922
+ scope = scope.parent;
923
+ }
924
+ if (!scope) return false;
925
+ let declared = false;
926
+ let escapes = false;
927
+ const visit = node => {
928
+ if (declared || escapes) return;
929
+ if (!sameNode(node, scope) && LOCAL_SCOPE_TYPES.has(node.type)) return;
930
+ if (declaresLocal) {
931
+ if (node.type === 'global_statement' || node.type === 'nonlocal_statement') {
932
+ for (let i = 0; i < node.namedChildCount; i++) {
933
+ if (node.namedChild(i).text === name) { escapes = true; return; }
934
+ }
935
+ }
936
+ } else if (node.type === 'identifier' && node.text === name) {
937
+ const parent = node.parent;
938
+ if (parent && LOCAL_DECLARATION_PARENTS.has(parent.type) &&
939
+ !(parent.type === 'foreach_statement' && !fieldIs(parent, 'left', node)) &&
940
+ !(parent.type === 'for_in_statement' && !fieldIs(parent, 'left', node))) {
941
+ const value = parent.childForFieldName('value');
942
+ const init = parent.childForFieldName('default_value') || parent.childForFieldName('right');
943
+ if (!(value && sameNode(value, node)) && !(init && sameNode(init, node))) {
944
+ declared = true;
945
+ return;
946
+ }
947
+ }
948
+ }
949
+ for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i));
950
+ };
951
+ visit(scope);
952
+ return declaresLocal ? !escapes : declared;
953
+ }
954
+
955
+ /**
956
+ * Whether a local that received an awaitable is read after the assignment
957
+ * anywhere in its function (nested closures included) - a stored value that
958
+ * is never read is as lost as a discarded one.
959
+ */
960
+ function storedValueRead(holder, name, functionTypes) {
961
+ let scope = holder.parent;
962
+ while (scope && !functionTypes.has(scope.type)) scope = scope.parent;
963
+ const root = scope ? (scope.childForFieldName('body') || scope) : null;
964
+ let top = holder;
965
+ while (!root && top.parent) top = top.parent;
966
+ const searchRoot = root || top;
967
+ const after = holder.endIndex;
968
+ let found = false;
969
+ const visit = node => {
970
+ if (found || node.endIndex <= after) return;
971
+ if (node.type === 'identifier' && node.text === name && node.startIndex >= after) {
972
+ const parent = node.parent;
973
+ const isWriteTarget = parent &&
974
+ ['assignment', 'assignment_expression', 'variable_declarator'].includes(parent.type) &&
975
+ (fieldIs(parent, 'left', node) || fieldIs(parent, 'name', node));
976
+ if (!isWriteTarget) { found = true; return; }
977
+ }
978
+ for (let i = 0; i < node.namedChildCount; i++) visit(node.namedChild(i));
979
+ };
980
+ visit(searchRoot);
981
+ return found;
982
+ }
983
+
984
+ /**
985
+ * Python (fix #398): where a local assigned straight from a coroutine call
986
+ * is used as the coroutine's result. A use counts only where every value the
987
+ * local can hold there is that coroutine: the assignment is the name's only
988
+ * binding in the function, or the use lies in the block holding the
989
+ * assignment (so it ran first), before any later binding, and outside any
990
+ * loop of that block that rebinds the name. A value chosen by a conditional
991
+ * or boolean expression, a walrus target, uses inside nested functions,
992
+ * classes, lambdas and generator expressions (they run later), identity
993
+ * tests, comparison with None, membership of the coroutine in a container
994
+ * and formatting the object into a string are not uses of its result.
995
+ */
996
+ function storedCoroutineMisuse(callNode, holder, name, functionTypes, language) {
997
+ if (holder.type !== 'assignment') return null;
998
+ let value = holder.childForFieldName('right');
999
+ while (value?.type === 'parenthesized_expression' && value.namedChildCount === 1) value = value.namedChild(0);
1000
+ if (!value || !sameNode(value, callNode)) return null;
1001
+ let scope = holder.parent;
1002
+ while (scope && !functionTypes.has(scope.type)) scope = scope.parent;
1003
+ let block = holder.parent;
1004
+ while (block && block.type !== 'block') block = block.parent;
1005
+ if (!scope || !block || scope.type === 'lambda') return null;
1006
+ // A nested function declaring the name nonlocal can rebind it unseen.
1007
+ if (scope.text.includes('nonlocal') && containsNode(scope, node => node.type === 'nonlocal_statement' &&
1008
+ node.namedChildren.some(child => child.text === name))) return null;
1009
+ const { pythonBindingSites } = require('../languages/lexical-scope');
1010
+ const sites = pythonBindingSites(scope, name);
1011
+ const writes = new Set(sites.filter(site => site.startIndex > holder.endIndex).map(site => site.startIndex));
1012
+ const inside = (node, outer) => node.startIndex >= outer.startIndex && node.endIndex <= outer.endIndex;
1013
+ // Parameters, imports and every other binding of the name are sites.
1014
+ const onlyBinding = sites.length === 1;
1015
+ const holds = use => {
1016
+ if (onlyBinding) return true;
1017
+ if (!inside(use, block)) return false;
1018
+ for (let node = use.parent; node && !sameNode(node, block); node = node.parent) {
1019
+ if ((node.type === 'for_statement' || node.type === 'while_statement') &&
1020
+ sites.some(site => inside(site, node))) return false;
1021
+ }
1022
+ return true;
1023
+ };
1024
+ const binds = node => !!node && (node.type === 'identifier' ? node.text === name
1025
+ : STORED_PATTERN_TYPES.has(node.type) && node.namedChildren.some(binds));
1026
+ let stopped = false;
1027
+ let misuse = null;
1028
+ const visit = node => {
1029
+ if (stopped || misuse || node.endIndex <= holder.endIndex) return;
1030
+ if (functionTypes.has(node.type) || node.type === 'class_definition') {
1031
+ if (writes.has(node.childForFieldName('name')?.startIndex)) stopped = true;
1032
+ return;
1033
+ }
1034
+ if (COMPREHENSION_TYPES.has(node.type)) {
1035
+ const clauses = node.namedChildren.filter(child => child.type === 'for_in_clause');
1036
+ // Only the first iterable is evaluated here and now; the rest
1037
+ // runs in the comprehension's own scope (later, for a generator).
1038
+ if (node.type === 'generator_expression' || clauses.some(clause => binds(clause.childForFieldName('left')))) {
1039
+ const right = clauses[0]?.childForFieldName('right');
1040
+ if (right) visit(right);
1041
+ return;
1042
+ }
1043
+ }
1044
+ if ((node.type === 'assignment' || node.type === 'named_expression' || node.type === 'for_statement') &&
1045
+ binds(node.childForFieldName('left') || node.childForFieldName('name'))) {
1046
+ // The right side is evaluated before the name is rebound.
1047
+ const right = node.childForFieldName('right') || node.childForFieldName('value');
1048
+ if (right) visit(right);
1049
+ stopped = true;
1050
+ return;
1051
+ }
1052
+ if (node.type === 'identifier' && node.text === name) {
1053
+ // `v += 1` reads the coroutine before rebinding it.
1054
+ const augmented = node.parent?.type === 'augmented_assignment' && fieldIs(node.parent, 'left', node);
1055
+ if (!augmented && writes.has(node.startIndex)) { stopped = true; return; }
1056
+ if (holds(node) && storedValueUsedAsResult(node, language)) misuse = node;
1057
+ if (augmented) { stopped = true; return; }
1058
+ }
1059
+ for (const child of node.namedChildren) visit(child);
1060
+ };
1061
+ visit(scope.childForFieldName('body') || scope);
1062
+ return misuse ? { line: misuse.startPosition.row + 1, variable: name,
1063
+ originLine: callNode.startPosition.row + 1, reason: 'stored-coroutine-used-as-value' } : null;
1064
+ }
1065
+
1066
+ const STORED_PATTERN_TYPES = new Set(['tuple_pattern', 'list_pattern', 'pattern_list', 'tuple', 'list',
1067
+ 'as_pattern_target', 'list_splat_pattern', 'parenthesized_expression']);
1068
+ const COMPREHENSION_TYPES = new Set(['list_comprehension', 'set_comprehension',
1069
+ 'dictionary_comprehension', 'generator_expression']);
1070
+
1071
+ /** Whether a read of a stored awaitable needs its result (see valueFlow). */
1072
+ function storedValueUsedAsResult(node, language) {
1073
+ let use = node;
1074
+ while (use.parent?.type === 'parenthesized_expression') use = use.parent;
1075
+ const parent = use.parent;
1076
+ if (parent?.type === 'interpolation') return false;
1077
+ if (parent?.type === 'comparison_operator') {
1078
+ const operators = parent.children.filter(child => !child.isNamed).map(child => child.text);
1079
+ const operands = parent.namedChildren.filter(child => !sameNode(child, use));
1080
+ if (operators.every(operator => operator === 'is' || operator === 'is not')) return false;
1081
+ if (operators.every(operator => operator === '==' || operator === '!=') &&
1082
+ operands.every(operand => operand.type === 'none')) return false;
1083
+ if (sameNode(parent.namedChild(0), use) && ['in', 'not in'].includes(operators[0])) return false;
1084
+ }
1085
+ if (parent?.type === 'binary_operator' && fieldIs(parent, 'right', use) &&
1086
+ parent.childForFieldName('operator')?.text === '%' &&
1087
+ ['string', 'concatenated_string'].includes(parent.childForFieldName('left')?.type)) return false;
1088
+ return valueFlow(node, language).kind === 'used-as-value';
1089
+ }
1090
+
1091
+ function containsNode(root, test) {
1092
+ const stack = [root];
1093
+ while (stack.length > 0) {
1094
+ const node = stack.pop();
1095
+ if (test(node)) return true;
1096
+ for (let i = 0; i < node.namedChildCount; i++) stack.push(node.namedChild(i));
1097
+ }
1098
+ return false;
1099
+ }
1100
+
1101
+ // ── Rust futures (fix #370) ──
1102
+ //
1103
+ // Calling an `async fn` (or a fn whose return type is a future) creates a
1104
+ // lazy future that runs only when polled: awaited, spawned, joined, selected,
1105
+ // boxed/stored and used, or returned. A future that is discarded, dropped by
1106
+ // `let _ =`, bound to a local that is never used, or used as if it were its
1107
+ // Output never runs. The enclosing function does not matter: a sync fn that
1108
+ // creates a future and drops it loses it too.
1109
+
1110
+ /** Base type name of a declared type string (`&'a mut foo::Bar<T>` -> Bar). */
1111
+ function rustTypeBase(text) {
1112
+ let rest = String(text || '').trim();
1113
+ for (;;) {
1114
+ const before = rest;
1115
+ rest = rest.replace(/^&\s*/, '').replace(/^'[A-Za-z_]\w*\s+/, '').replace(/^mut\s+/, '').replace(/^dyn\s+/, '');
1116
+ if (rest === before) break;
1117
+ }
1118
+ const lt = rest.indexOf('<');
1119
+ const head = (lt >= 0 ? rest.slice(0, lt) : rest).trim();
1120
+ if (!/^[A-Za-z_][\w]*(?:::[A-Za-z_][\w]*)*$/.test(head)) return null;
1121
+ const parts = head.split('::');
1122
+ return parts[parts.length - 1];
1123
+ }
1124
+
1125
+ /**
1126
+ * Project types a returned value can be a future through: aliases of future
1127
+ * types (`type Fut = Pin<Box<dyn Future..>>`) and structs/enums with an
1128
+ * `impl Future for T`. Map name -> { kind, output }.
1129
+ */
1130
+ function rustProjectFutureTypes(index) {
1131
+ const out = new Map();
1132
+ for (const [name, defs] of index.symbols) {
1133
+ let future = null;
1134
+ let other = false;
1135
+ for (const def of defs) {
1136
+ if (def.type === 'type' && def.futureReturn) {
1137
+ future = future || { kind: 'future', output: def.futureReturn.output ?? null };
1138
+ } else if ((def.type === 'struct' || def.type === 'enum') && Array.isArray(def.implements) &&
1139
+ def.implements.some(trait => rustTypeBase(trait) === 'Future')) {
1140
+ // A concrete type implementing Future may be a handle to work
1141
+ // that already started (a join handle): not known to be lazy.
1142
+ future = future || { kind: 'named', output: null, type: name };
1143
+ } else if (['struct', 'enum', 'type', 'trait', 'union'].includes(def.type)) {
1144
+ other = true;
1145
+ }
1146
+ }
1147
+ // A name shared with a non-future type cannot be told apart by name.
1148
+ if (future && !other) out.set(name, future);
1149
+ }
1150
+ return out;
1151
+ }
1152
+
1153
+ // Future kinds whose loss is reported. 'named' (a type implementing Future)
1154
+ // and 'wrapped' (a proc-macro attribute may rewrite the fn) are not known to
1155
+ // be lazy: counted as not audited.
1156
+ const RUST_LAZY_FUTURE_KINDS = new Set(['async', 'future']);
1157
+
1158
+ /** What calling a Rust definition returns when it is a future, else null. */
1159
+ function rustDefFuture(def, futureTypes) {
1160
+ if (!def) return null;
1161
+ if (def.futureReturn) return def.futureReturn;
1162
+ if (futureTypes.size === 0 || typeof def.returnType !== 'string') return null;
1163
+ return futureTypes.get(rustTypeBase(def.returnType)) || null;
1164
+ }
1165
+
1166
+
1167
+
1168
+
1169
+ const rustFlow = require('../languages/rust-value-flow');
1170
+ const { rustPatternBinds, rustLetPattern } = rustFlow;
1171
+
1172
+ /**
1173
+ * Where an unpolled Rust future value goes (languages/rust-value-flow.js).
1174
+ * A method called on it is judged here: a method of the future's own named
1175
+ * type flows; a project method of its Output type takes it for its Output.
1176
+ */
1177
+ function rustMemberVerdict(index) {
1178
+ return (future, member, isMethod) => {
1179
+ if (!isMethod) {
1180
+ // An opaque future has no fields; a project future type
1181
+ // may declare its own.
1182
+ return future.kind === 'named' ? { kind: 'flow' } : { kind: 'used-as-value', member };
1183
+ }
1184
+ if (future.kind === 'named' && rustOwnedMethods(index, member, future.type).length > 0) {
1185
+ return { kind: 'flow' };
1186
+ }
1187
+ const outputOwner = rustTypeBase(future.output);
1188
+ return rustOwnedMethods(index, member, outputOwner).length > 0
1189
+ ? { kind: 'used-as-value', member } : { kind: 'flow' };
1190
+ };
1191
+ }
1192
+
1193
+ function rustValueFlow(index, exprNode, future) {
1194
+ return rustFlow.rustValueFlow(exprNode, future, rustMemberVerdict(index));
1195
+ }
1196
+
1197
+ function rustStoredFutureUse(index, holder, name, future) {
1198
+ return rustFlow.rustStoredFutureUse(holder, name, future, rustMemberVerdict(index));
1199
+ }
1200
+
1201
+ /**
1202
+ * Lexical binding of a bare call name inside the enclosing function:
1203
+ * { future } for a local closure whose body is an async block,
1204
+ * { shadowed: true } for any other local/parameter, null when none binds it.
1205
+ */
1206
+ function rustLocalCallee(callNode, name) {
1207
+ let child = callNode;
1208
+ for (let scope = callNode.parent; scope; child = scope, scope = scope.parent) {
1209
+ if (scope.type === 'block') {
1210
+ let found = null;
1211
+ for (let i = 0; i < scope.namedChildCount; i++) {
1212
+ const statement = scope.namedChild(i);
1213
+ if (statement.startIndex >= child.startIndex) break;
1214
+ if (statement.type === 'let_declaration' && rustPatternBinds(rustLetPattern(statement), name)) {
1215
+ found = statement;
1216
+ }
1217
+ }
1218
+ if (found) {
1219
+ const value = found.childForFieldName('value');
1220
+ const body = value?.type === 'closure_expression' ? value.childForFieldName('body') : null;
1221
+ return body?.type === 'async_block' ? { future: { kind: 'future', output: null } } : { shadowed: true };
1222
+ }
1223
+ } else if (scope.type === 'closure_expression') {
1224
+ if (rustPatternBinds(scope.childForFieldName('parameters'), name)) return { shadowed: true };
1225
+ } else if (scope.type === 'function_item') {
1226
+ const params = scope.childForFieldName('parameters');
1227
+ for (const param of params?.namedChildren || []) {
1228
+ if (rustPatternBinds(param.childForFieldName('pattern'), name)) return { shadowed: true };
1229
+ }
1230
+ return null;
1231
+ } else if (scope.type === 'match_arm' || scope.type === 'if_expression' || scope.type === 'while_expression') {
1232
+ const pattern = scope.type === 'match_arm'
1233
+ ? scope.childForFieldName('pattern')
1234
+ : scope.childForFieldName('condition');
1235
+ if (pattern && !sameNode(pattern, child) && rustPatternBinds(
1236
+ pattern.type === 'let_condition' ? pattern.childForFieldName('pattern') : pattern, name)) {
1237
+ return { shadowed: true };
1238
+ }
1239
+ } else if (scope.type === 'for_expression') {
1240
+ if (!sameNode(scope.childForFieldName('value'), child) &&
1241
+ rustPatternBinds(scope.childForFieldName('pattern'), name)) return { shadowed: true };
1242
+ }
1243
+ }
1244
+ return null;
1245
+ }
1246
+
1247
+ /** Project methods named `method` owned by type `owner`. */
1248
+ function rustOwnedMethods(index, method, owner) {
1249
+ if (!owner) return [];
1250
+ return (index.symbols.get(method) || []).filter(def =>
1251
+ isCallableDef(def) && (def.className === owner || def.receiver === owner));
1252
+ }
1253
+
1254
+
1255
+
1256
+
1257
+ /**
1258
+ * Index of future-producing definitions by name, split by the call shapes
1259
+ * that can reach them: free functions (bare/module-path calls), members by
1260
+ * owner type, and trait-declared members (reachable through dispatch).
1261
+ */
1262
+ function rustFutureDefIndex(index, futureTypes) {
1263
+ const byName = new Map();
1264
+ for (const [name, defs] of index.symbols) {
1265
+ let entry = null;
1266
+ for (const def of defs) {
1267
+ if (!isCallableDef(def) || !rustDefFuture(def, futureTypes)) continue;
1268
+ entry = entry || { defs: [] };
1269
+ const owner = def.className || def.receiver;
1270
+ const ownerDefs = owner ? index.symbols.get(def.className || '') || [] : [];
1271
+ entry.defs.push({
1272
+ def,
1273
+ owner: owner || null,
1274
+ traitDeclared: ownerDefs.some(ownerDef => ownerDef.type === 'trait'),
1275
+ // Private items are visible only inside their own module.
1276
+ moduleScope: rustPrivateModuleScope(def, ownerDefs),
1277
+ });
1278
+ }
1279
+ if (entry) byName.set(name, entry);
1280
+ }
1281
+ return byName;
1282
+ }
1283
+
1284
+ /**
1285
+ * The module a private (no `pub`) Rust item is confined to, as
1286
+ * { file, dir }: its own file and, for `mod.rs`/`lib.rs`/`main.rs` or a
1287
+ * `name.rs` with a `name/` directory, the child module files. Null when the
1288
+ * item is visible beyond its module (any `pub`, trait declarations and trait
1289
+ * impl members, which take the trait's visibility).
1290
+ */
1291
+ function rustPrivateModuleScope(def, ownerDefs) {
1292
+ const modifiers = Array.isArray(def.modifiers) ? def.modifiers : [];
1293
+ if (modifiers.some(modifier => /^pub\b/.test(modifier))) return null;
1294
+ if (def.traitImpl || ownerDefs.some(ownerDef => ownerDef.type === 'trait')) return null;
1295
+ if (!def.file || !def.file.endsWith('.rs')) return null;
1296
+ const path = require('path');
1297
+ const base = path.basename(def.file);
1298
+ const dir = ['mod.rs', 'lib.rs', 'main.rs'].includes(base)
1299
+ ? path.dirname(def.file)
1300
+ : def.file.slice(0, -3);
1301
+ return { file: def.file, dir: dir + path.sep };
1302
+ }
1303
+
1304
+ function rustDefVisibleFrom(entryDef, filePath) {
1305
+ const scope = entryDef.moduleScope;
1306
+ return !scope || filePath === scope.file || String(filePath).startsWith(scope.dir);
1307
+ }
1308
+
1309
+ /**
1310
+ * `a.producer().name()`: every project definition of `producer` declares a
1311
+ * return type whose own `name` method is not a future (and the type has no
1312
+ * future `name`). Then the chained call cannot reach a future producer.
1313
+ */
1314
+ function rustProducerReturnsSyncOwner(index, producer, name, owners) {
1315
+ const producers = (index.symbols.get(producer) || []).filter(isCallableDef);
1316
+ if (producers.length === 0) return false;
1317
+ for (const def of producers) {
1318
+ let owner = rustTypeBase(def.returnTypeResolved || def.returnType);
1319
+ if (owner === 'Self') owner = def.className || def.receiver || null;
1320
+ if (!owner || owners.has(owner)) return false;
1321
+ const methods = (index.symbols.get(name) || []).filter(method =>
1322
+ isCallableDef(method) && (method.className === owner || method.receiver === owner));
1323
+ if (methods.length === 0) return false;
1324
+ }
1325
+ return true;
1326
+ }
1327
+
1328
+ /**
1329
+ * Cheap name-level test on an indexed call record: can this call shape reach
1330
+ * any future producer of its name? (A bare call never reaches a method; a
1331
+ * typed receiver reaches only its own type's members or a trait's.)
1332
+ */
1333
+ function rustRecordMayReachFuture(record, futureDefs, index, filePath) {
1334
+ const all = record?.name ? futureDefs.get(record.name) : null;
1335
+ if (!all) return false;
1336
+ const visible = filePath ? all.defs.filter(entryDef => rustDefVisibleFrom(entryDef, filePath)) : all.defs;
1337
+ if (visible.length === 0) return false;
1338
+ const entry = {
1339
+ free: visible.some(entryDef => !entryDef.owner),
1340
+ owners: new Set(visible.filter(entryDef => entryDef.owner).map(entryDef => entryDef.owner)),
1341
+ traitDeclared: visible.some(entryDef => entryDef.traitDeclared),
1342
+ };
1343
+ if (!record.isMethod) return entry.free;
1344
+ if (index && record.receiverCall && !record.receiverType && !record.isPathCall &&
1345
+ rustProducerReturnsSyncOwner(index, record.receiverCall, record.name, entry.owners)) return false;
1346
+ if (record.isPathCall) {
1347
+ const parts = String(record.receiver || '').split('::').filter(Boolean);
1348
+ const last = parts[parts.length - 1];
1349
+ if (['std', 'core', 'alloc'].includes(parts[0])) return false;
1350
+ return entry.free || last === 'Self' || entry.owners.has(last) || entry.traitDeclared;
1351
+ }
1352
+ if (record.receiverType) return entry.owners.has(record.receiverType) || entry.traitDeclared;
1353
+ return entry.owners.size > 0;
1354
+ }
1355
+
1356
+ /**
1357
+ * Whether the type through which a call reached a project method can be that
1358
+ * method's owner. The engine pins receivers by type NAME; a `use std::fs::File`
1359
+ * or a `crate::loom::sync::Mutex` field type names a different type than a
1360
+ * same-name project type, and such a site is not audited.
1361
+ */
1362
+ function rustTargetTypeNamed(index, filePath, record, def) {
1363
+ const owner = def.className || def.receiver;
1364
+ if (!owner) return true;
1365
+ const { rustTypeNameDenotes } = require('./callers');
1366
+ const external = qualifier => ['std', 'core', 'alloc'].includes(String(qualifier || '').split('::')[0]);
1367
+ // Several project types share the owner's name: the naming site must
1368
+ // positively reach the target's file.
1369
+ const ambiguous = (index.symbols.get(owner) || []).filter(symbol =>
1370
+ ['struct', 'enum', 'type', 'trait', 'union'].includes(symbol.type)).length > 1;
1371
+ const denotes = (file) => {
1372
+ const verdict = rustTypeNameDenotes(index, file, owner, def.file);
1373
+ return verdict === 'yes' || (verdict === 'unknown' && !ambiguous);
1374
+ };
1375
+ if (record.receiverTypeQualifier && external(record.receiverTypeQualifier)) return false;
1376
+ if (record.isPathCall && record.receiver) {
1377
+ const parts = String(record.receiver).split('::').filter(Boolean);
1378
+ if (parts.length > 1 && external(parts[0])) return false;
1379
+ const last = parts[parts.length - 1];
1380
+ if (last === owner && parts.length === 1 && !denotes(filePath)) return false;
1381
+ return true;
1382
+ }
1383
+ if (record.receiverRootType && Array.isArray(record.receiverFields) && record.receiverFields.length > 0) {
1384
+ // The last field's declared type is written in its struct's file.
1385
+ let type = record.receiverRootType;
1386
+ let field = null;
1387
+ for (const name of record.receiverFields) {
1388
+ field = (index.symbols.get(name) || []).find(symbol =>
1389
+ symbol.type === 'field' && symbol.className === type && symbol.fieldType);
1390
+ if (!field) return !ambiguous;
1391
+ type = rustTypeBase(field.fieldType);
1392
+ }
1393
+ if (type !== owner) return !ambiguous;
1394
+ return denotes(field.file);
1395
+ }
1396
+ if (record.receiverType === owner && ['annotation', 'guess', 'constructor'].includes(record.receiverTypeSource) &&
1397
+ !denotes(filePath)) return false;
1398
+ return true;
1399
+ }
1400
+
1401
+
1402
+ function rustEnclosingName(node) {
1403
+ for (let cur = node.parent; cur; cur = cur.parent) {
1404
+ if (cur.type === 'function_item') return cur.childForFieldName('name')?.text || '<anonymous>';
1405
+ }
1406
+ return '<module>';
1407
+ }
1408
+
1409
+ /**
1410
+ * Can calling project method `member` on the value of a `name(..)` call take
1411
+ * a future for its Output? Only when some producer of that name is not
1412
+ * known, is a named future type, or has an Output type owning `member`.
1413
+ * Name-level: needs no syntax tree (fix #371 uses it as a prefilter).
1414
+ */
1415
+ function rustMemberMayMisuse(index, futureDefs, futureTypes, name, member) {
1416
+ const entry = futureDefs?.get(name);
1417
+ if (!entry) return true;
1418
+ return entry.defs.some(({ def }) => {
1419
+ const future = rustDefFuture(def, futureTypes);
1420
+ return !future || future.kind === 'named' ||
1421
+ rustOwnedMethods(index, member, rustTypeBase(future.output)).length > 0;
1422
+ });
1423
+ }
1424
+
1425
+ /**
1426
+ * Whether a call record's value is provably not lost without reading the
1427
+ * file (fix #371): awaited or passed on where it is produced, or the
1428
+ * receiver of a method that cannot take a future for its Output.
1429
+ */
1430
+ function rustRecordValueConsumed(index, futureDefs, futureTypes, record) {
1431
+ if (record.valueConsumed) return true;
1432
+ return !!record.consumingMethod &&
1433
+ !rustMemberMayMisuse(index, futureDefs, futureTypes, record.name, record.consumingMethod);
1434
+ }
1435
+
1436
+ function rustCallableSymbols(fileEntry) {
1437
+ return (fileEntry.symbols || []).filter(sym => isCallableDef(sym) && sym.startLine && sym.endLine);
1438
+ }
1439
+
1440
+ /** The innermost callable definition enclosing a line (the callee-query owner). */
1441
+ function rustSiteOwner(symbols, line) {
1442
+ let owner = null;
1443
+ for (const sym of symbols) {
1444
+ if (sym.startLine <= line && sym.endLine >= line &&
1445
+ (!owner || sym.startLine >= owner.startLine)) owner = sym;
1446
+ }
1447
+ return owner;
1448
+ }
1449
+
1450
+ /**
1451
+ * Engine targets of the call named `name` starting at `start`, read from the
1452
+ * owner's findCallees result: confirmed callees at that site, plus the
1453
+ * trait's own declaration for a possible dispatch through a trait (every
1454
+ * implementation must match it). [] when a dispatch trait has no declared
1455
+ * member of that name.
1456
+ */
1457
+ function rustSiteTargets(index, callees, start, line, name) {
1458
+ const atSite = callee => callee.name === name && (callee.siteProvenance || []).some(site =>
1459
+ site.start === start || (!Number.isInteger(site.start) && site.line === line));
1460
+ const targets = callees.filter(atSite);
1461
+ for (const entry of callees.unverifiedCallees || []) {
1462
+ if (entry.reason !== 'possible-dispatch' || !entry.dispatchVia || !atSite(entry)) continue;
1463
+ const declared = rustOwnedMethods(index, name, entry.dispatchVia);
1464
+ if (declared.length === 0) return [];
1465
+ targets.push(...declared);
1466
+ }
1467
+ return targets;
1468
+ }
1469
+
1470
+ /**
1471
+ * Resolve every possibly-lost candidate call record of a file through the
1472
+ * engine, before any parse (fix #371): one restricted findCallees run per
1473
+ * enclosing function. Returns { calleeMemo, needsTree } - needsTree is false
1474
+ * when no candidate can reach a future producer, so the file is not read.
1475
+ */
1476
+ function rustResolveCandidates(ctx, records) {
1477
+ const { index, futureTypes, fileEntry, filePath } = ctx;
1478
+ const symbols = rustCallableSymbols(fileEntry);
1479
+ const byOwner = new Map();
1480
+ for (const record of records) {
1481
+ const owner = rustSiteOwner(symbols, record.line);
1482
+ if (!owner) continue;
1483
+ if (!byOwner.has(owner)) byOwner.set(owner, new Set());
1484
+ byOwner.get(owner).add(record.callStart);
1485
+ }
1486
+ const calleeMemo = new Map();
1487
+ for (const [owner, siteStarts] of byOwner) {
1488
+ let callees;
1489
+ try {
1490
+ callees = index.findCallees(owner, { collectAccount: true, siteStarts }) || [];
1491
+ } catch (_) {
1492
+ callees = [];
1493
+ }
1494
+ calleeMemo.set(owner, callees);
1495
+ }
1496
+ let needsTree = false;
1497
+ for (const record of records) {
1498
+ const owner = rustSiteOwner(symbols, record.line);
1499
+ if (!owner) continue;
1500
+ const targets = rustSiteTargets(index, calleeMemo.get(owner), record.callStart, record.line, record.name);
1501
+ if (targets.length > 0 && targets.every(def => rustTargetTypeNamed(index, filePath, record, def) &&
1502
+ rustDefFuture(def, futureTypes))) {
1503
+ needsTree = true;
1504
+ break;
1505
+ }
1506
+ }
1507
+ return { calleeMemo, needsTree };
1508
+ }
1509
+
1510
+ /**
1511
+ * Audit one Rust file. `ctx`: { index, futureTypes, candidateNames,
1512
+ * fileEntry, filePath, tree, calls }. Returns issue records.
1513
+ */
1514
+ function auditRustFile(ctx) {
1515
+ const { index, futureTypes, candidateNames, fileEntry, filePath, tree } = ctx;
1516
+ const issues = [];
1517
+ const relativePath = fileEntry.relativePath || filePath;
1518
+ const calls = Array.isArray(ctx.calls) ? ctx.calls : [];
1519
+ const callsByStart = new Map();
1520
+ for (const call of calls) {
1521
+ if (Number.isInteger(call?.callStart)) callsByStart.set(call.callStart, call);
1522
+ }
1523
+ const symbols = rustCallableSymbols(fileEntry);
1524
+ // Engine resolution is batched per enclosing function and restricted to
1525
+ // the sites that need it (fix #371): a collecting pass records them, one
1526
+ // findCallees run per function resolves exactly those sites. A caller
1527
+ // may pass sites it already resolved (ctx.calleeMemo).
1528
+ const calleeMemo = ctx.calleeMemo || new Map();
1529
+ let pendingSites = null;
1530
+ const lostCandidates = [];
1531
+ const confirmedTargets = (callNode, name) => {
1532
+ const line = callNode.startPosition.row + 1;
1533
+ const owner = rustSiteOwner(symbols, line);
1534
+ if (!owner) return null;
1535
+ if (pendingSites) {
1536
+ if (!calleeMemo.has(owner)) {
1537
+ if (!pendingSites.has(owner)) pendingSites.set(owner, new Set());
1538
+ pendingSites.get(owner).add(callNode.startIndex);
1539
+ }
1540
+ return null;
1541
+ }
1542
+ if (!calleeMemo.has(owner)) {
1543
+ let callees;
1544
+ try {
1545
+ callees = index.findCallees(owner, { collectAccount: true }) || [];
1546
+ } catch (_) {
1547
+ callees = [];
1548
+ }
1549
+ calleeMemo.set(owner, callees);
1550
+ }
1551
+ return rustSiteTargets(index, calleeMemo.get(owner), callNode.startIndex, line, name);
1552
+ };
1553
+ const futureOfCall = (callNode, fnNode) => {
1554
+ let target = fnNode;
1555
+ if (target.type === 'generic_function') target = target.childForFieldName('function');
1556
+ if (!target) return null;
1557
+ let name;
1558
+ if (target.type === 'identifier') name = target.text;
1559
+ else if (target.type === 'scoped_identifier') name = target.childForFieldName('name')?.text;
1560
+ else if (target.type === 'field_expression') name = target.childForFieldName('field')?.text;
1561
+ if (!name) return null;
1562
+ if (target.type === 'identifier') {
1563
+ const local = rustLocalCallee(callNode, name);
1564
+ if (local?.shadowed) return null;
1565
+ if (local?.future) return { name, future: local.future };
1566
+ }
1567
+ if (!candidateNames.has(name)) return null;
1568
+ const record = callsByStart.get(callNode.startIndex);
1569
+ if (!record || record.name !== name ||
1570
+ (ctx.futureDefs && !rustRecordMayReachFuture(record, ctx.futureDefs, index, filePath))) return null;
1571
+ const targets = confirmedTargets(callNode, name);
1572
+ if (!targets || targets.length === 0) return null;
1573
+ if (targets.some(def => !rustTargetTypeNamed(index, filePath, record, def))) return null;
1574
+ const futures = targets.map(def => rustDefFuture(def, futureTypes));
1575
+ if (futures.some(future => !future)) return null;
1576
+ return { name, future: futures[0] };
1577
+ };
1578
+ const lazy = future => RUST_LAZY_FUTURE_KINDS.has(future.kind);
1579
+ const push = (node, name, reason, extra) => issues.push({
1580
+ file: relativePath,
1581
+ line: node.startPosition.row + 1,
1582
+ callerName: rustEnclosingName(node),
1583
+ calleeName: name,
1584
+ reason,
1585
+ ...extra,
1586
+ });
1587
+ const skipped = ctx.skipped || { count: 0 };
1588
+ // A value that is awaited or handed on needs no producer resolution;
1589
+ // only a possibly lost or misused value is resolved (engine callees).
1590
+ // A method called on the value is a misuse only when it is a project
1591
+ // method of some producer's Output type (or a field of an opaque future).
1592
+ const memberMayMisuse = (name, memberNode) => {
1593
+ const parent = memberNode?.parent;
1594
+ const call = parent?.parent;
1595
+ if (!(call?.type === 'call_expression' && fieldIs(call, 'function', parent))) return true;
1596
+ const member = parent.childForFieldName('field')?.text;
1597
+ return rustMemberMayMisuse(index, ctx.futureDefs, futureTypes, name, member);
1598
+ };
1599
+ const mayBeLost = (node, name) => {
1600
+ const flow = rustValueFlow(index, node, null);
1601
+ if (flow.kind === 'awaited' || flow.kind === 'flow') return false;
1602
+ if (flow.kind === 'member') return memberMayMisuse(name, flow.node);
1603
+ if (flow.kind !== 'stored') return true;
1604
+ const use = rustStoredFutureUse(index, flow.holder, flow.binding, null);
1605
+ if (!use.read) return true;
1606
+ if (!use.misuse) return false;
1607
+ const misuseFlow = rustValueFlow(index, use.misuse, null);
1608
+ return misuseFlow.kind !== 'member' || memberMayMisuse(name, misuseFlow.node);
1609
+ };
1610
+ const visit = node => {
1611
+ if (node.type === 'call_expression') {
1612
+ const fnNode = node.childForFieldName('function');
1613
+ const target = fnNode?.type === 'generic_function' ? fnNode.childForFieldName('function') : fnNode;
1614
+ const quickName = target?.type === 'identifier' ? target.text
1615
+ : (target?.childForFieldName('name') || target?.childForFieldName('field'))?.text;
1616
+ const plausible = quickName && (candidateNames.has(quickName) ||
1617
+ (target.type === 'identifier' && ctx.asyncClosures?.has(quickName)));
1618
+ if (pendingSites) {
1619
+ // Collecting pass: record the sites that need resolution.
1620
+ if (plausible && mayBeLost(node, quickName)) {
1621
+ lostCandidates.push(node);
1622
+ futureOfCall(node, fnNode);
1623
+ }
1624
+ return;
1625
+ }
1626
+ const resolved = plausible && mayBeLost(node, quickName) ? futureOfCall(node, fnNode) : null;
1627
+ if (resolved) {
1628
+ const { name, future } = resolved;
1629
+ const flow = rustValueFlow(index, node, future);
1630
+ const use = flow.kind === 'stored'
1631
+ ? rustStoredFutureUse(index, flow.holder, flow.binding, future) : null;
1632
+ const lost = flow.kind === 'discarded' || flow.kind === 'dropped' ||
1633
+ flow.kind === 'used-as-value' || (use && (!!use.misuse || !use.read));
1634
+ if (lost && !lazy(future)) {
1635
+ skipped.count++;
1636
+ } else if (flow.kind === 'discarded') {
1637
+ push(node, name, 'future-discarded');
1638
+ } else if (flow.kind === 'dropped') {
1639
+ push(node, name, 'future-dropped');
1640
+ } else if (flow.kind === 'used-as-value') {
1641
+ push(node, name, 'async-result-used-as-value');
1642
+ } else if (use) {
1643
+ if (use.misuse) {
1644
+ issues.push({
1645
+ file: relativePath,
1646
+ line: use.misuse.startPosition.row + 1,
1647
+ callerName: rustEnclosingName(node),
1648
+ calleeName: name,
1649
+ reason: 'stored-future-used-as-value',
1650
+ variable: flow.binding,
1651
+ originLine: node.startPosition.row + 1,
1652
+ });
1653
+ } else if (!use.read) {
1654
+ push(node, name, 'future-unused', { variable: flow.binding });
1655
+ }
1656
+ }
1657
+ }
1658
+ }
1659
+ };
1660
+ // Only indexed call records can be resolved: jump to each candidate
1661
+ // record's call node instead of walking the whole tree.
1662
+ const root = tree.rootNode;
1663
+ const seen = new Set();
1664
+ const starts = [];
1665
+ for (const record of calls) {
1666
+ if (!Number.isInteger(record?.callStart) || !Number.isInteger(record?.callEnd) || record.inMacro) continue;
1667
+ if (!(record.localShadow && ctx.asyncClosures?.has(record.name)) && !(ctx.futureDefs
1668
+ ? rustRecordMayReachFuture(record, ctx.futureDefs, index, filePath)
1669
+ : candidateNames.has(record.name))) continue;
1670
+ if (seen.has(record.callStart)) continue;
1671
+ seen.add(record.callStart);
1672
+ // Awaited, passed on, or the receiver of a method that cannot take
1673
+ // it for its Output (parser facts): nothing to resolve (fix #371).
1674
+ if (rustRecordValueConsumed(index, ctx.futureDefs, futureTypes, record)) continue;
1675
+ starts.push(record);
1676
+ }
1677
+ starts.sort((a, b) => a.callStart - b.callStart);
1678
+ const nodes = [];
1679
+ for (const record of starts) {
1680
+ let node = root.descendantForIndex(record.callStart, record.callEnd);
1681
+ while (node && !(node.type === 'call_expression' && node.startIndex === record.callStart &&
1682
+ node.endIndex === record.callEnd)) node = node.parent;
1683
+ if (node) nodes.push(node);
1684
+ }
1685
+ // Collect the sites whose producer needs engine resolution, resolve them
1686
+ // per enclosing function, then audit in source order.
1687
+ pendingSites = new Map();
1688
+ for (const node of nodes) visit(node);
1689
+ const batches = pendingSites;
1690
+ pendingSites = null;
1691
+ for (const [owner, siteStarts] of batches) {
1692
+ if (calleeMemo.has(owner)) continue;
1693
+ let callees;
1694
+ try {
1695
+ callees = index.findCallees(owner, { collectAccount: true, siteStarts }) || [];
1696
+ } catch (_) {
1697
+ callees = [];
1698
+ }
1699
+ calleeMemo.set(owner, callees);
1700
+ }
1701
+ for (const node of lostCandidates) visit(node);
1702
+ return issues;
1703
+ }
1704
+
1705
+ module.exports = {
1706
+ auditRustFile,
1707
+ rustResolveCandidates,
1708
+ rustRecordValueConsumed,
1709
+ rustFutureDefIndex,
1710
+ rustRecordMayReachFuture,
1711
+ rustProjectFutureTypes,
1712
+ rustDefFuture,
1713
+ valueFlow,
1714
+ storedValueRead,
1715
+ storedCoroutineMisuse,
1716
+ isCallableDef,
1717
+ isDefAsync,
1718
+ producerKind,
1719
+ collapseKinds,
1720
+ createLexicalResolver,
1721
+ createImportedProducerResolver,
1722
+ createModuleImportResolver,
1723
+ jsModuleExportDefinitions,
1724
+ jsModuleExportReaches: (index, file, name, want, memo) =>
1725
+ !!jsModuleExportDefinitions(index, file, name, memo, 0, new Set(), want)?.some(want),
1726
+ asyncConsumerRole,
1727
+ isDiscardedCall,
1728
+ resolveDecorator,
1729
+ };