ucn 5.3.4 → 5.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/callers.js CHANGED
@@ -12,8 +12,13 @@ const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('.
12
12
  const { isTestFile } = require('./discovery');
13
13
  const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath, CALLABLE_SYMBOL_KINDS } = require('./shared');
14
14
  const { _resolveJavaPackageImport } = require('./graph-build');
15
- const { scoreEdge, tierForResolution, TIER } = require('./confidence');
15
+ const { scoreEdge, tierForResolution, TIER, validateConfirmation } = require('./confidence');
16
+ const { summarizeProvenance, declarationIdentity, sameDeclaration } = require('./provenance');
17
+ const { confirmationFacts, occurrenceIdentity } = require('./provenance-facts');
18
+ const { BUILTIN_RECEIVER_TYPES, isProvenanceBuiltinReceiver } = require('./receiver-types');
16
19
  const { findGoModule, resolveRustImport } = require('./imports');
20
+ const { rustWrapperContract, validateRustWrapperContract } = require('./rust-result-flow');
21
+ const { pythonFixtureReceiver, pythonFixtureType } = require('./python-fixture-flow');
17
22
 
18
23
  const CONSTRUCTABLE_BINDING_KINDS = new Set([
19
24
  'class', 'struct', 'record', 'enum', 'function',
@@ -23,6 +28,45 @@ const RESERVED_RECEIVER_NAMES = new Set([
23
28
  ]);
24
29
  const CROSS_OPERATION_FLOW_CACHE_LIMIT = 4096;
25
30
 
31
+ function _confirmationFacts(index, file, call, targets, options = {}) {
32
+ return confirmationFacts(index, file, call, targets, {
33
+ ...options,
34
+ selectOverload: selectProvenanceOverload,
35
+ typeHead: name => _structuralTypeHead(name, { index, language: index.files.get(file)?.language }),
36
+ resolveType(name, context, line, qualified) {
37
+ const language = index.files.get(context)?.language;
38
+ const parts = name.split(name.includes('::') ? '::' : '.');
39
+ if (parts.length > 1) {
40
+ const simple = parts.pop();
41
+ const qualifier = parts.join(language === 'cpp' ? '::' : '.');
42
+ const named = (index.symbols.get(simple) || []).filter(d =>
43
+ IDENTITY_TYPE_KINDS.has(d.type) && d.namespace === qualifier &&
44
+ (d.file === context || index.importGraph.get(context)?.has(d.file)));
45
+ if (named.length === 1) return named[0];
46
+ }
47
+ if (language === 'java' || language === 'csharp') {
48
+ const definitions = (index.symbols.get(name) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
49
+ const selected = definitions.filter(d => _resolveReceiverTypeIdentity(
50
+ index, context, name, [{ ...d, className: name }], line, qualified) === 'target');
51
+ if (selected.length === 1) return selected[0];
52
+ }
53
+ if (language === 'java' && qualified) {
54
+ const nested = (index.symbols.get(name) || []).filter(d =>
55
+ IDENTITY_TYPE_KINDS.has(d.type) && d.enclosingType === qualified);
56
+ if (nested.length === 1) return nested[0];
57
+ }
58
+ const origin = _resolveFlowTypeOrigin(index, context, name, qualified);
59
+ if (!origin?.fromFile) return null;
60
+ const definitions = (index.symbols.get(name) || []).filter(d =>
61
+ (IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf)) &&
62
+ d.file === origin.fromFile &&
63
+ (!d.lexicalScopeStartLine || (line != null &&
64
+ line >= d.lexicalScopeStartLine && line <= d.lexicalScopeEndLine)));
65
+ return definitions.length === 1 ? definitions[0] : null;
66
+ },
67
+ });
68
+ }
69
+
26
70
  // `base` is a contextual receiver keyword only in C#. TypeScript and the
27
71
  // other supported languages may bind an ordinary local named base; treating
28
72
  // it as reserved globally suppresses otherwise exact assignment flow.
@@ -290,8 +334,9 @@ function findCallers(index, name, options = {}) {
290
334
  // lookup); the rest stay as shadow-style records. Display caps are handled
291
335
  // by formatters — this only bounds file reads.
292
336
  const unverifiedEnrichLimit = options.unverifiedEnrichLimit ?? 10;
293
- const recordExcluded = (filePath, line, reason) => {
294
- if (accountRaw) accountRaw.excludedEntries.push({ file: filePath, line, reason });
337
+ const recordExcluded = (filePath, line, reason, provenance) => {
338
+ if (accountRaw) accountRaw.excludedEntries.push({ file: filePath, line, reason,
339
+ ...(provenance && { provenance }) });
295
340
  };
296
341
 
297
342
  const definitions = index.symbols.get(name) || [];
@@ -556,7 +601,7 @@ function findCallers(index, name, options = {}) {
556
601
  // unverified-tier entry (tiered caller contract: shown in its own
557
602
  // section, never silently hidden). Does NOT count toward pendingCount —
558
603
  // totals describe the confirmed answer.
559
- const routeUnverified = (filePath, fileEntry, call, reason, calledAs, meta) => {
604
+ const routeUnverified = (filePath, fileEntry, call, reason, calledAs, meta, facts) => {
560
605
  if (!collectAccount) return; // non-account paths (trace/blast/verify) keep the plain drop
561
606
  const compilerSelectsOverload = cppOverloadDispatchTarget &&
562
607
  (reason === 'overload-ambiguous' || reason === 'ambiguous-binding' ||
@@ -582,11 +627,28 @@ function findCallers(index, name, options = {}) {
582
627
  _tier: TIER.UNVERIFIED, _reason: reason, _meta: meta,
583
628
  // Dispatch-tiered routes carry their own resolution so JSON output
584
629
  // distinguishes "possible virtual dispatch" from a bare uncertain.
585
- _evidence: reason === 'possible-dispatch' ? { possibleDispatch: true }
630
+ _evidence: { ...(reason === 'single-owner' ? { hasSingleOwnerEvidence: true }
631
+ : reason === 'possible-dispatch' ? { possibleDispatch: true }
586
632
  : reason === 'method-ambiguous' ? { methodAmbiguous: true }
587
- : { isUncertain: true },
633
+ : { isUncertain: true }), ...(facts && { facts }) },
588
634
  });
589
635
  };
636
+ const excludeReceiver = (filePath, fileEntry, call, calledAs, receiverOptions = {}) => {
637
+ const targets = options.targetDefinitions || definitions;
638
+ const facts = _confirmationFacts(index, filePath, call, targets, receiverOptions);
639
+ if ((BUILTIN_RECEIVER_TYPES.has(facts.receiverType) ||
640
+ (fileEntry.language === 'rust' && ['Option', 'Result', 'Vec'].includes(facts.receiverType))) &&
641
+ (facts.receiverTypeSource === 'literal' || !facts.receiverTypeDeclaration)) {
642
+ facts.builtinReceiver = { type: facts.receiverType, language: fileEntry.language };
643
+ }
644
+ const provenance = scoreEdge({ hasReceiverType: true, facts }).provenance;
645
+ const checked = validateConfirmation(provenance, facts.targets);
646
+ if (checked.verdict === 'establishes-other' || checked.verdict === 'unsupported') {
647
+ recordExcluded(filePath, call.line, 'receiver-type-mismatch', provenance);
648
+ } else {
649
+ routeUnverified(filePath, fileEntry, call, 'provenance-incomplete', calledAs, undefined, facts);
650
+ }
651
+ };
590
652
  const maxResults = options.maxResults;
591
653
  // BUG-H1: when consumers (like `about`) need an accurate truncation header
592
654
  // ("showing N of <total>"), they pass needsTotal:true so Phase 1 runs to
@@ -670,6 +732,21 @@ function findCallers(index, name, options = {}) {
670
732
  calledAs = call.name;
671
733
  }
672
734
 
735
+ const standardWrapper = fileEntry.language === 'rust'
736
+ ? _rustStandardWrapperMethod(index, filePath, call, calls) : null;
737
+ if (standardWrapper) {
738
+ const facts = { language: 'rust', site: occurrenceIdentity(fileEntry.relativePath, call),
739
+ targets: (options.targetDefinitions || definitions).map(declarationIdentity), standardWrapper };
740
+ const checked = validateConfirmation({ facts }, facts.targets);
741
+ if (checked.verdict === 'establishes-other') {
742
+ recordExcluded(filePath, call.line, 'standard-wrapper-method', {
743
+ rule: 'rust-standard-wrapper', rules: ['rust-standard-wrapper'], facts,
744
+ validation: checked.verdict, diagnostic: 'standard-wrapper-method',
745
+ });
746
+ continue;
747
+ }
748
+ }
749
+
673
750
  // A direct static call cannot cross an unrelated runtime
674
751
  // language boundary. A Python `service.get` is not a possible
675
752
  // target of JavaScript `map.get()`, nor can a Java method be
@@ -692,6 +769,8 @@ function findCallers(index, name, options = {}) {
692
769
  call = {
693
770
  ...call,
694
771
  receiverType: indexedType.type,
772
+ receiverTypeSource: 'flow',
773
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
695
774
  ...(indexedType.fromFile && {
696
775
  receiverTypeFlowFile: indexedType.fromFile,
697
776
  }),
@@ -925,7 +1004,7 @@ function findCallers(index, name, options = {}) {
925
1004
  // persisted with this file's calls.
926
1005
  if (call.isMethod && call.receiver &&
927
1006
  !_isReservedReceiver(fileEntry.language, call.receiver) &&
928
- (!call.receiverType || call.receiverTypeGuessed) &&
1007
+ (!call.receiverType || call.receiverTypeGuessed || call.receiverTypeSource === 'guess') &&
929
1008
  !call.receiverPatternShadow && !call.receiverFlowInvalidated &&
930
1009
  !call.receiverIsChainRoot &&
931
1010
  (langTraits(fileEntry.language)?.typeSystem === 'structural' ||
@@ -954,7 +1033,7 @@ function findCallers(index, name, options = {}) {
954
1033
  receiverExternalConcreteFlow: true,
955
1034
  }),
956
1035
  };
957
- if (call.receiverTypeGuessed) {
1036
+ if (call.receiverTypeGuessed || call.receiverTypeSource === 'guess') {
958
1037
  call = { ...call, receiverType: undefined, receiverTypeGuessed: undefined };
959
1038
  }
960
1039
  } else if (flowEntry) {
@@ -965,12 +1044,30 @@ function findCallers(index, name, options = {}) {
965
1044
  // annotation says *DefaultCodecRegistry; the guess
966
1045
  // excluded all three true RegisterCodec callers).
967
1046
  call = { ...call, receiverType: flowEntry.type,
1047
+ receiverTypeSource: 'flow',
1048
+ receiverTypeEvidence: { source: 'flow', ...flowEntry },
968
1049
  receiverTypeGuessed: undefined,
969
1050
  receiverFlowInvalidated: false,
970
1051
  ...(flowEntry.fromFile && { receiverTypeFlowFile: flowEntry.fromFile }) };
971
1052
  }
972
1053
  }
973
1054
 
1055
+ if (fileEntry.language === 'python' && call.isMethod && !call.receiverType) {
1056
+ const fixture = _pythonFixtureReceiverType(index, filePath, call);
1057
+ if (fixture) call = { ...call, receiverType: fixture.type,
1058
+ receiverTypeSource: 'fixture', receiverTypeEvidence: { source: 'fixture', ...fixture },
1059
+ ...(fixture.fromFile && { receiverTypeFlowFile: fixture.fromFile }) };
1060
+ const external = !fixture && _pythonExternalFieldFlow(index, filePath, call);
1061
+ if (external) {
1062
+ call = { ...call, receiverTypeSource: 'flow',
1063
+ receiverTypeEvidence: { source: 'flow', externalFactory: external.proof } };
1064
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs,
1065
+ { dispatchVia: external.via, externalContract: true },
1066
+ _confirmationFacts(index, filePath, call, options.targetDefinitions || definitions));
1067
+ continue;
1068
+ }
1069
+ }
1070
+
974
1071
  // Python indexed receivers (fix #324): `layout["body"].update()`
975
1072
  // dispatch through the container's compiler-visible
976
1073
  // `__getitem__` return contract. The parser retains only a
@@ -992,6 +1089,8 @@ function findCallers(index, name, options = {}) {
992
1089
  call = {
993
1090
  ...call,
994
1091
  receiverType: indexedType.type,
1092
+ receiverTypeSource: 'flow',
1093
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
995
1094
  ...(indexedType.fromFile && {
996
1095
  receiverTypeFlowFile: indexedType.fromFile,
997
1096
  }),
@@ -1028,7 +1127,9 @@ function findCallers(index, name, options = {}) {
1028
1127
  const items = _pythonDeclaredIterablePathItems(
1029
1128
  index, rootType, call.receiverIterationFields);
1030
1129
  const item = items?.[call.receiverIterationIndex || 0];
1031
- if (item) call = { ...call, receiverType: item };
1130
+ if (item) call = { ...call, receiverType: item,
1131
+ receiverTypeSource: 'flow',
1132
+ receiverTypeEvidence: { source: 'flow', type: item, iteration: call.receiver } };
1032
1133
  }
1033
1134
  }
1034
1135
 
@@ -1038,6 +1139,8 @@ function findCallers(index, name, options = {}) {
1038
1139
  index, fileEntry, call.receiver);
1039
1140
  if (importedType) {
1040
1141
  call = { ...call, receiverType: importedType.type,
1142
+ receiverTypeSource: 'flow',
1143
+ receiverTypeEvidence: { source: 'flow', ...importedType },
1041
1144
  receiverTypeFlowFile: importedType.fromFile };
1042
1145
  }
1043
1146
  }
@@ -1082,6 +1185,9 @@ function findCallers(index, name, options = {}) {
1082
1185
  call = {
1083
1186
  ...call,
1084
1187
  receiverType: returnInfo.type.replace(/\[\]$/, '').split('.').pop(),
1188
+ receiverTypeSource: 'flow',
1189
+ receiverTypeEvidence: { source: 'flow', ...returnInfo,
1190
+ producerPath: call.receiverCallTypePath },
1085
1191
  ...(returnInfo.fromFile && {
1086
1192
  receiverTypeFlowFile: returnInfo.fromFile,
1087
1193
  }),
@@ -1113,7 +1219,7 @@ function findCallers(index, name, options = {}) {
1113
1219
  // exact indexed variant and positional payload before method
1114
1220
  // dispatch; external qualified payloads remain external
1115
1221
  // provenance, never borrowed project identity.
1116
- if (collectAccount && fileEntry.language === 'rust' &&
1222
+ if ((collectAccount || call.receiverPatternSourceCallStart != null) && fileEntry.language === 'rust' &&
1117
1223
  call.isMethod && !call.receiverType &&
1118
1224
  call.receiverPatternVariant) {
1119
1225
  const patternType = _rustPatternReceiverType(
@@ -1122,6 +1228,8 @@ function findCallers(index, name, options = {}) {
1122
1228
  call = {
1123
1229
  ...call,
1124
1230
  receiverType: patternType.type,
1231
+ receiverTypeSource: 'flow',
1232
+ receiverTypeEvidence: { source: 'flow', ...patternType },
1125
1233
  ...(patternType.fromFile && {
1126
1234
  receiverTypeFlowFile: patternType.fromFile,
1127
1235
  }),
@@ -1246,10 +1354,14 @@ function findCallers(index, name, options = {}) {
1246
1354
  const builtinChained = _pythonBuiltinChainedReceiverType(
1247
1355
  index, fileEntry, call, foldCtx);
1248
1356
  if (builtinChained) {
1249
- call = { ...call, receiverType: builtinChained,
1357
+ call = { ...call, receiverType: builtinChained.type,
1358
+ receiverTypeSource: 'flow',
1359
+ receiverTypeEvidence: { source: 'flow', ...builtinChained },
1250
1360
  receiverTypePlatform: true };
1251
1361
  } else if (folded && folded.type) {
1252
1362
  call = { ...call, receiverType: folded.type,
1363
+ receiverTypeSource: 'flow',
1364
+ receiverTypeEvidence: { source: 'flow', ...folded },
1253
1365
  ...(folded.fromFile && { receiverTypeFlowFile: folded.fromFile }) };
1254
1366
  } else if (folded && folded.externalVia) {
1255
1367
  call = {
@@ -1262,6 +1374,8 @@ function findCallers(index, name, options = {}) {
1262
1374
  } else if (!folded?.suppressFallback) {
1263
1375
  const chainedType = _chainedReceiverType(index, call, fileEntry.language);
1264
1376
  if (chainedType) call = { ...call, receiverType: chainedType.type,
1377
+ receiverTypeSource: 'flow',
1378
+ receiverTypeEvidence: { source: 'flow', ...chainedType },
1265
1379
  ...(chainedType.fromFile && { receiverTypeFlowFile: chainedType.fromFile }) };
1266
1380
  }
1267
1381
  } else if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
@@ -1303,6 +1417,8 @@ function findCallers(index, name, options = {}) {
1303
1417
  };
1304
1418
  } else if (flowEntry) {
1305
1419
  call = { ...call, receiverType: flowEntry.type,
1420
+ receiverTypeSource: 'flow',
1421
+ receiverTypeEvidence: { source: 'flow', ...flowEntry },
1306
1422
  ...(flowEntry.fromFile && { receiverTypeFlowFile: flowEntry.fromFile }) };
1307
1423
  }
1308
1424
  }
@@ -1313,7 +1429,7 @@ function findCallers(index, name, options = {}) {
1313
1429
  // analysis resolves the source owner and its declared
1314
1430
  // `Iterator<Item = T>` contract. Item-preserving std adapters
1315
1431
  // such as rev/filter/take recurse to that same declaration.
1316
- if (collectAccount && fileEntry.language === 'rust' &&
1432
+ if (fileEntry.language === 'rust' &&
1317
1433
  call.isMethod && call.receiver && !call.receiverType &&
1318
1434
  (call.receiverIterationCall || call.receiverIterationVariable)) {
1319
1435
  let foldCtx = foldCtxCache.get(filePath);
@@ -1329,37 +1445,14 @@ function findCallers(index, name, options = {}) {
1329
1445
  } };
1330
1446
  foldCtxCache.set(filePath, foldCtx);
1331
1447
  }
1332
- let items = [];
1333
- if (call.receiverIterationVariable) {
1334
- const flow = _lookupReturnTypeFlow(foldCtx.getFlowMap(), {
1335
- ...call,
1336
- receiver: call.receiverIterationVariable,
1337
- });
1338
- if (flow?.iteratorItemType) {
1339
- items = [{
1340
- type: flow.iteratorItemType,
1341
- fromFile: flow.iteratorItemFromFile,
1342
- }];
1343
- }
1344
- } else {
1345
- const sources = _chainedProducerRecords(foldCtx, {
1346
- receiverCall: call.receiverIterationCall,
1347
- receiverCallIsMethod: call.receiverIterationCallIsMethod,
1348
- receiverCallLine: call.receiverIterationCallLine,
1349
- receiverCallStart: call.receiverIterationCallStart,
1350
- receiverCallEnd: call.receiverIterationCallEnd,
1351
- });
1352
- items = sources.map(source => _rustIteratorOutputItemType(
1353
- index, fileEntry, filePath, source, foldCtx));
1354
- }
1355
- if (items.length > 0 && items.every(Boolean) &&
1356
- new Set(items.map(item => item.type)).size === 1 &&
1357
- new Set(items.map(item => item.fromFile)).size === 1 &&
1358
- items[0].fromFile) {
1448
+ const item = _rustIterationReceiverType(index, fileEntry, filePath, call, foldCtx);
1449
+ if (item) {
1359
1450
  call = {
1360
1451
  ...call,
1361
- receiverType: items[0].type,
1362
- receiverTypeFlowFile: items[0].fromFile,
1452
+ receiverType: item.type,
1453
+ receiverTypeSource: 'flow',
1454
+ receiverTypeEvidence: { source: 'flow', ...item },
1455
+ receiverTypeFlowFile: item.fromFile,
1363
1456
  };
1364
1457
  }
1365
1458
  }
@@ -1393,6 +1486,8 @@ function findCallers(index, name, options = {}) {
1393
1486
  call = {
1394
1487
  ...call,
1395
1488
  receiverType: closureType.type,
1489
+ receiverTypeSource: 'flow',
1490
+ receiverTypeEvidence: { source: 'flow', ...closureType },
1396
1491
  receiverTypeFlowFile: closureType.fromFile,
1397
1492
  };
1398
1493
  }
@@ -1590,7 +1685,7 @@ function findCallers(index, name, options = {}) {
1590
1685
  dispatchCandidates: countDispatchCandidates(call.receiverType),
1591
1686
  });
1592
1687
  } else {
1593
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
1688
+ excludeReceiver(filePath, fileEntry, call, calledAs);
1594
1689
  }
1595
1690
  }
1596
1691
  continue;
@@ -1619,9 +1714,9 @@ function findCallers(index, name, options = {}) {
1619
1714
  const cbTypedMatch = call.receiverType && cbTypes.has(call.receiverType);
1620
1715
  const cbAllTypeTargets = cbTargetDefs.length > 0 &&
1621
1716
  cbTargetDefs.every(d => IDENTITY_TYPE_KINDS.has(d.type));
1622
- if (!cbTypeQualified && !cbTypedMatch &&
1623
- (methodOwnerKeys().size > 1 || cbAllTypeTargets)) {
1624
- routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs,
1717
+ if (!cbTypeQualified && !cbTypedMatch) {
1718
+ routeUnverified(filePath, fileEntry, call,
1719
+ methodOwnerKeys().size === 1 && !cbAllTypeTargets ? 'single-owner' : 'method-ambiguous', calledAs,
1625
1720
  { dispatchCandidates: methodOwnerKeys().size });
1626
1721
  continue;
1627
1722
  }
@@ -2005,6 +2100,7 @@ function findCallers(index, name, options = {}) {
2005
2100
  // resolution, but its evidence grade is receiver-hint, not
2006
2101
  // same-class (the receiver is the field's class).
2007
2102
  let resolvedByTypedAttribute = false;
2103
+ let resolvedReceiverFacts = {};
2008
2104
  // Receiver/path type known to mismatch the target: such an edge can
2009
2105
  // never tier as confirmed even when legacy includeUncertain keeps it
2010
2106
  // visible (scoreEdge checks hasReceiverType before isUncertain, so
@@ -2053,6 +2149,9 @@ function findCallers(index, name, options = {}) {
2053
2149
  index, fileEntry, call.receiver);
2054
2150
  if (staticFieldType) {
2055
2151
  call = { ...call, receiverType: staticFieldType,
2152
+ receiverTypeSource: 'field',
2153
+ receiverTypeEvidence: { source: 'field', field: call.receiver,
2154
+ type: staticFieldType, imports: fileEntry.importBindings },
2056
2155
  receiverIsTypeQualified: false };
2057
2156
  }
2058
2157
  }
@@ -2160,6 +2259,12 @@ function findCallers(index, name, options = {}) {
2160
2259
  if (compatibleTypes.has(targetClass)) {
2161
2260
  resolvedBySameClass = true;
2162
2261
  resolvedByTypedAttribute = true;
2262
+ resolvedReceiverFacts = {
2263
+ receiverType: targetClass, receiverTypeSource: 'field',
2264
+ receiverOrigin: { source: 'field', rootType: callerSymbol.className,
2265
+ field: call.selfAttribute, scope: declarationIdentity(callerSymbol),
2266
+ ...attrTypes?.origins?.get(call.selfAttribute) },
2267
+ };
2163
2268
  } else if (_isAncestorOfTargetClass(index, targetClass, tDefs)) {
2164
2269
  // A field declared as a strict ancestor can
2165
2270
  // dynamically hold the pinned override.
@@ -2172,7 +2277,11 @@ function findCallers(index, name, options = {}) {
2172
2277
  // type is exact negative evidence. Earlier
2173
2278
  // code discarded this field contract and
2174
2279
  // emitted method-no-evidence.
2175
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
2280
+ excludeReceiver(filePath, fileEntry, call, calledAs, {
2281
+ receiverType: targetClass, receiverTypeSource: 'field',
2282
+ receiverOrigin: { source: 'field', rootType: callerSymbol?.className,
2283
+ field: call.selfAttribute, ...attrTypes?.origins?.get(call.selfAttribute) },
2284
+ });
2176
2285
  continue;
2177
2286
  } else if (options.collectAccount || !options.includeMethods) {
2178
2287
  routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
@@ -2351,7 +2460,9 @@ function findCallers(index, name, options = {}) {
2351
2460
  const builtinFieldType = _pythonBuiltinFieldPathType(
2352
2461
  index, fileEntry, call.receiverRoot, call.receiverFields);
2353
2462
  if (builtinFieldType) {
2354
- call = { ...call, receiverType: builtinFieldType,
2463
+ call = { ...call, receiverType: builtinFieldType.type,
2464
+ receiverTypeSource: 'field',
2465
+ receiverTypeEvidence: { source: 'field', ...builtinFieldType },
2355
2466
  receiverTypePlatform: true };
2356
2467
  }
2357
2468
  }
@@ -2774,6 +2885,42 @@ function findCallers(index, name, options = {}) {
2774
2885
  // modules must not exclude: a binding to an unresolved module
2775
2886
  // whose first segment matches a project directory routes
2776
2887
  // visible instead.
2888
+ // Rust item renames are name-level ownership (fix #357): a
2889
+ // bare call spelled by a `use path::name as local` alias
2890
+ // denotes exactly the item that binding resolves to. Two
2891
+ // renames of one source name from different modules
2892
+ // (`use alpha::widget as a; use beta::widget as b`) used to
2893
+ // fall through to file-level import evidence, which BOTH
2894
+ // modules satisfy, so each pin confirmed both sites. The
2895
+ // paired binding decides: resolves into a target file →
2896
+ // confirmable; resolves to another project file → excluded
2897
+ // other-definition-import; unresolvable → visible (a resolver
2898
+ // gap is never exclusion evidence).
2899
+ if (calledAs && !call.isMethod && !call.receiver &&
2900
+ fileEntry.language === 'rust' && call.name === calledAs) {
2901
+ const paired = (fileEntry.importBindings || []).filter(b =>
2902
+ b.alias === call.name);
2903
+ if (paired.length > 0) {
2904
+ const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
2905
+ let verdict = 'unknown';
2906
+ for (const b of paired) {
2907
+ const resolved = _rustBindingResolvedFiles(index, fileEntry, filePath, b);
2908
+ if (resolved.size === 0) { verdict = 'unknown'; break; }
2909
+ const hit = [...resolved].some(f => tFiles.has(f));
2910
+ if (hit) { verdict = 'target'; break; }
2911
+ verdict = 'other';
2912
+ }
2913
+ if (verdict === 'other') {
2914
+ recordExcluded(filePath, call.line, 'other-definition-import');
2915
+ continue;
2916
+ }
2917
+ if (verdict === 'unknown' && collectAccount) {
2918
+ routeUnverified(filePath, fileEntry, call, 'no-import-link', calledAs);
2919
+ continue;
2920
+ }
2921
+ }
2922
+ }
2923
+
2777
2924
  if (!bindingId && !call.isMethod &&
2778
2925
  langTraits(fileEntry.language)?.typeSystem === 'structural' &&
2779
2926
  (fileEntry.importBindings || []).length > 0) {
@@ -3036,6 +3183,13 @@ function findCallers(index, name, options = {}) {
3036
3183
  continue;
3037
3184
  }
3038
3185
  const targetHasClass = targetDefs.some(d => d.className);
3186
+ if (call.isMethod && !targetHasClass && fileEntry.language === 'c') {
3187
+ // C member calls invoke function-pointer fields. The
3188
+ // pointer can hold the pinned free function; member
3189
+ // syntax alone is neither binding nor exclusion proof.
3190
+ routeUnverified(filePath, fileEntry, call, 'callable-field', calledAs);
3191
+ continue;
3192
+ }
3039
3193
  if (call.isMethod && !targetHasClass) {
3040
3194
  // Method call but target is a standalone function — skip
3041
3195
  recordExcluded(filePath, call.line, 'method-kind-mismatch');
@@ -3227,7 +3381,7 @@ function findCallers(index, name, options = {}) {
3227
3381
  isUncertain = true;
3228
3382
  typeMismatch = true;
3229
3383
  if (collectAccount) {
3230
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3384
+ excludeReceiver(filePath, fileEntry, call, calledAs);
3231
3385
  continue;
3232
3386
  }
3233
3387
  if (!options.includeUncertain) {
@@ -3303,7 +3457,7 @@ function findCallers(index, name, options = {}) {
3303
3457
  isUncertain = true;
3304
3458
  typeMismatch = true;
3305
3459
  if (collectAccount) {
3306
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3460
+ excludeReceiver(filePath, fileEntry, call, calledAs);
3307
3461
  continue;
3308
3462
  }
3309
3463
  if (!options.includeUncertain) {
@@ -3312,6 +3466,14 @@ function findCallers(index, name, options = {}) {
3312
3466
  }
3313
3467
  }
3314
3468
 
3469
+ // C# dynamic is an unresolved runtime receiver, never a
3470
+ // concrete foreign type that can disprove a project edge.
3471
+ if (fileEntry.language === 'csharp' && call.receiverType === 'dynamic') {
3472
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs,
3473
+ { dispatchVia: 'dynamic' });
3474
+ continue;
3475
+ }
3476
+
3315
3477
  // Receiver-class disambiguation:
3316
3478
  // When the target definition has a class/receiver type, filter callers
3317
3479
  // whose receiverType is known to be a different type.
@@ -3571,7 +3733,8 @@ function findCallers(index, name, options = {}) {
3571
3733
  // Not evidence against — visible possible-dispatch.
3572
3734
  // Go struct embedding binds statically and stays
3573
3735
  // excluded.
3574
- if (_dispatchCapableSupertype(index, fileEntry.language, knownType, targetDefs, definitions)) {
3736
+ if (_dispatchCapableSupertype(index, fileEntry.language, knownType, targetDefs, definitions) ||
3737
+ (!knownTypeHasProjectIdentity && externalContractTarget()?.via === knownType)) {
3575
3738
  const externalContract = externalContractTarget();
3576
3739
  routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
3577
3740
  dispatchVia: knownType,
@@ -3582,7 +3745,11 @@ function findCallers(index, name, options = {}) {
3582
3745
  });
3583
3746
  continue;
3584
3747
  }
3585
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3748
+ excludeReceiver(filePath, fileEntry, call, calledAs, fieldHopType && !call.receiverType ? {
3749
+ receiverType: fieldHopType, receiverTypeSource: 'field',
3750
+ receiverOrigin: { source: 'field', root: call.receiverRoot,
3751
+ rootType: call.receiverRootType, field: call.receiverField },
3752
+ } : {});
3586
3753
  continue;
3587
3754
  }
3588
3755
  if (!options.includeUncertain) {
@@ -3695,7 +3862,7 @@ function findCallers(index, name, options = {}) {
3695
3862
  isUncertain = true;
3696
3863
  typeMismatch = true;
3697
3864
  if (collectAccount) {
3698
- recordExcluded(filePath, call.line, 'receiver-type-mismatch');
3865
+ excludeReceiver(filePath, fileEntry, call, calledAs);
3699
3866
  continue;
3700
3867
  }
3701
3868
  if (!options.includeUncertain) {
@@ -3736,6 +3903,10 @@ function findCallers(index, name, options = {}) {
3736
3903
  }
3737
3904
  inferredMatch = true;
3738
3905
  nominalInferredMatch = true;
3906
+ resolvedReceiverFacts = {
3907
+ receiverType: typeReceiver, receiverTypeSource: 'type-qualified',
3908
+ receiverOrigin: { source: 'type-qualified', site: occurrenceIdentity(fileEntry.relativePath, call) },
3909
+ };
3739
3910
  }
3740
3911
  // Still no type — fall back to receiver name matching when
3741
3912
  // multiple defs exist. A field-declared interface/trait type
@@ -4100,7 +4271,9 @@ function findCallers(index, name, options = {}) {
4100
4271
  let aliasResolvedFile = null;
4101
4272
  if (receiverName && !tTypes.has(receiverName)) {
4102
4273
  for (const im of (fileEntry.importBindings || [])) {
4103
- if (im.name !== receiverName) continue;
4274
+ // fix #357: Rust rename bindings carry the local
4275
+ // name as `alias` (the original is `name`).
4276
+ if ((im.alias || im.name) !== receiverName) continue;
4104
4277
  // fix #353: C# `using BH = Beta.Helper` (and Java
4105
4278
  // dotted paths) split on `.`; Rust paths on `::`.
4106
4279
  const orig = String(im.module || '').split(/::|\./).pop();
@@ -4154,6 +4327,13 @@ function findCallers(index, name, options = {}) {
4154
4327
  continue;
4155
4328
  }
4156
4329
  }
4330
+ if (typeQualifiedReceiver) {
4331
+ resolvedReceiverFacts = {
4332
+ receiverType: receiverName, receiverTypeSource: 'type-qualified',
4333
+ receiverOrigin: { source: 'type-qualified', site: occurrenceIdentity(fileEntry.relativePath, call) },
4334
+ ...(aliasResolvedFile && { originFile: aliasResolvedFile }),
4335
+ };
4336
+ }
4157
4337
  if (!typeQualifiedReceiver) {
4158
4338
  // External-producer receiver (fix #220): the variable
4159
4339
  // was assigned from a call into an external package
@@ -4344,6 +4524,15 @@ function findCallers(index, name, options = {}) {
4344
4524
  }
4345
4525
  }
4346
4526
  }
4527
+ // Module ownership is evidence for an exported item,
4528
+ // not for a method nested inside that module's type.
4529
+ // A same-named free function and method can share the
4530
+ // target file (itertools::peek_nth vs PeekNth::peek_nth).
4531
+ if (call.moduleOwnedPath && fileEntry.language === 'rust' &&
4532
+ targetDefs2.every(d => d.className || d.receiver)) {
4533
+ routeUnverified(filePath, fileEntry, call, 'provenance-incomplete', calledAs);
4534
+ continue;
4535
+ }
4347
4536
  const knownDispatchType = call.receiverType || fieldHopType || fieldDispatchType;
4348
4537
  // Module-owned qualified calls (fix #260b) are resolved
4349
4538
  // by OWNERSHIP — the dispatch-ambiguity routing below is
@@ -4373,8 +4562,11 @@ function findCallers(index, name, options = {}) {
4373
4562
  });
4374
4563
  continue;
4375
4564
  }
4376
- if (!call.moduleOwnedPath && !knownDispatchType && methodOwnerKeys().size > 1) {
4377
- routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
4565
+ if (!call.moduleOwnedPath && !knownDispatchType) {
4566
+ const contract = externalContractTarget();
4567
+ routeUnverified(filePath, fileEntry, call,
4568
+ contract ? 'possible-dispatch' : methodOwnerKeys().size === 1 ? 'single-owner' : 'method-ambiguous', calledAs, {
4569
+ ...(contract && { dispatchVia: contract.via, externalContract: true }),
4378
4570
  dispatchCandidates: methodOwnerKeys().size,
4379
4571
  });
4380
4572
  continue;
@@ -4566,8 +4758,8 @@ function findCallers(index, name, options = {}) {
4566
4758
  // in a file that imports _decoders.py is bytes.decode, not
4567
4759
  // ContentDecoder.decode. An untyped-receiver method call
4568
4760
  // confirms only via binding, same-class, a validated receiver
4569
- // type, a type-qualified receiver (Class.method static style),
4570
- // or a single project-wide owner. Multi-owner name matches
4761
+ // type or a type-qualified receiver (Class.method static style).
4762
+ // Single-owner and multi-owner name matches
4571
4763
  // route VISIBLE method-ambiguous — never dropped. Same for a
4572
4764
  // bare call against pure method targets (a bare name cannot
4573
4765
  // denote a method in JS/TS/Python — only a rebound alias can,
@@ -4583,7 +4775,11 @@ function findCallers(index, name, options = {}) {
4583
4775
  if (call.isMethod && !call.receiverIsModule &&
4584
4776
  !recvSubmoduleRel && !call.moduleOwnedPath) {
4585
4777
  const tTypes = dispatchTargetTypes(targetDefs2);
4586
- const typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
4778
+ let typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
4779
+ if (typeQualifiedReceiver) resolvedReceiverFacts = {
4780
+ receiverType: call.receiver, receiverTypeSource: 'type-qualified',
4781
+ receiverOrigin: { source: 'type-qualified', site: occurrenceIdentity(fileEntry.relativePath, call) },
4782
+ };
4587
4783
  const knownDispatchType = call.receiverType ||
4588
4784
  fieldHopType || fieldDispatchType;
4589
4785
  // A compiler/parser-known receiver type that is not a
@@ -4758,6 +4954,8 @@ function findCallers(index, name, options = {}) {
4758
4954
  });
4759
4955
  continue;
4760
4956
  }
4957
+ typeQualifiedReceiver = true;
4958
+ call = { ...call, moduleOwnedPath: true };
4761
4959
  }
4762
4960
  // External-contract single owner (fix #210): same
4763
4961
  // physics as the nominal gate above — an override
@@ -4787,7 +4985,7 @@ function findCallers(index, name, options = {}) {
4787
4985
  });
4788
4986
  continue;
4789
4987
  }
4790
- if (!typeQualifiedReceiver && methodOwnerKeys().size > 1) {
4988
+ if (!typeQualifiedReceiver) {
4791
4989
  const knownDispatchType = call.receiverType || fieldHopType || fieldDispatchType;
4792
4990
  if (knownDispatchType) {
4793
4991
  // Known-but-unvalidated type (supertype of the
@@ -4801,7 +4999,7 @@ function findCallers(index, name, options = {}) {
4801
4999
  dispatchCandidates: countDispatchCandidates(knownDispatchType),
4802
5000
  });
4803
5001
  } else {
4804
- routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
5002
+ routeUnverified(filePath, fileEntry, call, methodOwnerKeys().size === 1 ? 'single-owner' : 'method-ambiguous', calledAs, {
4805
5003
  dispatchCandidates: methodOwnerKeys().size,
4806
5004
  });
4807
5005
  }
@@ -4876,6 +5074,17 @@ function findCallers(index, name, options = {}) {
4876
5074
  }
4877
5075
  }
4878
5076
 
5077
+ if (!collectAccount && call.isMethod && !call.isConstructor &&
5078
+ !resolvedBySameClass && !receiverTypeValidated && !nominalInferredMatch &&
5079
+ !resolvedByExtensionMethod && !call.moduleOwnedPath &&
5080
+ !call.receiverIsModule && !recvSubmoduleRel) {
5081
+ const qualified = _calleeTypeQualifiedReceiver(index,
5082
+ { ...callerSymbol, file: filePath }, fileEntry, call, fileEntry.language);
5083
+ const exact = qualified?.match && targetDefs.some(target =>
5084
+ target.file === qualified.match.file && target.startLine === qualified.match.startLine);
5085
+ if (!exact) continue;
5086
+ }
5087
+
4879
5088
  if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
4880
5089
  pendingByFile.get(filePath).push({
4881
5090
  call, fileEntry, callerSymbol,
@@ -4891,6 +5100,19 @@ function findCallers(index, name, options = {}) {
4891
5100
  receiverType: call.receiverType,
4892
5101
  calledAs,
4893
5102
  _evidence: {
5103
+ facts: _confirmationFacts(index, filePath, call, targetDefs, {
5104
+ bindingId,
5105
+ ...resolvedReceiverFacts,
5106
+ sameClass: !!resolvedBySameClass && !resolvedByTypedAttribute,
5107
+ ...(fieldHopType && !call.receiverType && {
5108
+ receiverType: fieldHopType, receiverTypeSource: 'field',
5109
+ receiverOrigin: { source: 'field', root: call.receiverRoot,
5110
+ rootType: call.receiverRootType, field: call.receiverField },
5111
+ }),
5112
+ }),
5113
+ typeQualifiedReceiver: resolvedReceiverFacts.receiverTypeSource === 'type-qualified',
5114
+ moduleOwnedPath: !!call.moduleOwnedPath,
5115
+ extensionMethod: !!resolvedByExtensionMethod,
4894
5116
  hasBindingId: !!bindingId,
4895
5117
  resolvedBySameClass: !!resolvedBySameClass && !resolvedByTypedAttribute,
4896
5118
  hasSamePackageEvidence,
@@ -4912,8 +5134,8 @@ function findCallers(index, name, options = {}) {
4912
5134
  // The dispatch gates above have already rejected
4913
5135
  // external contracts, universal methods, wrong arity,
4914
5136
  // unresolved producer flow, and multi-owner names.
4915
- // What remains with exactly one project owner is
4916
- // positive project-scope identity evidence.
5137
+ // Owner count describes unresolved candidates. It
5138
+ // never establishes the identity of a value receiver (#355).
4917
5139
  hasSingleOwnerEvidence: !!(collectAccount && call.isMethod &&
4918
5140
  !call.inMacroDefinition && !call.isMacro &&
4919
5141
  !call.receiverType && !fieldHopType &&
@@ -4967,14 +5189,43 @@ function findCallers(index, name, options = {}) {
4967
5189
  for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference,
4968
5190
  isTypeReference, receiver, receiverType, calledAs, _evidence, _tier,
4969
5191
  _reason, _meta } of pending) {
4970
- const scored = scoreEdge(_evidence || {});
5192
+ const evidence = {
5193
+ ...(_evidence || {}),
5194
+ ...(_reason && { reason: _reason }),
5195
+ facts: _evidence?.facts || _confirmationFacts(index, filePath,
5196
+ call, options.targetDefinitions || definitions, {
5197
+ sameClass: _evidence?.resolvedBySameClass,
5198
+ }),
5199
+ };
5200
+ let scored = scoreEdge(evidence);
5201
+ let routedTier = _tier;
5202
+ let routedReason = _reason;
5203
+ const migrated = isMethod && !call.isConstructor && !evidence.resolvedBySameClass &&
5204
+ !evidence.typeQualifiedReceiver && !evidence.moduleOwnedPath && !evidence.extensionMethod &&
5205
+ (evidence.hasReceiverType || evidence.resolvedByReceiverHint || evidence.hasSingleOwnerEvidence);
5206
+ if (!_tier && migrated && collectAccount) {
5207
+ const checked = validateConfirmation(scored.provenance, evidence.facts.targets);
5208
+ if (checked.verdict === 'establishes-other') {
5209
+ recordExcluded(filePath, call.line, 'receiver-target-different', scored.provenance);
5210
+ continue;
5211
+ }
5212
+ if (checked.verdict !== 'establishes-target' && checked.verdict !== 'unsupported') {
5213
+ if (!collectAccount) continue;
5214
+ routedTier = TIER.UNVERIFIED;
5215
+ routedReason = evidence.hasSingleOwnerEvidence && !evidence.hasReceiverType
5216
+ ? 'single-owner' : 'provenance-incomplete';
5217
+ const ranking = scoreEdge({ isUncertain: true,
5218
+ hasSingleOwnerEvidence: routedReason === 'single-owner' });
5219
+ scored = { ...ranking, provenance: scored.provenance };
5220
+ }
5221
+ }
4971
5222
  // Family B contract field (fix #221): a bind/call/apply site reaches
4972
5223
  // the target through Function.prototype indirection, not direct call
4973
5224
  // syntax — label the edge calledAs:'bound'. Rename aliases keep their
4974
5225
  // surface name (they describe the same slot and are rarer). Label
4975
5226
  // only, computed at edge construction: routing logic never sees it.
4976
5227
  const edgeCalledAs = calledAs || (call.boundCall ? 'bound' : undefined);
4977
- if (_tier) {
5228
+ if (routedTier) {
4978
5229
  // Routed unverified entry — never competes with the main
4979
5230
  // answer for maxResults/enrichLimit slots.
4980
5231
  const base = {
@@ -4986,8 +5237,9 @@ function findCallers(index, name, options = {}) {
4986
5237
  evidenceScore: scored.evidenceScore,
4987
5238
  scoreKind: scored.scoreKind,
4988
5239
  resolution: scored.resolution,
4989
- tier: _tier,
4990
- reason: _reason,
5240
+ ...(collectAccount && { provenance: scored.provenance }),
5241
+ tier: routedTier,
5242
+ reason: routedReason,
4991
5243
  ...(_meta || {}),
4992
5244
  isMethod: call.isMethod || false,
4993
5245
  ...(isFunctionReference && { isFunctionReference: true }),
@@ -5036,6 +5288,7 @@ function findCallers(index, name, options = {}) {
5036
5288
  evidenceScore: scored.evidenceScore,
5037
5289
  scoreKind: scored.scoreKind,
5038
5290
  resolution: scored.resolution,
5291
+ ...(collectAccount && { provenance: scored.provenance }),
5039
5292
  ...(tier && { tier }),
5040
5293
  isMethod: call.isMethod || false,
5041
5294
  ...(isFunctionReference && { isFunctionReference: true }),
@@ -5074,6 +5327,7 @@ function findCallers(index, name, options = {}) {
5074
5327
  evidenceScore: scored.evidenceScore,
5075
5328
  scoreKind: scored.scoreKind,
5076
5329
  resolution: scored.resolution,
5330
+ ...(collectAccount && { provenance: scored.provenance }),
5077
5331
  ...(tier && { tier }),
5078
5332
  });
5079
5333
  enrichedCount++;
@@ -5211,6 +5465,30 @@ function findCallees(index, definition, options = {}) {
5211
5465
  : [];
5212
5466
 
5213
5467
  const callees = new Map(); // key -> { name, bindingId, count }
5468
+ const siteEvidence = new Map();
5469
+ const provenanceForSite = (siteId, target, reason) => {
5470
+ const record = siteEvidence.get(siteId) || { call: calls[siteId], evidence: {} };
5471
+ const call = record.call;
5472
+ const evidence = { ...record.evidence, ...(reason && { reason }) };
5473
+ evidence.facts = _confirmationFacts(index, def.file, call,
5474
+ target ? [target] : (index.symbols.get(call.name) || []), record.options);
5475
+ // A bindingId synthesized by receiver resolution is not a
5476
+ // lexical binding of the method token. Rank the actual site facts.
5477
+ if (call.isMethod && !call.receiverIsModule && !call.moduleOwnedPath &&
5478
+ !evidence.typeQualifiedReceiver && !evidence.extensionMethod) evidence.hasBindingId = false;
5479
+ if (!call.isMethod && target && !evidence.hasBindingId) {
5480
+ evidence.hasImportEvidence = (index.symbols.get(call.name) || []).length === 1 ||
5481
+ target.file === def.file || index.importGraph.get(def.file)?.has(target.file);
5482
+ }
5483
+ const scored = scoreEdge(evidence);
5484
+ return {
5485
+ ...occurrenceIdentity(fileEntry?.relativePath || def.file, call, siteId),
5486
+ ...(target && { targetDef: declarationIdentity(target) }),
5487
+ confidence: scored.confidence, evidenceScore: scored.evidenceScore,
5488
+ resolution: scored.resolution, scoreKind: scored.scoreKind,
5489
+ provenance: scored.provenance,
5490
+ };
5491
+ };
5214
5492
  let selfAttrCalls = null; // collected for Python self.attr.method() resolution
5215
5493
  let selfMethodCalls = null; // collected for Python self.method() resolution
5216
5494
 
@@ -5259,7 +5537,7 @@ function findCallees(index, definition, options = {}) {
5259
5537
  };
5260
5538
  // Retain an uncertain/unresolved call as a visible unverified callee
5261
5539
  // entry (aggregated by name+reason) and claim its site.
5262
- const noteUnverified = (siteId, call, reason, meta = {}) => {
5540
+ const noteUnverified = (siteId, call, reason, meta = {}, siteProof) => {
5263
5541
  if (!collectAccount || claimedSiteIds.has(siteId)) return;
5264
5542
  noteSite(siteId, 'unverified', reason, call);
5265
5543
  const key = `${call.name}|${reason}|${meta.dispatchVia || ''}`;
@@ -5272,6 +5550,13 @@ function findCallees(index, definition, options = {}) {
5272
5550
  }
5273
5551
  entry.callCount++;
5274
5552
  entry.sites.push(call.line);
5553
+ if (!entry.siteProvenance) entry.siteProvenance = [];
5554
+ const unverifiedScore = scoreEdge({ isUncertain: true, reason });
5555
+ entry.siteProvenance.push({ ...(siteProof || provenanceForSite(siteId, null, reason)),
5556
+ tier: TIER.UNVERIFIED, reason,
5557
+ confidence: unverifiedScore.confidence, evidenceScore: unverifiedScore.evidenceScore,
5558
+ resolution: reason, scoreKind: unverifiedScore.scoreKind });
5559
+ entry.provenance = summarizeProvenance(entry.siteProvenance);
5275
5560
  };
5276
5561
  // A statically selected base implementation is not the only runtime
5277
5562
  // target in languages with virtual/structural dispatch. If a project
@@ -5353,7 +5638,8 @@ function findCallees(index, definition, options = {}) {
5353
5638
  // already carry stronger identity and must not trigger a whole-file
5354
5639
  // flow build merely because their parser record lacks receiverType.
5355
5640
  const mayNeedDirectReceiverFlow = call =>
5356
- call.isMethod && call.receiver && !call.receiverType &&
5641
+ call.isMethod && call.receiver && (!call.receiverType ||
5642
+ call.receiverTypeGuessed || call.receiverTypeSource === 'guess') &&
5357
5643
  !call.receiverPatternShadow &&
5358
5644
  !_isReservedReceiver(language, call.receiver) &&
5359
5645
  !call.isPathCall && !call.receiverIsModule &&
@@ -5378,6 +5664,14 @@ function findCallees(index, definition, options = {}) {
5378
5664
  for (let call of calls) {
5379
5665
  siteOrdinal++;
5380
5666
  const siteId = siteOrdinal;
5667
+ siteEvidence.set(siteId, {
5668
+ call,
5669
+ evidence: {
5670
+ hasReceiverType: !!call.receiverType,
5671
+ resolvedBySameClass: !!call.receiver && _isReservedReceiver(language, call.receiver),
5672
+ },
5673
+ options: { sameClass: !!call.receiver && _isReservedReceiver(language, call.receiver) },
5674
+ });
5381
5675
  if (language === 'csharp') {
5382
5676
  // fix #353: `Beta.Helper.Widget()` — namespace-qualified type
5383
5677
  // receiver (see the findCallers twin).
@@ -5391,6 +5685,8 @@ function findCallees(index, definition, options = {}) {
5391
5685
  call = {
5392
5686
  ...call,
5393
5687
  receiverType: indexedType.type,
5688
+ receiverTypeSource: 'flow',
5689
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
5394
5690
  ...(indexedType.fromFile && {
5395
5691
  receiverTypeFlowFile: indexedType.fromFile,
5396
5692
  }),
@@ -5417,6 +5713,55 @@ function findCallees(index, definition, options = {}) {
5417
5713
  if (!isDirectMatch && !isNestedCallback) continue;
5418
5714
  if (calleeAccount) calleeAccount.totalSites++;
5419
5715
 
5716
+ if (language === 'python' && call.isMethod && !call.receiverType) {
5717
+ const fixture = _pythonFixtureReceiverType(index, def.file, call);
5718
+ if (fixture) call = { ...call, receiverType: fixture.type,
5719
+ receiverTypeSource: 'fixture', receiverTypeEvidence: { source: 'fixture', ...fixture },
5720
+ ...(fixture.fromFile && { receiverTypeFlowFile: fixture.fromFile }) };
5721
+ const external = !fixture && _pythonExternalFieldFlow(index, def.file, call);
5722
+ if (external) {
5723
+ call = { ...call, receiverTypeSource: 'flow',
5724
+ receiverTypeEvidence: { source: 'flow', externalFactory: external.proof } };
5725
+ const targets = (index.symbols.get(call.name) || []).filter(d => !NON_CALLABLE_TYPES.has(d.type));
5726
+ const facts = _confirmationFacts(index, def.file, call, targets);
5727
+ const provenance = scoreEdge({ possibleDispatch: true, facts }).provenance;
5728
+ noteUnverified(siteId, call, 'possible-dispatch', { dispatchVia: external.via, externalContract: true },
5729
+ { ...occurrenceIdentity(fileEntry.relativePath, call, siteId), provenance });
5730
+ continue;
5731
+ }
5732
+ }
5733
+
5734
+ if (language === 'rust' && _rustStandardWrapperMethod(index, def.file, call, allCalls)) {
5735
+ noteSite(siteId, 'external', 'standard-wrapper-method', call);
5736
+ continue;
5737
+ }
5738
+ if (language === 'rust' && call.isMethod && !call.receiverType &&
5739
+ call.receiverPatternVariant && (collectAccount || call.receiverPatternSourceCallStart != null)) {
5740
+ const item = _rustPatternReceiverType(index, fileEntry, def.file, call, foldCtx());
5741
+ if (item?.type) call = { ...call, receiverType: item.type,
5742
+ receiverTypeSource: 'flow', receiverTypeEvidence: { source: 'flow', ...item },
5743
+ receiverTypeFlowFile: item.fromFile };
5744
+ }
5745
+ if (language === 'rust' && call.isMethod &&
5746
+ call.receiver && !call.receiverType &&
5747
+ (call.receiverIterationCall || call.receiverIterationVariable)) {
5748
+ const item = _rustIterationReceiverType(index, fileEntry, def.file, call, foldCtx());
5749
+ if (item) call = { ...call, receiverType: item.type,
5750
+ receiverTypeSource: 'flow', receiverTypeEvidence: { source: 'flow', ...item },
5751
+ receiverTypeFlowFile: item.fromFile };
5752
+ }
5753
+
5754
+ if (language === 'c' && call.isMethod) {
5755
+ noteUnverified(siteId, call, 'callable-field');
5756
+ continue;
5757
+ }
5758
+
5759
+ if (language === 'csharp' && call.receiverType === 'dynamic') {
5760
+ if (_calleeZeroCandidateName(index, call)) noteSite(siteId, 'external', null, call);
5761
+ else noteUnverified(siteId, call, 'possible-dispatch', { dispatchVia: 'dynamic' });
5762
+ continue;
5763
+ }
5764
+
5420
5765
  if (call.macroParameter) {
5421
5766
  noteSite(siteId, 'excluded', 'macro-parameter', call);
5422
5767
  continue;
@@ -5460,6 +5805,7 @@ function findCallees(index, definition, options = {}) {
5460
5805
  const selected = _calleeOverloadSelect(
5461
5806
  index, call, extensions, language);
5462
5807
  if (selected.match) {
5808
+ if (collectAccount) siteEvidence.get(siteId).evidence.extensionMethod = true;
5463
5809
  const match = selected.match;
5464
5810
  const key = match.bindingId ||
5465
5811
  `${match.file}:${match.startLine}:${call.name}`;
@@ -5530,6 +5876,8 @@ function findCallees(index, definition, options = {}) {
5530
5876
  call = {
5531
5877
  ...call,
5532
5878
  receiverType: indexedType.type,
5879
+ receiverTypeSource: 'flow',
5880
+ receiverTypeEvidence: { source: 'flow', ...indexedType },
5533
5881
  ...(indexedType.fromFile && {
5534
5882
  receiverTypeFlowFile: indexedType.fromFile,
5535
5883
  }),
@@ -5648,6 +5996,22 @@ function findCallees(index, definition, options = {}) {
5648
5996
  }
5649
5997
  }
5650
5998
 
5999
+ if (collectAccount) {
6000
+ const record = siteEvidence.get(siteId);
6001
+ record.call = call;
6002
+ if (directReceiverFlow?.type || fieldHopType) {
6003
+ const source = directReceiverFlow?.type ? 'flow' : 'field';
6004
+ record.evidence.hasReceiverType = true;
6005
+ Object.assign(record.options, {
6006
+ receiverType: directReceiverFlow?.type || fieldHopType,
6007
+ receiverTypeSource: source,
6008
+ originFile: directReceiverFlow?.fromFile || fieldHopInfo?.fromFile,
6009
+ receiverOrigin: { source, ...(directReceiverFlow || fieldHopInfo || {}),
6010
+ field: call.receiverField, rootType: call.receiverRootType },
6011
+ });
6012
+ }
6013
+ }
6014
+
5651
6015
  if (fieldDispatchType) {
5652
6016
  noteUnverified(siteId, call, 'possible-dispatch', {
5653
6017
  dispatchVia: fieldDispatchType,
@@ -5667,6 +6031,7 @@ function findCallees(index, definition, options = {}) {
5667
6031
  const selected = _calleeOverloadSelect(
5668
6032
  index, extensionCall, extensions, language);
5669
6033
  if (selected.match) {
6034
+ if (collectAccount) siteEvidence.get(siteId).evidence.extensionMethod = true;
5670
6035
  const match = selected.match;
5671
6036
  const key = match.bindingId ||
5672
6037
  `${match.file}:${match.startLine}:${call.name}`;
@@ -5810,6 +6175,7 @@ function findCallees(index, definition, options = {}) {
5810
6175
  !_isReservedReceiver(language, call.receiver) &&
5811
6176
  !(localTypes && localTypes.has(call.receiver))) {
5812
6177
  typeQual = _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language);
6178
+ if (typeQual) siteEvidence.get(siteId).evidence.typeQualifiedReceiver = true;
5813
6179
  }
5814
6180
 
5815
6181
  // Package-qualified NON-method records (fix #268, chi-measured):
@@ -5954,7 +6320,7 @@ function findCallees(index, definition, options = {}) {
5954
6320
  }
5955
6321
  continue;
5956
6322
  }
5957
- const typeName = call.receiverType || directReceiverFlow?.type || fieldHopType;
6323
+ const typeName = directReceiverFlow?.type || call.receiverType || fieldHopType;
5958
6324
  const symbols = index.symbols.get(call.name);
5959
6325
  const qualifiedType = language === 'go' && call.receiverType
5960
6326
  ? _goQualifiedReceiverType(index, fileEntry,
@@ -5980,8 +6346,8 @@ function findCallees(index, definition, options = {}) {
5980
6346
  const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
5981
6347
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
5982
6348
  // Same-class overload selection by static call shape (fix #268)
5983
- const receiverOriginFile = call.receiverTypeFlowFile ||
5984
- directReceiverFlow?.fromFile ||
6349
+ const receiverOriginFile = directReceiverFlow?.fromFile ||
6350
+ call.receiverTypeFlowFile ||
5985
6351
  fieldHopInfo?.fromFile ||
5986
6352
  (call.receiverType
5987
6353
  ? _resolveFlowTypeOrigin(
@@ -6103,6 +6469,12 @@ function findCallees(index, definition, options = {}) {
6103
6469
  chained = _nominalChainedReceiverType(index, call, fileEntry, def.file);
6104
6470
  }
6105
6471
  if (chained?.type) {
6472
+ const record = siteEvidence.get(siteId);
6473
+ record.evidence.hasReceiverType = true;
6474
+ Object.assign(record.options, {
6475
+ receiverType: chained.type, receiverTypeSource: 'flow',
6476
+ receiverOrigin: { source: 'flow', ...chained }, originFile: chained.fromFile,
6477
+ });
6106
6478
  const symbols = index.symbols.get(call.name);
6107
6479
  const isCallableCh = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
6108
6480
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
@@ -6849,21 +7221,28 @@ function findCallees(index, definition, options = {}) {
6849
7221
  if (bound && routeVirtualOverride(siteId, call, def.className, bound)) continue;
6850
7222
  }
6851
7223
 
6852
- // Single project-wide owner (fix #236 — the caller side's
6853
- // #204/#209 rule on the callee side): an untyped-receiver method
6854
- // call whose name has exactly ONE owner type resolves to that
6855
- // owner's method — `k.run()` where only Kit defines run. Without
6856
- // it, trace trees stopped expanding at statically-resolvable
6857
- // calls the caller direction confirms.
7224
+ // Receiver evidence for the former single-owner fallback (#355).
7225
+ // A name with one owner still needs an independent typed lookup;
7226
+ // unresolved values remain visible in the contract.
6858
7227
  if (isUncertain && call.isMethod && call.receiver && !bindingResolved) {
6859
7228
  const fm = mayNeedDirectReceiverFlow(call) ? flowMap() : null;
6860
7229
  const flowEntry = fm ? _lookupReturnTypeFlow(fm, call) : undefined;
6861
7230
  const owner = _calleeSingleOwnerMatch(index, def, fileEntry, call, effectiveName, language, flowEntry);
6862
7231
  if (owner) {
7232
+ siteEvidence.get(siteId).evidence.hasSingleOwnerEvidence = true;
6863
7233
  isUncertain = false;
6864
7234
  bindingResolved = owner.bindingId;
6865
7235
  calleeKey = owner.bindingId ||
6866
7236
  `${owner.className || (owner.receiver || '').replace(/^\*/, '')}.${effectiveName}`;
7237
+ } else if (!call.receiverType && !flowEntry?.type && !fieldHopType &&
7238
+ !call.receiverExternalFlow && !call.receiverQualifiedFlow) {
7239
+ const owners = new Set((index.symbols.get(effectiveName) || [])
7240
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type) && (d.className || d.receiver))
7241
+ .map(d => `${d.file}:${d.className || d.receiver}`));
7242
+ if (owners.size === 1) {
7243
+ uncertainReason = 'single-owner';
7244
+ siteEvidence.get(siteId).evidence.hasSingleOwnerEvidence = true;
7245
+ }
6867
7246
  }
6868
7247
  }
6869
7248
 
@@ -6891,6 +7270,59 @@ function findCallees(index, definition, options = {}) {
6891
7270
  }
6892
7271
  }
6893
7272
 
7273
+ // Legacy callers receive no uncertainty band. Drop an untyped
7274
+ // value receiver even if a same-file or unique-name lookup picked
7275
+ // a method; only actual receiver/module/type evidence survives.
7276
+ if (!collectAccount && call.isMethod && !call.receiverType &&
7277
+ !directReceiverFlow?.type && !fieldHopType &&
7278
+ !localTypes?.has(call.receiver) &&
7279
+ !siteEvidence.get(siteId).evidence.resolvedBySameClass &&
7280
+ !siteEvidence.get(siteId).evidence.typeQualifiedReceiver &&
7281
+ !call.receiverIsModule && !call.receiverModuleSpecifier &&
7282
+ !call.receiverModuleComposition && !call.moduleOwnedPath &&
7283
+ !call.isConstructor) continue;
7284
+
7285
+ if (collectAccount) {
7286
+ const record = siteEvidence.get(siteId);
7287
+ record.call = call;
7288
+ if (bindingResolved && !record.evidence.hasSingleOwnerEvidence) {
7289
+ record.evidence.hasBindingId = true;
7290
+ record.options.bindingId = bindingResolved;
7291
+ }
7292
+ }
7293
+ if (collectAccount) {
7294
+ const record = siteEvidence.get(siteId);
7295
+ const valueReceiver = call.isMethod && !call.isConstructor &&
7296
+ !call.receiverIsModule && !call.receiverModuleSpecifier &&
7297
+ !call.receiverModuleComposition && !call.moduleOwnedPath &&
7298
+ !record.evidence.typeQualifiedReceiver && !record.evidence.extensionMethod &&
7299
+ !record.evidence.resolvedBySameClass;
7300
+ if (valueReceiver) {
7301
+ // Resolve and classify THIS occurrence before its grouping
7302
+ // key is chosen. A sibling site cannot lend its receiver
7303
+ // evidence or overload identity to this one.
7304
+ const proof = provenanceForSite(siteId, null);
7305
+ const facts = proof.provenance.facts;
7306
+ const checked = validateConfirmation(proof.provenance, facts.targets);
7307
+ const selected = checked.verdict === 'establishes-target' &&
7308
+ (index.symbols.get(effectiveName) || []).find(candidate =>
7309
+ sameDeclaration(declarationIdentity(candidate), facts.lookup?.selected));
7310
+ const reportOnly = checked.verdict === 'unsupported' && record.evidence.hasReceiverType;
7311
+ if (!selected && !reportOnly) {
7312
+ const reason = !record.evidence.hasReceiverType && facts.ownerCount === 1
7313
+ ? 'single-owner' : 'provenance-incomplete';
7314
+ if (reason === 'single-owner') proof.provenance = scoreEdge({
7315
+ hasSingleOwnerEvidence: true, facts,
7316
+ }).provenance;
7317
+ noteUnverified(siteId, call, reason, {}, proof);
7318
+ continue;
7319
+ }
7320
+ if (selected) {
7321
+ bindingResolved = selected.bindingId;
7322
+ calleeKey = selected.bindingId || `${selected.file}:${selected.startLine}:${selected.name}`;
7323
+ }
7324
+ }
7325
+ }
6894
7326
  const existing = callees.get(calleeKey);
6895
7327
  if (existing) {
6896
7328
  existing.count += 1;
@@ -7254,24 +7686,61 @@ function findCallees(index, definition, options = {}) {
7254
7686
  }
7255
7687
  }
7256
7688
 
7257
- const calleeScored = scoreEdge({
7689
+ let calleeScored = scoreEdge({
7258
7690
  hasBindingId: !!bindingId,
7259
7691
  hasImportEvidence: !!bindingId || resolutionSymbols.length === 1 ||
7260
7692
  (callee.file === def.file) || callerImportSet.has(callee.file),
7261
7693
  isUncertain: false, // uncertain callees already filtered above
7262
7694
  });
7695
+ const siteProvenance = collectAccount ? siteIds.map(siteId => ({
7696
+ ...provenanceForSite(siteId, callee), tier: TIER.CONFIRMED,
7697
+ })).sort((a, b) => a.siteId - b.siteId).filter(site => {
7698
+ const record = siteEvidence.get(site.siteId);
7699
+ const valueReceiver = record.call.isMethod &&
7700
+ !record.call.receiverIsModule && !record.call.receiverModuleSpecifier &&
7701
+ !record.call.receiverModuleComposition && !record.evidence.typeQualifiedReceiver;
7702
+ const untypedValue = valueReceiver && !record.evidence.hasReceiverType &&
7703
+ !record.evidence.resolvedBySameClass;
7704
+ if (untypedValue && site.provenance.facts.ownerCount === 1) {
7705
+ record.evidence.hasSingleOwnerEvidence = true;
7706
+ }
7707
+ const migrated = valueReceiver && !record.call.isConstructor &&
7708
+ !record.evidence.resolvedBySameClass && !record.evidence.extensionMethod &&
7709
+ (record.evidence.hasReceiverType || record.evidence.hasSingleOwnerEvidence || untypedValue);
7710
+ if (!migrated) return true;
7711
+ const checked = validateConfirmation(site.provenance, [declarationIdentity(callee)]);
7712
+ if (checked.verdict === 'establishes-target' ||
7713
+ (checked.verdict === 'unsupported' && record.evidence.hasReceiverType)) return true;
7714
+ if (checked.verdict === 'establishes-other') {
7715
+ noteSite(site.siteId, 'excluded', 'receiver-target-different', record.call);
7716
+ (calleeAccount.excluded.evidence ||= []).push(site);
7717
+ } else {
7718
+ const reason = record.evidence.hasSingleOwnerEvidence && !record.evidence.hasReceiverType
7719
+ ? 'single-owner' : 'provenance-incomplete';
7720
+ if (reason === 'single-owner') site.provenance = scoreEdge({
7721
+ hasSingleOwnerEvidence: true, facts: site.provenance.facts,
7722
+ }).provenance;
7723
+ noteUnverified(site.siteId, record.call, reason, {}, site);
7724
+ }
7725
+ return false;
7726
+ }) : null;
7727
+ if (collectAccount && siteProvenance.length === 0) continue;
7728
+ if (collectAccount) calleeScored = siteProvenance.reduce((weakest, site) =>
7729
+ site.evidenceScore < weakest.evidenceScore ? site : weakest);
7263
7730
  claimSites('confirmed', null);
7264
7731
  result.push({
7265
7732
  ...callee,
7266
- callCount: count,
7267
- weight: index.calculateWeight(count),
7733
+ callCount: collectAccount ? siteProvenance.length : count,
7734
+ weight: index.calculateWeight(collectAccount ? siteProvenance.length : count),
7268
7735
  confidence: calleeScored.confidence,
7269
7736
  evidenceScore: calleeScored.evidenceScore,
7270
7737
  scoreKind: calleeScored.scoreKind,
7271
7738
  resolution: calleeScored.resolution,
7272
7739
  ...(collectAccount && {
7273
7740
  tier: TIER.CONFIRMED,
7274
- sites: [...sites].sort((a, b) => a - b),
7741
+ provenance: summarizeProvenance(siteProvenance),
7742
+ siteProvenance,
7743
+ sites: siteProvenance.map(site => site.line).sort((a, b) => a - b),
7275
7744
  ...(isFunctionReference && { functionReference: true }),
7276
7745
  }),
7277
7746
  });
@@ -7369,6 +7838,28 @@ function getInstanceAttributeTypes(index, filePath, className) {
7369
7838
  'python', { filePath, consumerAwaited: false });
7370
7839
  return result?.fromFile ? result.type : null;
7371
7840
  },
7841
+ resolveImportedCallType(name) {
7842
+ const bindings = (fileEntry.importBindings || []).filter(binding =>
7843
+ (binding.alias || binding.name) === name);
7844
+ if (bindings.length !== 1 || (fileEntry.moduleAssignedNames || []).includes(name) ||
7845
+ (index.symbols.get(name) || []).some(d => d.file === filePath)) return null;
7846
+ const binding = bindings[0];
7847
+ if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) return null;
7848
+ const type = langModule.getBuiltinCallReturnType?.(binding.module, binding.name);
7849
+ return type ? { type, binding: { ...binding }, externalModule: binding.module } : null;
7850
+ },
7851
+ resolveExternalFactory(parts) {
7852
+ const name = parts[0];
7853
+ const bindings = (fileEntry.importBindings || []).filter(binding => (binding.alias || binding.name) === name);
7854
+ if (bindings.length !== 1 || (fileEntry.moduleAssignedNames || []).includes(name) ||
7855
+ (index.symbols.get(name) || []).some(d => d.file === filePath)) return null;
7856
+ const binding = bindings[0];
7857
+ if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) return null;
7858
+ const importedFunction = ['from', 'relative'].includes(binding.kind);
7859
+ if (parts.length !== (importedFunction ? 1 : 2)) return null;
7860
+ return { module: binding.module, producer: importedFunction ? binding.name : parts[1],
7861
+ binding: { ...binding }, projectDeclarations: [], projectBindings: [], resolvedModule: null };
7862
+ },
7372
7863
  });
7373
7864
  index._attrTypeCache.set(filePath, fileCache);
7374
7865
  } catch {
@@ -7616,6 +8107,30 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7616
8107
  candidate.callStart === call.receiverCallStart &&
7617
8108
  candidate.callEnd === call.receiverCallEnd);
7618
8109
 
8110
+ // A separate unwrap/expect assignment consumes the payload of the
8111
+ // receiver's declared standard wrapper. Its spelling alone is not a
8112
+ // contract: project-defined Result/Option classes remain ordinary
8113
+ // method producers. Reassignments are checked by the same nearest-
8114
+ // assignment lookup used for every other flow receiver.
8115
+ if (language === 'rust' && call.isMethod && call.receiver &&
8116
+ !call.receiverPatternShadow && !call.receiverFlowInvalidated &&
8117
+ ((call.name === 'unwrap' && call.argCount === 0) ||
8118
+ (call.name === 'expect' && call.argCount === 1))) {
8119
+ const receiverFlow = _lookupReturnTypeFlow(map, call);
8120
+ const contract = receiverFlow?.rustWrapper;
8121
+ if (contract?.type && validateRustWrapperContract(contract)) {
8122
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
8123
+ const key = `${scope}:${call.assignedTo}`;
8124
+ if (!map.has(key)) map.set(key, []);
8125
+ map.get(key).push({ line: call.line, start: call.callStart,
8126
+ type: contract.type, fromFile: contract.fromFile,
8127
+ wrapperUnwrap: { method: call.name,
8128
+ producer: { line: receiverFlow.line, start: receiverFlow.start,
8129
+ type: receiverFlow.type, fromFile: receiverFlow.fromFile }, contract } });
8130
+ continue;
8131
+ }
8132
+ }
8133
+
7619
8134
  // Rust's `collect::<Vec<Item>>()` turbofish fixes the concrete result
7620
8135
  // and its item type at the call site. Preserve the item identity even
7621
8136
  // though the outer collection is a standard-library type with no
@@ -7692,7 +8207,7 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7692
8207
  ? resolved[0]
7693
8208
  : null;
7694
8209
  }
7695
- if (folded?.type || folded?.externalVia) {
8210
+ if (folded?.type || folded?.externalVia || folded?.rustWrapper) {
7696
8211
  const scope = call.enclosingFunction
7697
8212
  ? `${call.enclosingFunction.startLine}` : '';
7698
8213
  const key = `${scope}:${call.assignedTo}`;
@@ -7701,12 +8216,15 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7701
8216
  map.get(key).push({ line: call.line, start: call.callStart,
7702
8217
  ...(folded.type && { type: folded.type }),
7703
8218
  ...(folded.fromFile && { fromFile: folded.fromFile }),
8219
+ ...(folded.rustWrapper && { rustWrapper: folded.rustWrapper }),
8220
+ ...(folded.rustReturnDeclaration && { rustReturnDeclaration: folded.rustReturnDeclaration }),
8221
+ ...(folded.moduleProducer && { moduleProducer: folded.moduleProducer }),
7704
8222
  ...(folded.externalVia && { externalVia: folded.externalVia }),
7705
8223
  ...(folded.externalConcrete && { externalConcrete: true }) });
7706
8224
  continue;
7707
8225
  }
7708
8226
  }
7709
- let returnType, fromFile, selfClass, returnedFunctionResult, returnDefinition;
8227
+ let returnType, fromFile, selfClass, returnedFunctionResult, returnDefinition, moduleProducer;
7710
8228
  const builtinCallReturn = !nominal && language === 'python'
7711
8229
  ? _pythonBuiltinCallReturnType(index, fileEntry, call) : null;
7712
8230
  if (call.localValueCall && call.returnTypeHint) {
@@ -7732,10 +8250,14 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7732
8250
  returnType = callableFlow.returnedFunctionResult;
7733
8251
  fromFile = callableFlow.fromFile;
7734
8252
  } else if (call.isMethod && call.receiverType &&
7735
- !call.receiverTypeGuessed) {
8253
+ !call.receiverTypeGuessed && call.receiverTypeSource !== 'guess') {
7736
8254
  const defs = index.symbols.get(call.name) || [];
7737
8255
  if (nominal) {
7738
- const matches = defs.filter(d => d.className === call.receiverType && d.returnType);
8256
+ const rustOwner = language === 'rust'
8257
+ ? _rustFlowReceiverOrigin(index, filePath, call.receiverType, call.receiverTypeQualifier)
8258
+ : null;
8259
+ const matches = defs.filter(d => d.className === call.receiverType && d.returnType &&
8260
+ (language !== 'rust' || rustOwner?.fromFile));
7739
8261
  if (matches.length > 0 && new Set(matches.map(d => d.returnType)).size === 1) {
7740
8262
  returnType = matches[0].returnType;
7741
8263
  fromFile = matches[0].file;
@@ -7846,6 +8368,15 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7846
8368
  cls = next;
7847
8369
  }
7848
8370
  } else if (nominal && call.isMethod && call.isPathCall && call.receiver) {
8371
+ if (language === 'rust') {
8372
+ const producer = _rustModuleProducer(index, fileEntry, filePath, call);
8373
+ if (producer) {
8374
+ returnDefinition = producer.definition;
8375
+ returnType = returnDefinition.returnType;
8376
+ fromFile = returnDefinition.file;
8377
+ moduleProducer = producer.facts;
8378
+ }
8379
+ }
7849
8380
  if (language === 'cpp') {
7850
8381
  // C++ value-initialization through a qualified type name:
7851
8382
  // `auto p = fmt::pipe()`. The portable tree-sitter AST uses
@@ -8327,6 +8858,18 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
8327
8858
  }
8328
8859
  const origin = _resolveFlowTypeOrigin(index, fromFile || filePath, parsed.name, parsed.qualifier);
8329
8860
  if (!origin) {
8861
+ const wrapper = language === 'rust' && !call.assignedUnwrap &&
8862
+ _rustReturnWrapper(index, returnType, fromFile || filePath, selfClass,
8863
+ returnDefinition, [], true);
8864
+ if (wrapper && validateRustWrapperContract(wrapper, true)) {
8865
+ const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
8866
+ const key = `${scope}:${call.assignedTo}`;
8867
+ if (!map) map = new Map();
8868
+ if (!map.has(key)) map.set(key, []);
8869
+ map.get(key).push({ line: call.line, start: call.callStart,
8870
+ rustWrapper: wrapper, rustReturnDeclaration: _rustReturnDeclaration(returnDefinition) });
8871
+ continue;
8872
+ }
8330
8873
  // A project producer can return a package-qualified external
8331
8874
  // Go type (`DefaultLogger(...) http.Handler`). That still
8332
8875
  // gives compiler-grade provenance: the assigned receiver is
@@ -8373,6 +8916,12 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
8373
8916
  if (!map.has(key)) map.set(key, []);
8374
8917
  map.get(key).push({ line: call.line, start: call.callStart, type: typeName,
8375
8918
  ...(entryFromFile && { fromFile: entryFromFile }),
8919
+ ...(moduleProducer && { moduleProducer }),
8920
+ ...(language === 'rust' && returnDefinition && { rustReturnDeclaration: _rustReturnDeclaration(returnDefinition) }),
8921
+ ...(language === 'rust' && !call.assignedUnwrap && {
8922
+ rustWrapper: _rustReturnWrapper(index, returnType, fromFile || filePath,
8923
+ selfClass, returnDefinition),
8924
+ }),
8376
8925
  ...(iteratorItemType && {
8377
8926
  iteratorItemType,
8378
8927
  iteratorItemFromFile,
@@ -8480,7 +9029,15 @@ function _lookupReturnTypeFlow(map, call) {
8480
9029
  for (const e of entries) {
8481
9030
  if (precedesCall(e) && laterThan(e, best)) best = e;
8482
9031
  }
8483
- if (best) return best.invalidated ? undefined : best;
9032
+ if (best) {
9033
+ const binding = call.receiverTypeEvidence?.aliasBinding;
9034
+ // A non-call copy/rebinding is absent from the producer map.
9035
+ // Its newer AST declaration must still invalidate an older
9036
+ // factory payload with the same local variable name.
9037
+ if (binding?.target === call.receiver && Number.isInteger(binding.assignment?.start) &&
9038
+ (best.start == null || binding.assignment.start > best.start)) return undefined;
9039
+ return best.invalidated ? undefined : best;
9040
+ }
8484
9041
  }
8485
9042
  return undefined;
8486
9043
  }
@@ -8555,6 +9112,59 @@ function _splitTopLevelGenericArgs(s) {
8555
9112
  return out;
8556
9113
  }
8557
9114
 
9115
+ function _rustReturnWrapper(index, text, file, selfClass, producer, projection = [], allowUnknownPayload = false) {
9116
+ if (!text || !file || !producer) return null;
9117
+ const contract = rustWrapperContract(index, text, file, {
9118
+ projection,
9119
+ allowUnknownPayload,
9120
+ parseType: value => _returnTypeNameNominal(value, 'rust', { selfClass }),
9121
+ resolveType: (context, name, qualifier) => _resolveFlowTypeOrigin(index, context, name, qualifier),
9122
+ });
9123
+ return contract && { ...contract, producer: { ...declarationIdentity(producer), returnType: producer.returnType } };
9124
+ }
9125
+
9126
+ function _rustReturnDeclaration(definition) {
9127
+ return { ...declarationIdentity(definition), file: definition.file,
9128
+ relativePath: definition.relativePath, returnType: definition.returnType };
9129
+ }
9130
+
9131
+ function _rustStandardWrapperMethod(index, file, call, calls) {
9132
+ if (!call.isMethod || !call.receiver || call.isPathCall ||
9133
+ call.receiverPatternShadow || call.receiverFlowInvalidated ||
9134
+ !['unwrap', 'expect'].includes(call.name)) return null;
9135
+ const flow = _lookupReturnTypeFlow(_buildReturnTypeFlowMap(index, file, calls), call);
9136
+ const contract = flow?.rustWrapper;
9137
+ if (contract && validateRustWrapperContract(contract, true)) {
9138
+ return { method: call.name, receiver: call.receiver, callKind: 'method',
9139
+ producer: { line: flow.line, start: flow.start }, contract };
9140
+ }
9141
+ // Ownership of a standard wrapper does not require knowing its payload:
9142
+ // `value: Result<T, E>` still uses Result's inherent unwrap. This negative
9143
+ // proof never types T or attributes its later methods to a project class.
9144
+ const type = call.receiverType;
9145
+ if (!['Result', 'Option'].includes(type) || call.receiverTypeSource !== 'annotation' ||
9146
+ call.receiverTypeEvidence?.source !== 'annotation' ||
9147
+ _isGenericParamReceiverType(index, file, call.line, type)) return null;
9148
+ const entry = index.files.get(file);
9149
+ const module = type === 'Result' ? 'result' : 'option';
9150
+ const standardPaths = [`std::${module}::${type}`, `core::${module}::${type}`];
9151
+ const qualifier = call.receiverTypeQualifier;
9152
+ const local = (index.symbols.get(type) || []).filter(d => d.file === file &&
9153
+ (IDENTITY_TYPE_KINDS.has(d.type) || d.type === 'type'));
9154
+ const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === type);
9155
+ if (qualifier ? !standardPaths.includes(`${qualifier}::${type}`)
9156
+ : local.length || bindings.some(b => !standardPaths.includes(b.module))) return null;
9157
+ if ((entry.importBindings || []).some(b => b.name === '*' || b.module?.endsWith('::*'))) return null;
9158
+ const root = qualifier?.split('::')[0] || bindings[0]?.module.split('::')[0];
9159
+ if (root && ((index.symbols.get(root) || []).some(d => d.file === file) ||
9160
+ (entry.importBindings || []).some(b => (b.alias || b.name) === root))) return null;
9161
+ return { method: call.name, receiver: call.receiver, callKind: 'method',
9162
+ annotationReceiver: { type, origin: call.receiverTypeEvidence,
9163
+ qualifier: qualifier || null, localDeclarations: local.map(declarationIdentity),
9164
+ bindings, wildcardImports: [], rootDeclarations: [], rootBindings: [],
9165
+ genericParameter: false } };
9166
+ }
9167
+
8558
9168
  function _splitTopLevelDelimiter(s, delimiter) {
8559
9169
  const out = [];
8560
9170
  let angle = 0, paren = 0, bracket = 0, brace = 0, cur = '';
@@ -8659,10 +9269,11 @@ function _returnTypeNameNominal(text, language, opts = {}) {
8659
9269
  if (language === 'go') {
8660
9270
  const qm = t.replace(/^\*+/, '').match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
8661
9271
  if (qm) qualifier = qm[1];
8662
- } else if (language === 'cpp') {
8663
- const stripped = t
8664
- .replace(/\b(const|volatile|class|struct|typename)\b/g, '')
8665
- .replace(/[*&]+/g, '').trim();
9272
+ } else if (language === 'cpp' || language === 'rust') {
9273
+ const stripped = language === 'rust'
9274
+ ? t.replace(/^&(?:\s*'\w+)?\s*/, '').replace(/^mut\s+/, '').trim()
9275
+ : t.replace(/\b(const|volatile|class|struct|typename)\b/g, '')
9276
+ .replace(/[*&]+/g, '').trim();
8666
9277
  const genericStart = stripped.indexOf('<');
8667
9278
  const head = genericStart >= 0
8668
9279
  ? stripped.slice(0, genericStart).trim() : stripped;
@@ -8807,6 +9418,35 @@ function _rustImportedTypeIdentity(index, filePath, localName) {
8807
9418
  * trusted (a use/import of an external type can shadow it invisibly)
8808
9419
  * - no project type def at all: external name — safe, can't conflate
8809
9420
  */
9421
+ function _rustFlowReceiverOrigin(index, file, name, qualifier) {
9422
+ const entry = index.files.get(file);
9423
+ // An annotation names a type in this module's scope. A same-directory
9424
+ // project Result/Option declaration does not shadow the standard prelude
9425
+ // of another module, nor does an unrelated imported file bind its names.
9426
+ const local = (index.symbols.get(name) || []).some(d => d.file === file &&
9427
+ (IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf)));
9428
+ const bound = (entry?.importBindings || []).some(b => (b.alias || b.name) === name);
9429
+ if (!qualifier && !local && !bound) return null;
9430
+ return _resolveFlowTypeOrigin(index, file, name, qualifier);
9431
+ }
9432
+
9433
+ function _rustSameNameAliasOrigin(index, definition, seen = new Set()) {
9434
+ if (!definition || seen.has(definition) || seen.size >= 8) return null;
9435
+ seen.add(definition);
9436
+ if (definition.type !== 'type') return { fromFile: definition.file };
9437
+ if (!definition.aliasTypeText) return null;
9438
+ const parsed = _returnTypeNameNominal(definition.aliasTypeText, 'rust');
9439
+ if (parsed?.name !== definition.name || !parsed.qualifier) return null;
9440
+ const relative = index.files.get(definition.file)?.moduleResolved?.[parsed.qualifier];
9441
+ const resolved = relative ? path.resolve(index.root, relative)
9442
+ : resolveRustImport(`${parsed.qualifier}::${parsed.name}`, definition.file, index.root) ||
9443
+ resolveRustImport(parsed.qualifier, definition.file, index.root);
9444
+ if (!resolved) return null;
9445
+ const targets = (index.symbols.get(parsed.name) || []).filter(d =>
9446
+ IDENTITY_TYPE_KINDS.has(d.type) && d.file === resolved);
9447
+ return targets.length === 1 ? _rustSameNameAliasOrigin(index, targets[0], seen) : null;
9448
+ }
9449
+
8810
9450
  function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier = undefined) {
8811
9451
  const opCache = index._opFlowTypeOriginCache;
8812
9452
  const cacheKey = `${producerFile}\x00${typeName}\x00${qualifier || ''}`;
@@ -8863,6 +9503,13 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier = undef
8863
9503
  const resolved = resolveRustImport(
8864
9504
  `${qualifier}::${typeName}`, producerFile, index.root);
8865
9505
  if (resolved) {
9506
+ const local = typeDefs.filter(d => d.file === resolved);
9507
+ if (local.length === 1) {
9508
+ const direct = local[0].type === 'type' && local[0].aliasOf === typeName
9509
+ ? _rustSameNameAliasOrigin(index, local[0])
9510
+ : { fromFile: local[0].file };
9511
+ if (direct) return finish(direct);
9512
+ }
8866
9513
  const reachable = typeDefs.filter(d =>
8867
9514
  d.file === resolved ||
8868
9515
  _importReaches(index, resolved, new Set([d.file])));
@@ -9052,22 +9699,7 @@ const JS_GLOBAL_RECEIVERS = new Set([
9052
9699
  'crypto', 'performance', 'history', 'location', 'screen',
9053
9700
  ]);
9054
9701
 
9055
- const BUILTIN_RECEIVER_TYPES = new Set([
9056
- 'dict', 'list', 'set', 'tuple', 'str', 'int', 'float', 'bool', 'bytes', 'frozenset',
9057
- 'Mapping', 'MutableMapping', 'Sequence', 'MutableSequence',
9058
- 'Collection', 'Iterable', 'Iterator', 'KeysView', 'ValuesView', 'ItemsView',
9059
- 'IO', 'TextIO', 'BinaryIO', 'StringIO', 'BytesIO',
9060
- 'ZlibCompress', 'ZlibDecompress',
9061
- 'AsyncEvent',
9062
- 'Generator', 'AsyncGenerator', 'ContextManager', 'AsyncContextManager',
9063
- 'Array', 'String', 'Object', 'RegExp', 'Number', 'Boolean', 'Map', 'Set', 'Promise',
9064
- 'WeakMap', 'WeakSet',
9065
- 'string', 'number', 'boolean', 'bigint', 'symbol',
9066
- 'object', 'dynamic', 'decimal', 'byte', 'sbyte', 'char',
9067
- 'short', 'ushort', 'uint', 'long', 'ulong', 'double',
9068
- 'List', 'Dictionary', 'HashSet', 'Queue', 'Stack',
9069
- 'Task', 'ValueTask', 'IEnumerable', 'ICollection', 'IList',
9070
- ]);
9702
+
9071
9703
 
9072
9704
  // Universal-contract method names (fix #265, hono-measured: 183 untyped
9073
9705
  // `x.toString()` calls confirmed against JSXNode.toString via the single-
@@ -10959,6 +11591,60 @@ function _pythonBuiltinContractAllowed(index, fileEntry, moduleName) {
10959
11591
  return !_unresolvedModuleIsGap(index, module);
10960
11592
  }
10961
11593
 
11594
+ function _pythonExternalFieldFlow(index, file, call) {
11595
+ const field = call.receiverRoot === 'self' && (!call.receiverFields || call.receiverFields.length === 1)
11596
+ ? call.receiverField : call.receiver?.startsWith('self.') && call.receiver.split('.').length === 2
11597
+ ? call.receiver.slice(5) : null;
11598
+ if (!field || call.receiverFlowInvalidated) return null;
11599
+ const enclosing = index.findEnclosingFunction(file, call.line, true);
11600
+ if (!enclosing?.className) return null;
11601
+ const owner = (index.symbols.get(enclosing.className) || []).filter(d => d.file === file && d.type === 'class');
11602
+ if (owner.length !== 1) return null;
11603
+ const flow = getInstanceAttributeTypes(index, file, enclosing.className)?.externalFlows?.get(field);
11604
+ if (!flow) return null;
11605
+ const producer = flow.assignments[0];
11606
+ return { via: `${producer.module}.${producer.producer}${'()'.repeat(producer.depth)}`,
11607
+ proof: { ...flow, owner: declarationIdentity(owner[0]), enclosing: declarationIdentity(enclosing) } };
11608
+ }
11609
+
11610
+ function _pythonFixtureReceiverType(index, file, call) {
11611
+ const resolveType = (context, name, qualifier) => {
11612
+ const root = qualifier || name, entry = index.files.get(context);
11613
+ if ((entry.moduleAssignedNames || []).includes(root) ||
11614
+ (entry.importBindings || []).filter(b => (b.alias || b.name) === root).length > 1 ||
11615
+ (qualifier && (index.symbols.get(root) || []).some(d => d.file === context))) return null;
11616
+ return pythonFixtureType(index, context, qualifier ? `${qualifier}.${name}` : name,
11617
+ d => IDENTITY_TYPE_KINDS.has(d.type));
11618
+ };
11619
+ return pythonFixtureReceiver(index, file, call, {
11620
+ resolveType,
11621
+ externalModule: (context, module) => _pythonBuiltinContractAllowed(index, index.files.get(context), module),
11622
+ field(type, context, field) {
11623
+ const owner = resolveType(context, type)?.declaration;
11624
+ if (!owner) return null;
11625
+ const members = (index.symbols.get(field) || []).filter(d => d.className === type && d.file === owner.file &&
11626
+ ['property', 'getter', 'field'].includes(d.type));
11627
+ if (members.length !== 1) return null;
11628
+ const member = members[0], text = member.returnType || member.fieldType;
11629
+ const result = _structuralTypeExpression(text);
11630
+ if (!result || result.args.length) return null;
11631
+ const qualifier = result.qualifiedHead.includes('.')
11632
+ ? result.qualifiedHead.slice(0, result.qualifiedHead.lastIndexOf('.')) : undefined;
11633
+ const next = resolveType(member.file, result.head, qualifier);
11634
+ const builtin = !qualifier && !next && isProvenanceBuiltinReceiver(result.head, 'python') &&
11635
+ !(index.files.get(member.file).importBindings || []).some(b => (b.alias || b.name) === result.head) &&
11636
+ !(index.files.get(member.file).moduleAssignedNames || []).includes(result.head) &&
11637
+ !(index.symbols.get(result.head) || []).some(d => d.file === member.file);
11638
+ if (!next && !builtin) return null;
11639
+ return { type: result.head, fromFile: next?.declaration.file,
11640
+ fact: { owner: declarationIdentity(owner), member: declarationIdentity(member), annotation: text,
11641
+ result: next ? declarationIdentity(next.declaration) : { builtin: result.head, language: 'python' },
11642
+ ...(builtin && { builtinShadowDeclarations: [], builtinShadowBindings: [] }),
11643
+ importChain: next?.chain || [] } };
11644
+ },
11645
+ });
11646
+ }
11647
+
10962
11648
  function _structuralImportedReceiverType(index, fileEntry, receiver) {
10963
11649
  const bindings = (fileEntry.importBindings || []).filter(binding =>
10964
11650
  binding.name === receiver || binding.alias === receiver);
@@ -10981,7 +11667,7 @@ function _structuralImportedReceiverType(index, fileEntry, receiver) {
10981
11667
  return { type: receiver, fromFile: definition.file };
10982
11668
  }
10983
11669
 
10984
- function _pythonBuiltinCallReturnType(index, fileEntry, call) {
11670
+ function _pythonBuiltinCallReturnType(index, fileEntry, call, evidence = null) {
10985
11671
  if (fileEntry?.language !== 'python') return null;
10986
11672
  const adapter = getLanguageAdapter('python');
10987
11673
  if (typeof adapter?.getBuiltinCallReturnType !== 'function') return null;
@@ -10998,7 +11684,10 @@ function _pythonBuiltinCallReturnType(index, fileEntry, call) {
10998
11684
  for (const binding of bindings) {
10999
11685
  if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) continue;
11000
11686
  const type = adapter.getBuiltinCallReturnType(binding.module, call.name);
11001
- if (type) types.add(type);
11687
+ if (type) {
11688
+ types.add(type);
11689
+ if (evidence) (evidence.bindings ||= []).push({ ...binding, returnType: type });
11690
+ }
11002
11691
  }
11003
11692
  return types.size === 1 ? [...types][0] : null;
11004
11693
  }
@@ -11010,21 +11699,30 @@ function _pythonBuiltinFieldPathType(index, fileEntry, root, fields) {
11010
11699
  const bindings = (fileEntry.importBindings || []).filter(binding =>
11011
11700
  binding.name === root || binding.alias === root);
11012
11701
  const types = new Set();
11702
+ const contracts = [];
11013
11703
  for (const binding of bindings) {
11014
11704
  if (!_pythonBuiltinContractAllowed(index, fileEntry, binding.module)) continue;
11015
11705
  const type = adapter.getBuiltinFieldType(binding.module, fields[0]);
11016
- if (type) types.add(type);
11706
+ if (type) {
11707
+ types.add(type);
11708
+ contracts.push({ ...binding, field: fields[0], type });
11709
+ }
11017
11710
  }
11018
- return types.size === 1 ? [...types][0] : null;
11711
+ return types.size === 1 ? { type: [...types][0], root, fields, bindings: contracts } : null;
11019
11712
  }
11020
11713
 
11021
11714
  function _pythonBuiltinChainedReceiverType(index, fileEntry, call, foldCtx) {
11022
11715
  if (fileEntry?.language !== 'python') return null;
11023
11716
  const producers = _chainedProducerRecords(foldCtx, call);
11024
11717
  if (producers.length === 0) return null;
11025
- const types = producers.map(producer =>
11026
- _pythonBuiltinCallReturnType(index, fileEntry, producer));
11027
- return types.every(Boolean) && new Set(types).size === 1 ? types[0] : null;
11718
+ const witnesses = producers.map(producer => {
11719
+ const evidence = { name: producer.name, line: producer.line, site: producer.callSite };
11720
+ const type = _pythonBuiltinCallReturnType(index, fileEntry, producer, evidence);
11721
+ return { type, ...evidence };
11722
+ });
11723
+ const types = witnesses.map(witness => witness.type);
11724
+ return types.every(Boolean) && new Set(types).size === 1
11725
+ ? { type: types[0], producers: witnesses } : null;
11028
11726
  }
11029
11727
 
11030
11728
  /**
@@ -11062,12 +11760,22 @@ function _calleeStructuralModuleRoute(index, fileEntry, call, language) {
11062
11760
  }
11063
11761
 
11064
11762
  function _calleeStructuralImportedNameRoute(index, fileEntry, call, language) {
11065
- const lookupName = call.resolvedName || call.name;
11763
+ let lookupName = call.resolvedName || call.name;
11066
11764
  let bindings = (fileEntry?.importBindings || []).filter(b => b.name === lookupName);
11067
11765
  if (call.resolvedName && bindings.some(b => b.alias)) {
11068
11766
  const paired = bindings.filter(b => b.alias === call.name);
11069
11767
  if (paired.length > 0) bindings = paired;
11070
11768
  }
11769
+ // fix #357: a call spelled by a rename's LOCAL alias (Rust `use m::a as
11770
+ // b; b()`) pairs with exactly the binding carrying that alias, and the
11771
+ // exported item is the binding's ORIGINAL name.
11772
+ if (bindings.length === 0) {
11773
+ const aliased = (fileEntry?.importBindings || []).filter(b => b.alias === call.name);
11774
+ if (aliased.length > 0 && new Set(aliased.map(b => b.name)).size === 1) {
11775
+ bindings = aliased;
11776
+ lookupName = aliased[0].name;
11777
+ }
11778
+ }
11071
11779
  if (bindings.length === 0) return null;
11072
11780
  return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, lookupName, true);
11073
11781
  }
@@ -11127,6 +11835,7 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
11127
11835
  let unknown = false;
11128
11836
  let frontier = [[startAbs, exposedName]];
11129
11837
  const shapeMatches = d =>
11838
+ (language !== 'rust' || !(d.className || d.receiver)) &&
11130
11839
  (!NON_CALLABLE_TYPES.has(d.type) ||
11131
11840
  (call.isConstructor && d.type === 'class') ||
11132
11841
  (langTraits(language)?.classesCallableWithoutNew && d.type === 'class')) &&
@@ -11264,6 +11973,19 @@ function _calleeGoPackageMatch(index, call, importModule) {
11264
11973
  return bestMatch;
11265
11974
  }
11266
11975
 
11976
+ function selectProvenanceOverload(index, call, candidates, language) {
11977
+ const remaining = new Set(candidates);
11978
+ const slots = [];
11979
+ for (const definition of candidates) {
11980
+ if (!remaining.has(definition)) continue;
11981
+ const group = _closeCallableIdentityGroup(index, [definition], candidates);
11982
+ const representative = group.find(d => !d.isSignature && !d.isDeclaration) || definition;
11983
+ slots.push(representative);
11984
+ for (const member of group) remaining.delete(member);
11985
+ }
11986
+ return _calleeOverloadSelect(index, call, slots, language);
11987
+ }
11988
+
11267
11989
  function _calleeOverloadSelect(index, call, matches, language) {
11268
11990
  if (matches.length === 0) return { match: null };
11269
11991
  if (call.argCount == null || call.argSpread) {
@@ -11580,6 +12302,7 @@ function _declaredFieldType(
11580
12302
  className: rootType,
11581
12303
  fieldType,
11582
12304
  file: owner.file,
12305
+ ...(attrs.origins?.get(fieldName) && { fieldOrigin: attrs.origins.get(fieldName) }),
11583
12306
  });
11584
12307
  }
11585
12308
  if (complete && inferred.length > 0) {
@@ -11642,6 +12365,7 @@ function _declaredFieldType(
11642
12365
  const aliasBase = _pureAliasBase(index, typeName);
11643
12366
  if (aliasBase) typeName = aliasBase;
11644
12367
  if (info) {
12368
+ if (onType.length === 1 && onType[0].fieldOrigin) Object.assign(info, onType[0].fieldOrigin);
11645
12369
  const origins = new Set();
11646
12370
  const namespaces = new Set();
11647
12371
  let complete = true;
@@ -12309,7 +13033,14 @@ function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, fl
12309
13033
  if (matchFe && callerFe &&
12310
13034
  (isTestFile(matchFe.relativePath, matchFe.language) || isTestPath(matchFe.relativePath)) &&
12311
13035
  !(isTestFile(callerFe.relativePath, callerFe.language) || isTestPath(callerFe.relativePath))) return null;
12312
- return match;
13036
+ const facts = _confirmationFacts(index, def.file, call, [match], {
13037
+ ...(flowEntry?.type && !call.receiverType && {
13038
+ receiverType: flowEntry.type, receiverTypeSource: 'flow',
13039
+ receiverOrigin: { source: 'flow', ...flowEntry }, originFile: flowEntry.fromFile,
13040
+ }),
13041
+ });
13042
+ return validateConfirmation({ facts }, facts.targets).verdict === 'establishes-target'
13043
+ ? match : null;
12313
13044
  }
12314
13045
 
12315
13046
  const _CSHARP_TYPE_ALIASES = new Map([
@@ -15111,8 +15842,18 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
15111
15842
  const parsed = _returnTypeNameNominal(def.returnType, language, { selfClass: selfType });
15112
15843
  if (!parsed) return null;
15113
15844
  const origin = _resolveFlowTypeOrigin(index, def.file || opts.filePath, parsed.name, parsed.qualifier);
15114
- if (!origin) return null;
15115
- return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
15845
+ const wrappers = language === 'rust' ? owned.map(candidate =>
15846
+ _rustReturnWrapper(index, candidate.returnType, candidate.file || opts.filePath,
15847
+ selfType, candidate, [], true)) : [];
15848
+ // Identical annotation text in different impl files can bind different
15849
+ // aliases. Every possible producer must agree on the payload identity.
15850
+ const rustWrapper = wrappers.length && wrappers.every(wrapper => wrapper &&
15851
+ wrapper.kind === wrappers[0].kind && wrapper.type === wrappers[0].type &&
15852
+ wrapper.fromFile === wrappers[0].fromFile) ? wrappers[0] : null;
15853
+ if (!origin && !rustWrapper) return null;
15854
+ return { ...(origin && { type: parsed.name }), ...(origin?.fromFile && { fromFile: origin.fromFile }),
15855
+ ...(language === 'rust' && owned.length === 1 && { rustReturnDeclaration: _rustReturnDeclaration(def) }),
15856
+ ...(rustWrapper && { rustWrapper }) };
15116
15857
  }
15117
15858
  // Structural: heads must agree; `this`/`Self` are the receiver's type
15118
15859
  // (checked BEFORE the reject set — with a known owner they ARE identity);
@@ -15266,7 +16007,72 @@ function _rustPathIsKnownExternal(index, fileEntry, filePath, receiver, name) {
15266
16007
  `${segments.join('::')}::${name}`, filePath, index.root);
15267
16008
  }
15268
16009
 
15269
- function _rustPatternReceiverType(index, fileEntry, filePath, record) {
16010
+ /** A qualified free function belongs to its exact module, never an impl
16011
+ * whose type or method happens to share either path component. Re-exports
16012
+ * and inline modules abstain until their complete item path is available.
16013
+ */
16014
+ function _rustModuleProducer(index, fileEntry, filePath, call) {
16015
+ if (!call.isPathCall || !call.receiver || call.receiverType || call.localShadow) return null;
16016
+ const parts = call.receiver.split('::');
16017
+ const root = parts[0];
16018
+ const bindings = (fileEntry.importBindings || []).filter(b => (b.alias || b.name) === root);
16019
+ const locals = (index.symbols.get(root) || []).filter(d => d.file === filePath);
16020
+ if (locals.some(d => d.type !== 'module') || bindings.length > 1) return null;
16021
+ const binding = bindings[0];
16022
+ const specifier = binding ? [binding.module, ...parts.slice(1)].join('::') : call.receiver;
16023
+ const terminal = specifier.split('::').at(-1);
16024
+ if (!terminal || ['Self', 'self', 'super', 'crate'].includes(terminal)) return null;
16025
+ const relative = fileEntry.moduleResolved?.[specifier];
16026
+ const destination = relative ? path.resolve(index.root, relative)
16027
+ : resolveRustImport(specifier, filePath, index.root);
16028
+ if (!destination || !index.files.has(destination) || destination === filePath) return null;
16029
+ const base = path.basename(destination, '.rs');
16030
+ if ((base === 'mod' ? path.basename(path.dirname(destination)) : base) !== terminal) return null;
16031
+ const candidates = (index.symbols.get(call.name) || []).filter(d => d.file === destination &&
16032
+ !NON_CALLABLE_TYPES.has(d.type) && !d.className && !d.receiver && !d.namespace);
16033
+ if (candidates.length !== 1 || !candidates[0].returnType) return null;
16034
+ const definition = candidates[0];
16035
+ if ((index.files.get(destination).symbols || []).some(d => d.type === 'module' &&
16036
+ d.startLine < definition.startLine && d.endLine >= definition.endLine)) return null;
16037
+ return { definition, facts: {
16038
+ call: { file: fileEntry.relativePath, line: call.line, start: call.callStart,
16039
+ end: call.callEnd, receiver: call.receiver, name: call.name },
16040
+ binding: binding || null, module: { specifier, file: path.relative(index.root, destination) },
16041
+ declaration: { ...declarationIdentity(definition), returnType: definition.returnType },
16042
+ } };
16043
+ }
16044
+
16045
+ function _rustPatternReceiverType(index, fileEntry, filePath, record, ctx) {
16046
+ if (['Some', 'Ok'].includes(record.receiverPatternVariant) && !record.receiverPatternOwner &&
16047
+ (record.receiverPatternIndex || 0) === 0 &&
16048
+ (record.receiverPatternSourceCallStart != null || record.receiverPatternSourceVariable)) {
16049
+ if (!ctx) {
16050
+ const records = index.getCachedCalls(filePath);
16051
+ ctx = { records, memo: new Map(), visiting: new Set(),
16052
+ getFlowMap: () => _buildReturnTypeFlowMap(index, filePath, records) };
16053
+ }
16054
+ const shadow = (index.symbols.get(record.receiverPatternVariant) || []).some(d => d.file === filePath) ||
16055
+ (fileEntry.importBindings || []).some(b => (b.alias || b.name) === record.receiverPatternVariant || b.name === '*');
16056
+ const sources = record.receiverPatternSourceVariable
16057
+ ? [_lookupReturnTypeFlow(ctx.getFlowMap(), { ...record, receiver: record.receiverPatternSourceVariable,
16058
+ receiverPatternShadow: false })]
16059
+ : ctx.records.filter(source => source.callStart === record.receiverPatternSourceCallStart &&
16060
+ source.callEnd === record.receiverPatternSourceCallEnd).map(source =>
16061
+ _typeOfCallResultFold(index, fileEntry, filePath, source, ctx));
16062
+ const producer = sources.length === 1 && sources[0]?.rustReturnDeclaration;
16063
+ const contract = !shadow && producer && _rustReturnWrapper(index, producer.returnType, producer.file,
16064
+ producer.className, producer, record.receiverPatternProjection || []);
16065
+ if (contract && validateRustWrapperContract(contract) &&
16066
+ contract.kind === (record.receiverPatternVariant === 'Some' ? 'Option' : 'Result')) {
16067
+ return { type: contract.type, fromFile: contract.fromFile,
16068
+ wrapperPattern: { variant: record.receiverPatternVariant, contract,
16069
+ source: { start: record.receiverPatternSourceCallStart, end: record.receiverPatternSourceCallEnd,
16070
+ variable: record.receiverPatternSourceVariable }, shadowDeclarations: [], shadowBindings: [] } };
16071
+ }
16072
+ // A tuple payload cannot be treated as its outer type by the older
16073
+ // whole-payload path below.
16074
+ if (record.receiverPatternProjection?.length) return null;
16075
+ }
15270
16076
  let variants = (index.symbols.get(record.receiverPatternVariant) || [])
15271
16077
  .filter(definition => definition.type === 'variant');
15272
16078
  if (record.receiverPatternOwner) {
@@ -15356,7 +16162,15 @@ const _RUST_ITERATOR_ITEM_CALLBACKS = new Set([
15356
16162
  ]);
15357
16163
 
15358
16164
  function _rustRecordReceiverType(index, fileEntry, filePath, record, ctx) {
15359
- if (record.receiverType && !record.receiverIsChainRoot) {
16165
+ if (record.receiver === 'self') {
16166
+ const enclosing = index.findEnclosingFunction(filePath, record.line, true);
16167
+ if (enclosing?.className) {
16168
+ const origin = _resolveFlowTypeOrigin(index, filePath, enclosing.className);
16169
+ if (origin?.fromFile) return { type: enclosing.className, fromFile: origin.fromFile };
16170
+ }
16171
+ }
16172
+ if (record.receiverType && !record.receiverIsChainRoot &&
16173
+ !record.receiverTypeGuessed && record.receiverTypeSource !== 'guess') {
15360
16174
  const origin = _resolveFlowTypeOrigin(
15361
16175
  index, filePath, record.receiverType, record.receiverTypeQualifier);
15362
16176
  if (origin?.fromFile) {
@@ -15383,9 +16197,13 @@ function _rustRecordReceiverType(index, fileEntry, filePath, record, ctx) {
15383
16197
  }
15384
16198
  if (record.receiverPatternVariant) {
15385
16199
  const pattern = _rustPatternReceiverType(
15386
- index, fileEntry, filePath, record);
16200
+ index, fileEntry, filePath, record, ctx);
15387
16201
  if (pattern) return pattern;
15388
16202
  }
16203
+ if (record.enclosingFunction?.closureParameterNames?.includes(record.receiver)) {
16204
+ const parameter = _rustClosureReceiverType(index, fileEntry, filePath, record, ctx);
16205
+ if (parameter) return parameter;
16206
+ }
15389
16207
  if (record.receiver && !record.receiverIsChainRoot &&
15390
16208
  !record.receiverPatternShadow) {
15391
16209
  const flow = _lookupReturnTypeFlow(ctx.getFlowMap(), record);
@@ -15485,6 +16303,35 @@ const _RUST_ITERATOR_ITEM_PRESERVING = new Set([
15485
16303
  'skip', 'skip_while', 'step_by', 'take', 'take_while',
15486
16304
  ]);
15487
16305
 
16306
+ function _rustIterationReceiverType(index, fileEntry, filePath, call, ctx) {
16307
+ let items = [];
16308
+ let producers = [];
16309
+ if (call.receiverIterationVariable) {
16310
+ const flow = _lookupReturnTypeFlow(ctx.getFlowMap(), {
16311
+ ...call, receiver: call.receiverIterationVariable,
16312
+ });
16313
+ if (flow?.iteratorItemType) {
16314
+ items = [{ type: flow.iteratorItemType, fromFile: flow.iteratorItemFromFile }];
16315
+ producers = [{ ...flow }];
16316
+ }
16317
+ } else {
16318
+ const sources = _chainedProducerRecords(ctx, {
16319
+ receiverCall: call.receiverIterationCall,
16320
+ receiverCallIsMethod: call.receiverIterationCallIsMethod,
16321
+ receiverCallLine: call.receiverIterationCallLine,
16322
+ receiverCallStart: call.receiverIterationCallStart,
16323
+ receiverCallEnd: call.receiverIterationCallEnd,
16324
+ });
16325
+ items = sources.map(source => _rustIteratorOutputItemType(index, fileEntry, filePath, source, ctx));
16326
+ producers = sources.map(source => ({ name: source.name,
16327
+ site: occurrenceIdentity(fileEntry.relativePath, source) }));
16328
+ }
16329
+ if (!items.length || items.some(item => !item?.fromFile) ||
16330
+ new Set(items.map(item => item.type)).size !== 1 ||
16331
+ new Set(items.map(item => item.fromFile)).size !== 1) return null;
16332
+ return { ...items[0], alternatives: items, producers };
16333
+ }
16334
+
15488
16335
  function _rustIteratorOutputItemType(index, fileEntry, filePath, source, ctx, visiting = new Set()) {
15489
16336
  if (!source || visiting.has(source)) return null;
15490
16337
  visiting.add(source);
@@ -15731,6 +16578,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
15731
16578
  externalConcrete: true,
15732
16579
  };
15733
16580
  }
16581
+ const module = language === 'rust' ? _rustModuleProducer(index, fileEntry, filePath, record) : null;
16582
+ if (module) {
16583
+ const producer = module.definition;
16584
+ const parsed = _returnTypeNameNominal(producer.returnType, language);
16585
+ const origin = parsed && _resolveFlowTypeOrigin(index, producer.file, parsed.name, parsed.qualifier);
16586
+ if (!origin) return null;
16587
+ return { type: parsed.name, fromFile: origin.fromFile, moduleProducer: module.facts,
16588
+ rustReturnDeclaration: _rustReturnDeclaration(producer),
16589
+ rustWrapper: _rustReturnWrapper(index, producer.returnType, producer.file, null, producer) };
16590
+ }
15734
16591
  const segs = String(record.receiver).split('::');
15735
16592
  let seg = segs.pop();
15736
16593
  if (seg === 'Self') {
@@ -15872,11 +16729,15 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
15872
16729
  if (record.isMethod) {
15873
16730
  let rt = _goBuiltinChainedReceiverType(
15874
16731
  index, fileEntry, filePath, record);
15875
- if (record.receiverType && !record.receiverIsChainRoot) {
15876
- const origin = nominal
16732
+ if (record.receiverType && !record.receiverIsChainRoot &&
16733
+ !record.receiverTypeGuessed && record.receiverTypeSource !== 'guess') {
16734
+ const origin = language === 'rust'
16735
+ ? _rustFlowReceiverOrigin(index, filePath, record.receiverType, record.receiverTypeQualifier)
16736
+ : nominal
15877
16737
  ? _resolveFlowTypeOrigin(index, filePath, record.receiverType,
15878
16738
  record.receiverTypeQualifier)
15879
16739
  : null;
16740
+ if (language === 'rust' && !origin?.fromFile) return null;
15880
16741
  rt = {
15881
16742
  type: record.receiverType,
15882
16743
  ...(origin?.fromFile && { fromFile: origin.fromFile }),
@@ -16073,7 +16934,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
16073
16934
  if (!parsed) return null;
16074
16935
  const origin = _resolveFlowTypeOrigin(index, chosen.file || filePath, parsed.name, parsed.qualifier);
16075
16936
  if (!origin) return null;
16076
- return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
16937
+ return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }),
16938
+ ...(language === 'rust' && chosen.file === filePath && { rustReturnDeclaration: _rustReturnDeclaration(chosen) }) };
16077
16939
  }
16078
16940
  if (language === 'python' && !consumerAwaited && chosen.isAsync) return null;
16079
16941
  let head = _structuralTypeHead(chosen.returnType, {
@@ -16167,6 +17029,9 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
16167
17029
  const typeTexts = new Set(results.map(r => r.typeText));
16168
17030
  let result = {
16169
17031
  type: results[0].type,
17032
+ ...(results.length === 1 && results[0].moduleProducer && {
17033
+ moduleProducer: results[0].moduleProducer,
17034
+ }),
16170
17035
  ...(typeTexts.size === 1 && results[0].typeText && {
16171
17036
  typeText: results[0].typeText,
16172
17037
  }),
@@ -16317,4 +17182,4 @@ function findCallbackUsages(index, name) {
16317
17182
  return usages;
16318
17183
  }
16319
17184
 
16320
- module.exports = { _unresolvedModuleIsGap, _importReaches, _sameNominalPackageDir, getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };
17185
+ module.exports = { isProvenanceBuiltinReceiver, provenanceParameterIdentity: _overloadTypeIdentity, selectProvenanceOverload, _unresolvedModuleIsGap, _importReaches, _sameNominalPackageDir, getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };