ucn 5.0.6 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/callers.js CHANGED
@@ -148,6 +148,54 @@ function _javaPackageKey(relativePath) {
148
148
  return path.posix.dirname(packagePath);
149
149
  }
150
150
 
151
+ /**
152
+ * Resolve a capitalized Java receiver NAME through the file's static imports
153
+ * to a static FIELD's declared type (fix #286c, jsoup-measured: `import
154
+ * static ...SimpleBufferedInput.BufferPool` binds `BufferPool.borrow()` to
155
+ * the SoftPool-typed field — javac resolves single-static-imports ahead of
156
+ * the capitalized static-call reading). Returns the declared type HEAD, or
157
+ * null when the name isn't a static-imported field (a static import naming a
158
+ * nested TYPE keeps type-qualified semantics), the field has no usable
159
+ * declared type, or the type head is package-qualified (unpinnable here —
160
+ * the #268(4a) qualified-field discipline).
161
+ */
162
+ function _javaStaticImportedFieldType(index, fileEntry, receiverName) {
163
+ if (!receiverName || !fileEntry) return null;
164
+ // Single-static-imports only: the import record cannot distinguish a
165
+ // STATIC wildcard (`import static Owner.*` — imports fields) from a
166
+ // package wildcard (`import com.ex.*` — types only), and typing a
167
+ // receiver from a field javac never binds could exclude a true edge.
168
+ const candidates = [];
169
+ for (const im of (fileEntry.importBindings || [])) {
170
+ const mod = String(im.module || '');
171
+ if (im.name === receiverName && mod.endsWith('.' + receiverName)) {
172
+ const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[mod];
173
+ if (rel) candidates.push(rel);
174
+ break;
175
+ }
176
+ }
177
+ for (const rel of candidates) {
178
+ const entry = index.files.get(path.join(index.root, rel));
179
+ if (!entry) continue;
180
+ // The import binding shape also matches plain class imports
181
+ // (`import com.ex.BufferPool`): any resolved TYPE of the name —
182
+ // top-level or nested — keeps static-call semantics.
183
+ const namedType = (entry.symbols || []).some(s =>
184
+ s.name === receiverName &&
185
+ ['class', 'interface', 'enum', 'record', 'annotation'].includes(s.type));
186
+ if (namedType) return null;
187
+ const field = (entry.symbols || []).find(s =>
188
+ s.name === receiverName && s.className &&
189
+ (s.type === 'field' || s.memberType === 'field') &&
190
+ (s.modifiers || []).includes('static'));
191
+ if (!field) continue;
192
+ const head = String(field.fieldType || '').replace(/<[\s\S]*$/, '').trim();
193
+ if (!head || head.includes('.') || head.endsWith('[]')) return null;
194
+ return head;
195
+ }
196
+ return null;
197
+ }
198
+
151
199
  function _javaConstructorDisposition(index, filePath, fileEntry, call, targetDefs) {
152
200
  const typeKinds = new Set(['class', 'record', 'enum']);
153
201
  const targets = (targetDefs || []).filter(d => typeKinds.has(d.type));
@@ -271,6 +319,8 @@ function findCallers(index, name, options = {}) {
271
319
  const typeQueryTargets = options.targetDefinitions || definitions;
272
320
  const targetIsTypeQuery = typeQueryTargets.length > 0 &&
273
321
  typeQueryTargets.every(target => IDENTITY_TYPE_KINDS.has(target.type));
322
+ const targetDefinitionFiles = new Set(
323
+ typeQueryTargets.map(definition => definition.file).filter(Boolean));
274
324
 
275
325
  // Possible-dispatch tiering inputs (nominal contract surface) — all fixed
276
326
  // per query, computed lazily once. targetTypes mirrors the receiver-class
@@ -435,7 +485,8 @@ function findCallers(index, name, options = {}) {
435
485
  const moduleFile = path.join(index.root, rel);
436
486
  if (!aliasTargetFiles.has(moduleFile)) continue;
437
487
  const exported = index.files.get(moduleFile)?.exportDetails || [];
438
- if (!exported.some(e => e.type === 'module.exports' && e.name === name)) continue;
488
+ if (!exported.some(e => e.type === 'module.exports' &&
489
+ e.defaultLike && (e.localName || e.name) === name)) continue;
439
490
  if (!importAliasLocals.has(fp)) importAliasLocals.set(fp, new Set());
440
491
  importAliasLocals.get(fp).add(b.alias || b.name);
441
492
  }
@@ -557,6 +608,8 @@ function findCallers(index, name, options = {}) {
557
608
  try {
558
609
  const calls = getCachedCalls(index, filePath);
559
610
  if (!calls) continue;
611
+ const structuralLanguage =
612
+ langTraits(fileEntry.language)?.typeSystem === 'structural';
560
613
 
561
614
  for (let call of calls) {
562
615
  // Skip if not matching our target name (also check alias resolution)
@@ -607,6 +660,35 @@ function findCallers(index, name, options = {}) {
607
660
  continue;
608
661
  }
609
662
 
663
+ if (fileEntry.language === 'cpp' && call.macroArguments) {
664
+ const macroDisposition = _cppMacroTargetDisposition(
665
+ index, filePath, call,
666
+ options.targetDefinitions || definitions);
667
+ if (macroDisposition?.kind === 'other') {
668
+ recordExcluded(filePath, call.line,
669
+ 'macro-requalified');
670
+ continue;
671
+ }
672
+ if (macroDisposition?.kind === 'uncertain') {
673
+ if (collectAccount) {
674
+ routeUnverified(
675
+ filePath, fileEntry, call,
676
+ 'macro-expansion', calledAs, {
677
+ uncertaintyClass: 'compile-time-dispatch',
678
+ dispatchFamily:
679
+ `${call.name} macro expansion`,
680
+ });
681
+ }
682
+ continue;
683
+ }
684
+ if (macroDisposition?.kind === 'qualified') {
685
+ call = macroDisposition.qualifier === 'global'
686
+ ? { ...call, globalQualified: true }
687
+ : { ...call, isMethod: true, isPathCall: true,
688
+ receiver: macroDisposition.qualifier };
689
+ }
690
+ }
691
+
610
692
  // Static member syntax carries two compiler symbols:
611
693
  // `JValue.Compare(...)` calls Compare and references the
612
694
  // JValue type. Class/type queries promise usages rather than
@@ -1127,8 +1209,10 @@ function findCallers(index, name, options = {}) {
1127
1209
  } };
1128
1210
  foldCtxCache.set(filePath, foldCtx);
1129
1211
  }
1130
- const flowEntry = _foldChainedReceiverType(
1131
- index, fileEntry, filePath, call, foldCtx) ||
1212
+ const flowEntry = _goBuiltinChainedReceiverType(
1213
+ index, fileEntry, filePath, call) ||
1214
+ _foldChainedReceiverType(
1215
+ index, fileEntry, filePath, call, foldCtx) ||
1132
1216
  _nominalChainedReceiverType(
1133
1217
  index, call, fileEntry, filePath);
1134
1218
  if (flowEntry && flowEntry.externalVia) {
@@ -1359,7 +1443,50 @@ function findCallers(index, name, options = {}) {
1359
1443
  if (td.className) targetTypes.add(td.className);
1360
1444
  if (td.receiver) targetTypes.add(td.receiver.replace(/^\*/, ''));
1361
1445
  }
1362
- if (targetTypes.size > 0 && call.receiver && call.receiverType &&
1446
+ // Qualified receiver types resolve their package FIRST
1447
+ // (fix #298 — the #273 gate's callback twin):
1448
+ // `netDial = (&net.Dialer{}).DialContext` is net's
1449
+ // Dialer; the bare name must never match a project
1450
+ // `Dialer` pin, and the external contract must carry
1451
+ // its label so consumers can defer the site.
1452
+ if (fileEntry.language === 'go' &&
1453
+ call.receiverType && call.receiverTypeQualifier) {
1454
+ const cbQualified = _goQualifiedReceiverType(
1455
+ index, fileEntry, call.receiverTypeQualifier,
1456
+ call.receiverType);
1457
+ if (cbQualified) {
1458
+ const cbIsContract = cbQualified.kind === 'project' &&
1459
+ cbQualified.defs.some(d =>
1460
+ d.type === 'interface' || d.type === 'trait');
1461
+ if (cbQualified.kind !== 'project' || cbIsContract) {
1462
+ if (collectAccount) {
1463
+ const cbPlatformConcrete = cbQualified.kind !== 'project' &&
1464
+ getLanguageAdapter('go')?.isPlatformConcreteType?.(
1465
+ call.receiverTypeQualifier, call.receiverType);
1466
+ if (cbPlatformConcrete) {
1467
+ recordExcluded(filePath, call.line, 'external-package');
1468
+ } else {
1469
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
1470
+ dispatchVia: cbQualified.via,
1471
+ dispatchCandidates: countDispatchCandidates(call.receiverType),
1472
+ ...(cbQualified.kind !== 'project' && { externalContract: true }),
1473
+ });
1474
+ }
1475
+ }
1476
+ continue;
1477
+ }
1478
+ } else {
1479
+ // Unresolvable qualifier — the bare type name
1480
+ // must not match a project pin (#206c). Visible,
1481
+ // never validated, never excluded.
1482
+ if (collectAccount) {
1483
+ routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs,
1484
+ { dispatchCandidates: countDispatchCandidates(call.receiverType) });
1485
+ }
1486
+ continue;
1487
+ }
1488
+ }
1489
+ if (targetTypes.size > 0 && call.receiverType &&
1363
1490
  !targetTypes.has(call.receiverType)) {
1364
1491
  // Raw-set mismatch — check the CLOSED set (aliases +
1365
1492
  // non-overriding subtypes incl. Go embedding) before
@@ -1792,6 +1919,21 @@ function findCallers(index, name, options = {}) {
1792
1919
  options.targetDefinitions || definitions);
1793
1920
  if (resolvedByExtensionMethod) receiverTypeValidated = true;
1794
1921
  }
1922
+ // A static-imported FIELD shadows the capitalized static-
1923
+ // qualifier reading (fix #286c, jsoup-measured): javac binds
1924
+ // `BufferPool.borrow()` to the SoftPool-typed field imported
1925
+ // by `import static ...SimpleBufferedInput.BufferPool`, never
1926
+ // to a class named BufferPool. The declared type is compiler-
1927
+ // true — feed it to the normal nominal receiver physics.
1928
+ if (call.isMethod && !call.receiverType && call.receiver &&
1929
+ fileEntry.language === 'java' && /^[A-Z]/.test(call.receiver)) {
1930
+ const staticFieldType = _javaStaticImportedFieldType(
1931
+ index, fileEntry, call.receiver);
1932
+ if (staticFieldType) {
1933
+ call = { ...call, receiverType: staticFieldType,
1934
+ receiverIsTypeQualified: false };
1935
+ }
1936
+ }
1795
1937
  if (collectAccount && call.isMethod && !call.receiverType &&
1796
1938
  fileEntry.language === 'java' && /^[A-Z]/.test(call.receiver || '')) {
1797
1939
  // Java permits inherited static methods to be invoked
@@ -2242,6 +2384,16 @@ function findCallers(index, name, options = {}) {
2242
2384
  }
2243
2385
  }
2244
2386
 
2387
+ // Resolve this before the generic untyped-field guard below:
2388
+ // `api.nested.run()` is syntactically a field receiver, but an
2389
+ // exported namespace chain can make both hops compiler-exact.
2390
+ const recvExportedNamespace = (!call.receiverIsModule && call.isMethod &&
2391
+ (call.receiver || call.receiverRoot) &&
2392
+ structuralLanguage)
2393
+ ? _importedNamespaceMemberOwnership(
2394
+ index, fileEntry, call, targetDefinitionFiles)
2395
+ : null;
2396
+
2245
2397
  // Go package-owned value receiver (`io.Discard.Write`). The
2246
2398
  // imported package owns `Discard`; file/package method-name
2247
2399
  // bindings cannot identify its concrete type. Keep the edge
@@ -2263,7 +2415,7 @@ function findCallers(index, name, options = {}) {
2263
2415
  // allowing a same-file `query` definition to claim it.
2264
2416
  if (collectAccount && call.isMethod && call.receiverField &&
2265
2417
  !call.receiverType && !fieldHopType && !fieldDispatchType &&
2266
- !resolvedBySameClass &&
2418
+ !resolvedBySameClass && !recvExportedNamespace &&
2267
2419
  ['javascript', 'typescript', 'tsx', 'html'].includes(
2268
2420
  fileEntry.language)) {
2269
2421
  routeUnverified(
@@ -2383,15 +2535,27 @@ function findCallers(index, name, options = {}) {
2383
2535
  }
2384
2536
  const inherited = _isAncestorOfTargetClass(
2385
2537
  index, d.className, [boundDef]);
2538
+ const derivedApplicableSibling = definitions.some(candidate =>
2539
+ candidate !== boundDef &&
2540
+ !NON_CALLABLE_TYPES.has(candidate.type) &&
2541
+ candidate.className === boundDef.className &&
2542
+ candidate.file === boundDef.file &&
2543
+ _overloadApplicable(index, call, candidate));
2386
2544
  // A derived same-name method hides inherited
2387
2545
  // overloads until it proves inapplicable.
2388
2546
  // Receiver-blind bindings may therefore be
2389
2547
  // overruled only when static argument evidence
2390
2548
  // accepts this inherited target and rejects the
2391
- // derived declaration.
2549
+ // ENTIRE derived overload family. Looking only
2550
+ // at the first bound declaration let an
2551
+ // inapplicable one-arg sibling expose a hidden
2552
+ // base method even when another derived sibling
2553
+ // accepted the call (Newtonsoft's new static
2554
+ // JArray/JObject/JProperty.Load families).
2392
2555
  return inherited &&
2393
2556
  _overloadApplicable(index, call, d) &&
2394
- !_overloadApplicable(index, call, boundDef);
2557
+ !_overloadApplicable(index, call, boundDef) &&
2558
+ !derivedApplicableSibling;
2395
2559
  })) ||
2396
2560
  (fileEntry.language === 'cpp' &&
2397
2561
  !boundDef.className && !boundDef.receiver &&
@@ -2553,7 +2717,9 @@ function findCallers(index, name, options = {}) {
2553
2717
  continue;
2554
2718
  }
2555
2719
  const resolvedAbs = path.join(index.root, rel);
2556
- const verdict = _nameBindingReaches(index, resolvedAbs, b.name, tFiles);
2720
+ const verdict = b.defaultLike
2721
+ ? _defaultBindingReaches(index, resolvedAbs, tFiles)
2722
+ : _nameBindingReaches(index, resolvedAbs, b.name, tFiles);
2557
2723
  if (verdict === 'yes') { reaches = true; break; }
2558
2724
  if (verdict === 'unknown') undetermined = true;
2559
2725
  }
@@ -2731,12 +2897,18 @@ function findCallers(index, name, options = {}) {
2731
2897
  const recvSubmoduleRel = (!call.receiverIsModule && call.isMethod && call.receiver &&
2732
2898
  langTraits(fileEntry.language)?.typeSystem === 'structural')
2733
2899
  ? _submoduleReceiverModule(index, fileEntry, call.receiver) : null;
2900
+ // A named/default import can itself be an exported namespace
2901
+ // object: `import { z } from './index'; z.string()`, where
2902
+ // index does `import * as z from './api'; export { z }`.
2903
+ // Preserve that compiler-exact namespace identity without
2904
+ // treating arbitrary imported object values as modules.
2734
2905
 
2735
2906
  // Module receiver: httpx.get() / ns.helper() dispatches to a
2736
2907
  // module export — it can never be a CLASS METHOD call. Applies
2737
2908
  // only when every target is a class method; standalone-function
2738
2909
  // and class (constructor) targets keep flowing on import evidence.
2739
- if (!bindingId && !resolvedBySameClass && call.isMethod && call.receiverIsModule &&
2910
+ if ((!bindingId || recvExportedNamespace) && !resolvedBySameClass && call.isMethod &&
2911
+ (call.receiverIsModule || recvExportedNamespace) &&
2740
2912
  langTraits(fileEntry.language)?.typeSystem === 'structural' &&
2741
2913
  targetDefs.length > 0 && targetDefs.every(d => d.className)) {
2742
2914
  isUncertain = true;
@@ -2758,30 +2930,54 @@ function findCallers(index, name, options = {}) {
2758
2930
  // binding (name-level — the file importing the target for
2759
2931
  // other names proves nothing): binding module external →
2760
2932
  // excluded; resolves to a project file that doesn't reach a
2761
- // target (directly or one re-export hop) → visible, not
2762
- // excluded (deep barrel chains exceed the hop budget);
2933
+ // target by a definitive name-level `no` → excluded as another
2934
+ // definition; an unknown chain stays visible (deep barrels and
2935
+ // dynamic CJS surfaces can exceed the modeled ownership);
2763
2936
  // unresolved-but-project-looking → visible (resolver gap).
2764
- if (!bindingId && !resolvedBySameClass && call.isMethod &&
2765
- (call.receiverIsModule || recvSubmoduleRel) &&
2937
+ if ((!bindingId || recvExportedNamespace) && !resolvedBySameClass && call.isMethod &&
2938
+ (call.receiverIsModule || recvSubmoduleRel || recvExportedNamespace) &&
2766
2939
  langTraits(fileEntry.language)?.typeSystem === 'structural') {
2767
- const recvBindings = _structuralModuleBindings(fileEntry, call);
2768
- const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
2769
- if (recvBindings.length > 0 && !tFiles.has(filePath)) {
2770
- let reaches = false;
2771
- let projectish = false;
2940
+ const recvBindings = recvExportedNamespace
2941
+ ? [] : _structuralModuleBindings(fileEntry, call);
2942
+ const tFiles = targetDefinitionFiles;
2943
+ // Same-file targets get NO bypass (fix #294, flask-measured:
2944
+ // `import json as _json; _json.dump(...)` in the file
2945
+ // defining flask's own `dump` confirmed a self-recursive
2946
+ // caller — the module indirection REPLACES file scope, so
2947
+ // ownership adjudicates regardless of where the pin sits;
2948
+ // the Go twin already excludes same-file package-qualified
2949
+ // targets). A self-module import stays confirmable: a
2950
+ // binding resolving into a target file reaches immediately.
2951
+ if (recvBindings.length > 0 || recvExportedNamespace) {
2952
+ let reaches = recvExportedNamespace?.verdict === 'yes';
2953
+ let projectish = !!recvExportedNamespace;
2954
+ let undetermined = recvExportedNamespace?.verdict === 'unknown';
2955
+ let resolvedBindings = recvExportedNamespace ? 1 : 0;
2956
+ let definitiveOtherBindings = recvExportedNamespace?.verdict === 'no' ? 1 : 0;
2772
2957
  for (const b of recvBindings) {
2773
- const rel = (fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]) ||
2774
- recvSubmoduleRel;
2958
+ // A #224-proven submodule receiver chases from the
2959
+ // SUBMODULE file, never the from-module (fix #294b,
2960
+ // flask fresh-arm-measured: `from . import cli;
2961
+ // cli.AppGroup()` chased flask/__init__.py — where
2962
+ // 'AppGroup' dead-ends — because moduleResolved['.']
2963
+ // shadowed the composed '.cli' spec, excluding a
2964
+ // compiler-true constructor edge other-definition-
2965
+ // import. Python binds the submodule OBJECT; its
2966
+ // attributes live in the submodule file.
2967
+ const rel = recvSubmoduleRel ||
2968
+ (fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]);
2775
2969
  if (!rel) {
2776
2970
  const mod = String(b.module);
2777
2971
  const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
2778
2972
  if (mod.startsWith('.') ||
2779
2973
  (firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
2780
2974
  projectish = true;
2975
+ undetermined = true;
2781
2976
  }
2782
2977
  continue;
2783
2978
  }
2784
2979
  projectish = true;
2980
+ resolvedBindings++;
2785
2981
  const resolvedAbs = path.join(index.root, rel);
2786
2982
  // Name-level ownership (fix #217 applied to module
2787
2983
  // receivers — zod family D): `z._default(...)` asks
@@ -2791,17 +2987,29 @@ function findCallers(index, name, options = {}) {
2791
2987
  // chase is definitive only on fully-modeled ESM/
2792
2988
  // Python surfaces — 'unknown' (CJS, stars, module
2793
2989
  // assignments) falls back to file-level reach.
2794
- const verdict = _nameBindingReaches(index, resolvedAbs, call.name, tFiles);
2990
+ const ambiguousCjsMember =
2991
+ _cjsMemberOwnershipAmbiguous(index, resolvedAbs, call.name);
2992
+ const verdict = ambiguousCjsMember
2993
+ ? 'unknown'
2994
+ : _nameBindingReaches(index, resolvedAbs, call.name, tFiles);
2795
2995
  if (verdict === 'yes' ||
2796
- (verdict === 'unknown' && _importReaches(index, resolvedAbs, tFiles))) {
2996
+ (verdict === 'unknown' && !ambiguousCjsMember &&
2997
+ _importReaches(index, resolvedAbs, tFiles))) {
2797
2998
  reaches = true; break;
2798
2999
  }
3000
+ if (verdict === 'no') definitiveOtherBindings++;
3001
+ else undetermined = true;
2799
3002
  }
2800
3003
  if (!reaches) {
2801
3004
  if (!projectish) {
2802
3005
  recordExcluded(filePath, call.line, 'external-package');
2803
3006
  continue;
2804
3007
  }
3008
+ if (!undetermined && resolvedBindings > 0 &&
3009
+ definitiveOtherBindings === resolvedBindings) {
3010
+ recordExcluded(filePath, call.line, 'other-definition-import');
3011
+ continue;
3012
+ }
2805
3013
  if (collectAccount) {
2806
3014
  routeUnverified(filePath, fileEntry, call, 'no-import-link', calledAs);
2807
3015
  continue;
@@ -2979,7 +3187,8 @@ function findCallers(index, name, options = {}) {
2979
3187
  }
2980
3188
  if (knownType && !BUILTIN_RECEIVER_TYPES.has(knownType) &&
2981
3189
  _isGenericParamReceiverType(index, filePath, call.line, knownType)) {
2982
- knownType = null;
3190
+ knownType = _genericParamTraitTarget(
3191
+ index, filePath, call.line, knownType, targetDefs);
2983
3192
  }
2984
3193
  if (knownType) {
2985
3194
  const explicitInterfaceTarget = fileEntry.language === 'csharp' &&
@@ -3077,7 +3286,15 @@ function findCallers(index, name, options = {}) {
3077
3286
  if (sameNameTypeDefs.length > 1 ||
3078
3287
  (call.receiverTypeQualifier && call.receiverTypeFlowFile)) {
3079
3288
  const identity = _resolveStructuralFlowTypeIdentity(
3080
- index, call.receiverTypeFlowFile || filePath, knownType, targetDefs);
3289
+ index, call.receiverTypeFlowFile || filePath, knownType, targetDefs,
3290
+ call.receiverTypeFlowFile ? undefined : call.receiverTypeQualifier,
3291
+ (call.receiverTypeFlowFile &&
3292
+ call.receiverTypeFlowFile !== filePath) ? undefined : {
3293
+ file: filePath,
3294
+ line: call.line,
3295
+ scopeStart: call.enclosingFunction?.startLine,
3296
+ scopeEnd: call.enclosingFunction?.endLine,
3297
+ });
3081
3298
  if (identity === 'other') {
3082
3299
  receiverTypeValidated = false;
3083
3300
  } else if (identity === 'unknown') {
@@ -3093,7 +3310,15 @@ function findCallers(index, name, options = {}) {
3093
3310
  // exact declaration and walk its file-scoped
3094
3311
  // ancestry before treating it as unrelated.
3095
3312
  const identity = _resolveStructuralFlowTypeIdentity(
3096
- index, call.receiverTypeFlowFile || filePath, knownType, targetDefs);
3313
+ index, call.receiverTypeFlowFile || filePath, knownType, targetDefs,
3314
+ call.receiverTypeFlowFile ? undefined : call.receiverTypeQualifier,
3315
+ (call.receiverTypeFlowFile &&
3316
+ call.receiverTypeFlowFile !== filePath) ? undefined : {
3317
+ file: filePath,
3318
+ line: call.line,
3319
+ scopeStart: call.enclosingFunction?.startLine,
3320
+ scopeEnd: call.enclosingFunction?.endLine,
3321
+ });
3097
3322
  if (identity === 'target') {
3098
3323
  receiverTypeValidated = true;
3099
3324
  } else if (identity === 'unknown') {
@@ -3387,6 +3612,31 @@ function findCallers(index, name, options = {}) {
3387
3612
  // resolution above owns it.
3388
3613
  if (call.isPathCall && /^[A-Z]/.test(receiverSegment) &&
3389
3614
  receiverSegment !== 'Self') {
3615
+ // Generic-param carve-out (fix #296 — the
3616
+ // #222(2) single-def branch had this, the
3617
+ // multi-def fallback didn't): `F::as_cast(x)`
3618
+ // instantiates with ANY bound-satisfying
3619
+ // type — visible, never excluded.
3620
+ if (collectAccount &&
3621
+ /^[A-Z][A-Z0-9]?$/.test(receiverSegment) &&
3622
+ !(index.symbols.get(receiverSegment) || []).some(d => IDENTITY_TYPE_KINDS.has(d.type))) {
3623
+ routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
3624
+ dispatchCandidates: methodOwnerKeys().size,
3625
+ });
3626
+ continue;
3627
+ }
3628
+ // Trait-declaration pin (fix #296): a
3629
+ // concrete non-target receiver can
3630
+ // implement the pinned trait — route
3631
+ // visible, never exclude.
3632
+ const traitVia = collectAccount &&
3633
+ _traitDeclPinImplementorRoute(index, fileEntry, receiverSegment, targetDefs);
3634
+ if (traitVia) {
3635
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
3636
+ dispatchVia: `${receiverSegment} — ${traitVia} implementor`,
3637
+ });
3638
+ continue;
3639
+ }
3390
3640
  isUncertain = true;
3391
3641
  typeMismatch = true;
3392
3642
  if (collectAccount) {
@@ -3403,7 +3653,9 @@ function findCallers(index, name, options = {}) {
3403
3653
  const t = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
3404
3654
  if (t && !targetTypes.has(t)) nonTargetClasses.add(t);
3405
3655
  }
3406
- const matchesOther = [...nonTargetClasses].some(cn => cn.toLowerCase() === receiverLower);
3656
+ const matchesOtherExact = [...nonTargetClasses].some(cn => cn === receiverSegment);
3657
+ const matchesOther = matchesOtherExact ||
3658
+ [...nonTargetClasses].some(cn => cn.toLowerCase() === receiverLower);
3407
3659
  if (matchesOther) {
3408
3660
  isUncertain = true;
3409
3661
  typeMismatch = true;
@@ -3423,6 +3675,21 @@ function findCallers(index, name, options = {}) {
3423
3675
  });
3424
3676
  continue;
3425
3677
  }
3678
+ // fix #300: a CROSS-CASE receiver-name→class
3679
+ // match is the naming-convention guess
3680
+ // (`storage` ~ `Storage`) — a guess is never
3681
+ // exclusion evidence (#266(2)). itertools-
3682
+ // measured: a test-file `struct Iter` poisoned
3683
+ // every `iter`-named receiver project-wide,
3684
+ // excluding rust-analyzer-attested true edges.
3685
+ // Exact-case matches keep excluding (grpc-go
3686
+ // names builder structs and locals both `bb`).
3687
+ if (!matchesOtherExact) {
3688
+ routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
3689
+ dispatchCandidates: methodOwnerKeys().size,
3690
+ });
3691
+ continue;
3692
+ }
3426
3693
  recordExcluded(filePath, call.line, 'receiver-other-class');
3427
3694
  continue;
3428
3695
  }
@@ -3534,12 +3801,21 @@ function findCallers(index, name, options = {}) {
3534
3801
  const targetDefs2 = options.targetDefinitions || definitions;
3535
3802
  const targetFiles2 = new Set(targetDefs2.map(d => d.file).filter(Boolean));
3536
3803
  const callerImports = index.importGraph.get(filePath);
3537
- let importEdgeLink = !!(callerImports && setSome(callerImports, imp => targetFiles2.has(imp)));
3804
+ // The caller's OWN file never counts as an import-edge hit
3805
+ // (fix #294): an import cycle (flask json/__init__ →
3806
+ // wrappers → json/__init__) would launder same-file
3807
+ // membership into "import edge" evidence, bypassing the
3808
+ // same-file clause's receiver guard below. Same-file
3809
+ // evidence is governed there, not here.
3810
+ let importEdgeLink = !!(callerImports && setSome(callerImports,
3811
+ imp => imp !== filePath && targetFiles2.has(imp)));
3538
3812
  // Check one level of re-exports (barrel files) for import evidence
3539
3813
  if (!importEdgeLink && callerImports) {
3540
3814
  for (const imp of callerImports) {
3815
+ if (imp === filePath) continue;
3541
3816
  const transImports = index.importGraph.get(imp);
3542
- if (transImports && setSome(transImports, ti => targetFiles2.has(ti))) {
3817
+ if (transImports && setSome(transImports,
3818
+ ti => ti !== filePath && targetFiles2.has(ti))) {
3543
3819
  importEdgeLink = true;
3544
3820
  break;
3545
3821
  }
@@ -3735,6 +4011,21 @@ function findCallers(index, name, options = {}) {
3735
4011
  });
3736
4012
  continue;
3737
4013
  }
4014
+ // Trait-declaration pin (fix #296, serde-as_cast-
4015
+ // measured): `u64::as_cast` / `Limb::as_cast` /
4016
+ // `Self::Unsigned::as_cast` under the trait's own
4017
+ // method pin dispatch through the pinned slot —
4018
+ // macro-generated impls are invisible to the
4019
+ // index, so the receiver not being a target type
4020
+ // proves nothing. Route visible, never exclude.
4021
+ const traitVia2 = _traitDeclPinImplementorRoute(
4022
+ index, fileEntry, pathReceiverSegment, targetDefs2);
4023
+ if (traitVia2) {
4024
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
4025
+ dispatchVia: `${pathReceiverSegment} — ${traitVia2} implementor`,
4026
+ });
4027
+ continue;
4028
+ }
3738
4029
  recordExcluded(filePath, call.line, 'path-type-mismatch');
3739
4030
  continue;
3740
4031
  }
@@ -4073,7 +4364,8 @@ function findCallers(index, name, options = {}) {
4073
4364
  // module-ownership block above already routed the ones
4074
4365
  // whose module doesn't reach the target. Submodule
4075
4366
  // receivers (fix #224) are module receivers too.
4076
- if (call.isMethod && !call.receiverIsModule && !recvSubmoduleRel) {
4367
+ if (call.isMethod && !call.receiverIsModule &&
4368
+ !recvSubmoduleRel && !call.moduleOwnedPath) {
4077
4369
  const tTypes = dispatchTargetTypes(targetDefs2);
4078
4370
  const typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
4079
4371
  const knownDispatchType = call.receiverType ||
@@ -4142,6 +4434,42 @@ function findCallers(index, name, options = {}) {
4142
4434
  });
4143
4435
  continue;
4144
4436
  }
4437
+ // Module-rooted dotted receiver (fix #294, flask-
4438
+ // measured: `flask.json.load(out)` — receiverRoot
4439
+ // resolves to a MODULE import binding, so the call
4440
+ // dispatches through a module attribute the field-hop
4441
+ // machinery cannot type. Single project-wide ownership
4442
+ // of the method name is not evidence about a module
4443
+ // attribute's value: demote-only, visible, attributed
4444
+ // via the dotted path. A root the hop DID type is
4445
+ // handled above (knownDispatchType); a from-import
4446
+ // root counts only when the resolver PROVED it a
4447
+ // submodule (#224 — a from-import name may be a
4448
+ // plain symbol, never assume).
4449
+ if (!typeQualifiedReceiver && !knownDispatchType &&
4450
+ call.receiverRoot && !call.receiverRootType &&
4451
+ langTraits(fileEntry.language)?.typeSystem === 'structural') {
4452
+ const rootBinding = (fileEntry.importBindings || []).find(b =>
4453
+ b && b.name === call.receiverRoot);
4454
+ const rootIsModule = !!rootBinding && (
4455
+ rootBinding.kind === 'import' ||
4456
+ !!_submoduleReceiverModule(index, fileEntry, call.receiverRoot));
4457
+ if (rootIsModule) {
4458
+ const field = call.receiverField ? `.${call.receiverField}` : '';
4459
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
4460
+ dispatchVia: `${call.receiverRoot}${field} — module attribute`,
4461
+ // Structured marker (outcome-eval-measured,
4462
+ // 2026-08-19): the site's name binds the
4463
+ // module's export surface, not the pinned
4464
+ // def — rename policies defer these like
4465
+ // external contracts (renaming the line
4466
+ // edits whatever the module exports under
4467
+ // the name, which the engine could not pin).
4468
+ moduleAttribute: true,
4469
+ });
4470
+ continue;
4471
+ }
4472
+ }
4145
4473
  // Unshadowed builtin-global receiver (fix #232):
4146
4474
  // JSON.parse/console.log resolve on the host object,
4147
4475
  // never a same-named project method. A lexical binding
@@ -4153,6 +4481,22 @@ function findCallers(index, name, options = {}) {
4153
4481
  (index.symbols.get(call.receiver) || []).length === 0 &&
4154
4482
  !fileEntry.bindings?.some(b => b.name === call.receiver) &&
4155
4483
  !call.receiverMemberAssigned) {
4484
+ // A candidate target that IS a member assignment
4485
+ // onto this same global (fix #286a, fastify-
4486
+ // measured: `console.log = () => {}` in a test's
4487
+ // beforeEach) patches the host object process-
4488
+ // wide — the assignment's own file cannot scope
4489
+ // it, so every `console.log(...)` site MAY
4490
+ // dispatch into the project def. Demote-only:
4491
+ // visible possible-dispatch, never confirmed,
4492
+ // never excluded.
4493
+ if (targetDefs2.some(d => d.memberAssigned &&
4494
+ d.assignedReceiver === call.receiver)) {
4495
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
4496
+ dispatchVia: `${call.receiver} — builtin global patched in project`,
4497
+ });
4498
+ continue;
4499
+ }
4156
4500
  recordExcluded(filePath, call.line, 'external-package');
4157
4501
  continue;
4158
4502
  }
@@ -4802,7 +5146,7 @@ function findCallees(index, definition, options = {}) {
4802
5146
  };
4803
5147
 
4804
5148
  let siteOrdinal = -1;
4805
- for (const call of calls) {
5149
+ for (let call of calls) {
4806
5150
  siteOrdinal++;
4807
5151
  const siteId = siteOrdinal;
4808
5152
  // Filter to calls within this function's scope
@@ -4824,6 +5168,30 @@ function findCallees(index, definition, options = {}) {
4824
5168
  continue;
4825
5169
  }
4826
5170
 
5171
+ if (language === 'cpp' && call.macroArguments) {
5172
+ const macroDisposition = _cppMacroTargetDisposition(
5173
+ index, def.file, call, index.symbols.get(call.name) || []);
5174
+ if (macroDisposition?.kind === 'other') {
5175
+ noteSite(siteId, 'excluded', 'macro-requalified', call);
5176
+ continue;
5177
+ }
5178
+ if (macroDisposition?.kind === 'uncertain') {
5179
+ if (collectAccount) {
5180
+ noteUnverified(siteId, call, 'macro-expansion', {
5181
+ uncertaintyClass: 'compile-time-dispatch',
5182
+ dispatchFamily: `${call.name} macro expansion`,
5183
+ });
5184
+ }
5185
+ continue;
5186
+ }
5187
+ if (macroDisposition?.kind === 'qualified') {
5188
+ call = macroDisposition.qualifier === 'global'
5189
+ ? { ...call, globalQualified: true }
5190
+ : { ...call, isMethod: true, isPathCall: true,
5191
+ receiver: macroDisposition.qualifier };
5192
+ }
5193
+ }
5194
+
4827
5195
  // C# extension methods are statically resolved compiler calls,
4828
5196
  // despite using instance-call syntax. Resolve them before the
4829
5197
  // ordinary receiver-owner path: the receiver type matches the
@@ -4879,11 +5247,15 @@ function findCallees(index, definition, options = {}) {
4879
5247
 
4880
5248
  // Parser-proven lexical shadow: a parameter/local named `option`
4881
5249
  // cannot call an imported/project `option`. A nested local
4882
- // function with that name is the one safe exception and remains
4883
- // eligible for exact same-file resolution below.
5250
+ // function — or a nested CLASS, whose bare name constructs it
5251
+ // (fix #286f, flask-measured: `class Foo` inside test_custom_tag,
5252
+ // `Foo("bar")` excluded local-shadow while the oracle pins the
5253
+ // nested class) — is the safe exception and remains eligible for
5254
+ // exact same-file resolution below.
4884
5255
  if (call.localShadow && !call.resolvedName && !call.resolvedNames) {
4885
5256
  const localTarget = (index.symbols.get(call.name) || []).some(s =>
4886
- s.file === def.file && !NON_CALLABLE_TYPES.has(s.type) &&
5257
+ s.file === def.file &&
5258
+ (s.type === 'class' || !NON_CALLABLE_TYPES.has(s.type)) &&
4887
5259
  s.startLine >= def.startLine && s.endLine <= def.endLine &&
4888
5260
  s.startLine <= call.line);
4889
5261
  if (!localTarget) {
@@ -4899,6 +5271,18 @@ function findCallees(index, definition, options = {}) {
4899
5271
  const directReceiverFlow = mayNeedDirectReceiverFlow(call)
4900
5272
  ? _lookupReturnTypeFlow(flowMap(), call)
4901
5273
  : undefined;
5274
+ // Callee-side twin of the structural caller gate (#222(4)). A
5275
+ // local receiver whose nearest producer was examined but could
5276
+ // not be typed (`C2 = decorator(Base); value = C2()`) has unknown
5277
+ // runtime identity. Never let unique project-wide spelling turn
5278
+ // that into an exact callee; keep the site visible and conserved.
5279
+ if (collectAccount && mayNeedDirectReceiverFlow(call) &&
5280
+ !directReceiverFlow && _receiverAssignedUntyped(flowMap(), call)) {
5281
+ noteUnverified(siteId, call, 'possible-dispatch', {
5282
+ dispatchVia: 'local receiver',
5283
+ });
5284
+ continue;
5285
+ }
4902
5286
 
4903
5287
  // Declared-field receiver hop (fix #231 — callee-side parity
4904
5288
  // with the caller side's #202/#219): `tm.service.Save()` /
@@ -5551,8 +5935,18 @@ function findCallees(index, definition, options = {}) {
5551
5935
  const shadowsBuiltin = !call.receiver && (
5552
5936
  fileEntry?.importBindings?.some(b => b.name === call.name) ||
5553
5937
  fileEntry?.bindings?.some(b => b.name === call.name));
5554
- if (!selfShaped && !call.receiverIsModule && !shadowsBuiltin &&
5555
- index.isKeyword(call.name, language)) {
5938
+ // A method call on a receiver TYPED to a project class is
5939
+ // evidence for a project method, not the builtin (fix #300,
5940
+ // attrs callee arm: `c2.classmethod()` on constructor-typed
5941
+ // C2Slots was routed external because `classmethod` sits in the
5942
+ // Python builtin set — the #238 self-shaped exemption's typed-
5943
+ // receiver twin). The receiver-type routing below decides.
5944
+ const typedProjectReceiver = call.isMethod && call.receiverType &&
5945
+ typeof call.receiverType === 'string' &&
5946
+ (index.symbols.get(call.receiverType) || [])
5947
+ .some(d => IDENTITY_TYPE_KINDS.has(d.type));
5948
+ if (!selfShaped && !typedProjectReceiver && !call.receiverIsModule &&
5949
+ !shadowsBuiltin && index.isKeyword(call.name, language)) {
5556
5950
  noteSite(siteId, 'external', null, call);
5557
5951
  continue;
5558
5952
  }
@@ -5661,8 +6055,17 @@ function findCallees(index, definition, options = {}) {
5661
6055
  // name through the same #217 export-chain discipline used for
5662
6056
  // module receivers. Only intercept when an explicit binding of
5663
6057
  // this name exists; ordinary locals continue to lexical binding.
5664
- if (!call.isMethod && !call.receiver &&
5665
- langTraits(language)?.typeSystem === 'structural') {
6058
+ // fix #300 (itertools-measured): the gate covers every language
6059
+ // where a bare call cannot denote a method (#220(4)) — Rust
6060
+ // `use itertools::free::merge_join_by;` owns the bare name, and
6061
+ // canonical-order def selection was confirming the TRAIT METHOD
6062
+ // (lib.rs Itertools::merge_join_by) instead of the imported free
6063
+ // fn. Go item-level import bindings don't exist (package-
6064
+ // qualified imports only), so the route is a structural+Rust
6065
+ // change in practice; Java (bareCallReachesMethods) keeps its
6066
+ // implicit-this/static-import machinery.
6067
+ if (!call.isMethod && !call.receiver && !call.isConstructor &&
6068
+ !langTraits(language)?.bareCallReachesMethods) {
5666
6069
  const importRoute = _calleeStructuralImportedNameRoute(index, fileEntry, call, language);
5667
6070
  if (importRoute) {
5668
6071
  if (importRoute.matches?.length) {
@@ -5692,6 +6095,12 @@ function findCallees(index, definition, options = {}) {
5692
6095
  let isUncertain = call.uncertain;
5693
6096
  let uncertainReason = null; // account-mode reason for the unverified bucket
5694
6097
  let compilerResolvedBare = false;
6098
+ // A bare call can never bind a METHOD def in languages without
6099
+ // implicit-this (fix #300, #220(4) on the callee side) — drives
6100
+ // the bindings filter below and the finalization kind rule.
6101
+ const bareKindFiltered = !call.isMethod && !call.receiver &&
6102
+ !call.isConstructor &&
6103
+ !langTraits(language)?.bareCallReachesMethods;
5695
6104
  if (!call.bindingId && language === 'cpp' &&
5696
6105
  !call.isMethod && !call.receiver && !call.isConstructor) {
5697
6106
  // C++ namespace/free-function lookup is declaration-order
@@ -5764,6 +6173,16 @@ function findCallees(index, definition, options = {}) {
5764
6173
  if (call.isConstructor) {
5765
6174
  bindings = bindings.filter(binding => binding.type !== 'field');
5766
6175
  }
6176
+ // A bare call can never bind a METHOD def (fix #300, the
6177
+ // caller side's #220(4)/#222(3) filter brought to the callee
6178
+ // direction): inside Itertools::merge_join_by, the body's
6179
+ // `merge_join_by(self, other, cmp_fn)` bound the ENCLOSING
6180
+ // trait method through the file bindings table and confirmed
6181
+ // a self-edge — the compiler's target is the free fn. Java
6182
+ // (bareCallReachesMethods: implicit-this) keeps its bindings.
6183
+ if (bareKindFiltered) {
6184
+ bindings = bindings.filter(binding => binding.type !== 'method');
6185
+ }
5767
6186
  // Method call with no binding for the method name:
5768
6187
  // Different strategies by language family:
5769
6188
  if (bindings.length === 0 && call.isMethod) {
@@ -5775,7 +6194,12 @@ function findCallees(index, definition, options = {}) {
5775
6194
  // CacheService.set. Builtin-typed receivers are host calls;
5776
6195
  // a receiver typed to a project class resolves to that class
5777
6196
  // (or an ancestor defining the method), never by bare name.
5778
- const route = _calleeReceiverTypeRoute(index, call, localTypes, language);
6197
+ const route = _calleeReceiverTypeRoute(index, call, localTypes, language, {
6198
+ file: def.file,
6199
+ line: call.line,
6200
+ scopeStart: def.startLine,
6201
+ scopeEnd: def.endLine,
6202
+ });
5779
6203
  if (route?.external) {
5780
6204
  noteSite(siteId, 'external', null, call);
5781
6205
  continue;
@@ -6170,6 +6594,10 @@ function findCallees(index, definition, options = {}) {
6170
6594
  name: effectiveName,
6171
6595
  bindingId: bindingResolved,
6172
6596
  count: 1,
6597
+ // Name-keyed entries from bare calls carry the #220(4)
6598
+ // kind rule into finalization (fix #300): the resolver
6599
+ // there must never select a method-kind def for them.
6600
+ ...(bareKindFiltered && !bindingResolved && { bareNonMethod: true }),
6173
6601
  ...(call.isConstructor && { isConstructor: true }),
6174
6602
  ...(collectAccount && {
6175
6603
  sites: [call.line],
@@ -6356,14 +6784,24 @@ function findCallees(index, definition, options = {}) {
6356
6784
  const cFamilyVisibleFiles = ['c', 'cpp'].includes(language)
6357
6785
  ? _cppVisibleFiles(index, def.file) : null;
6358
6786
 
6359
- for (const { name: calleeName, bindingId, count, isConstructor, sites, siteIds, isFunctionReference } of callees.values()) {
6787
+ for (const { name: calleeName, bindingId, count, isConstructor, sites, siteIds, isFunctionReference, bareNonMethod } of callees.values()) {
6360
6788
  const claimSites = (bucket, reason) => {
6361
6789
  if (!collectAccount || !siteIds) return;
6362
6790
  for (let i = 0; i < siteIds.length; i++) {
6363
6791
  noteSite(siteIds[i], bucket, reason, { name: calleeName, line: sites[i] });
6364
6792
  }
6365
6793
  };
6366
- const symbols = index.symbols.get(calleeName);
6794
+ let symbols = index.symbols.get(calleeName);
6795
+ if (symbols && symbols.length > 0 && bareNonMethod) {
6796
+ // fix #300 (#220(4), callee finalization): a bare call's
6797
+ // candidate set never includes METHOD defs — without this the
6798
+ // same-file priority selected the enclosing trait method for
6799
+ // `merge_join_by(self, other, cmp_fn)` inside its own trait
6800
+ // wrapper, fabricating a self-edge over the imported free fn.
6801
+ const nonMethod = symbols.filter(s =>
6802
+ !(s.className || s.receiver) || NON_CALLABLE_TYPES.has(s.type));
6803
+ if (nonMethod.length > 0) symbols = nonMethod;
6804
+ }
6367
6805
  if (!symbols || symbols.length === 0) {
6368
6806
  // Name not in the symbol table — external library, builtin, or
6369
6807
  // unindexed code. Visible in the callee account, not an edge.
@@ -6793,6 +7231,22 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
6793
7231
  };
6794
7232
  for (const call of calls) {
6795
7233
  if (!call.assignedTo) continue;
7234
+ // For-loop iteration provenance (fix #294, flask-measured: `for ep in
7235
+ // importlib.metadata.entry_points(...)` — ep.load() confirmed
7236
+ // JSONProvider.load via single-owner while ep is an external
7237
+ // EntryPoint). The loop variable holds an ELEMENT of the producer's
7238
+ // result — the return annotation types the container, never the
7239
+ // element, so assignedIter records NEVER type positively. External
7240
+ // module-qualified producers stamp external provenance (the
7241
+ // #220(6)/#222(4) demote-only rail: blocks single-owner confirmation,
7242
+ // never excludes). Bare-call and untyped producers add nothing —
7243
+ // sorted()/filter() wrappers routinely yield project values, and
7244
+ // demoting those has no measured evidence.
7245
+ if (call.assignedIter) {
7246
+ const via = _iterExternalProducerVia(index, fileEntry, call);
7247
+ if (via) routeUnknownAssignment(call, via);
7248
+ continue;
7249
+ }
6796
7250
  const delegatedUnwrapAssignment = language === 'rust' &&
6797
7251
  call.receiverCall && calls.some(candidate =>
6798
7252
  candidate !== call &&
@@ -6889,7 +7343,13 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
6889
7343
  let returnType, fromFile, selfClass, returnedFunctionResult, returnDefinition;
6890
7344
  const builtinCallReturn = !nominal && language === 'python'
6891
7345
  ? _pythonBuiltinCallReturnType(index, fileEntry, call) : null;
6892
- if (builtinCallReturn) {
7346
+ if (call.localValueCall && call.returnTypeHint) {
7347
+ // Lexical function values are deliberately absent from the
7348
+ // project symbol table, but their declared result remains exact
7349
+ // local compiler evidence (Go: `f := func() (*A, *B)`).
7350
+ returnType = call.returnTypeHint;
7351
+ fromFile = filePath;
7352
+ } else if (builtinCallReturn) {
6893
7353
  returnType = builtinCallReturn;
6894
7354
  } else if (!nominal && language === 'python' && !call.isMethod &&
6895
7355
  !call.receiver && call.name === 'open' && !call.localShadow &&
@@ -7358,11 +7818,63 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
7358
7818
  });
7359
7819
  continue;
7360
7820
  }
7821
+ // Go multi-return assignments have one compiler-declared type per
7822
+ // LHS position. Record every named target, including assignments
7823
+ // whose first position is `_`; treating only element zero left later
7824
+ // receivers permanently ambiguous (`route, router := split(...)`).
7825
+ if (language === 'go' && call.assignedTuple &&
7826
+ Array.isArray(call.assignedTupleTargets) &&
7827
+ call.assignedTupleTargets.length > 0) {
7828
+ const scope = call.enclosingFunction
7829
+ ? `${call.enclosingFunction.startLine}` : '';
7830
+ if (!map) map = new Map();
7831
+ for (const target of call.assignedTupleTargets) {
7832
+ const key = `${scope}:${target.name}`;
7833
+ if (!map.has(key)) map.set(key, []);
7834
+ const base = { line: call.line, start: call.callStart };
7835
+ const parsed = _returnTypeNameNominal(returnType, language, {
7836
+ tuple: true,
7837
+ tupleIndex: target.index,
7838
+ selfClass,
7839
+ index,
7840
+ originFile: fromFile || filePath,
7841
+ });
7842
+ if (!parsed) {
7843
+ const uncertainVia = _rejectedNominalFlowVia(
7844
+ returnType, language, {
7845
+ tuple: true,
7846
+ tupleIndex: target.index,
7847
+ });
7848
+ map.get(key).push(uncertainVia
7849
+ ? { ...base, externalVia: uncertainVia }
7850
+ : { ...base, invalidated: true });
7851
+ continue;
7852
+ }
7853
+ if (parsed.qualifier) {
7854
+ const producerEntry =
7855
+ index.files.get(fromFile || filePath) || fileEntry;
7856
+ const qualified = _goQualifiedReceiverType(
7857
+ index, producerEntry, parsed.qualifier, parsed.name);
7858
+ if (qualified && qualified.kind !== 'project') {
7859
+ map.get(key).push({ ...base, externalVia: qualified.via });
7860
+ continue;
7861
+ }
7862
+ }
7863
+ const origin = _resolveFlowTypeOrigin(
7864
+ index, fromFile || filePath, parsed.name, parsed.qualifier);
7865
+ map.get(key).push(origin?.fromFile
7866
+ ? { ...base, type: parsed.name, fromFile: origin.fromFile }
7867
+ : { ...base, invalidated: true });
7868
+ }
7869
+ continue;
7870
+ }
7871
+
7361
7872
  let typeName, entryFromFile;
7362
7873
  if (nominal) {
7363
7874
  const parsed = _returnTypeNameNominal(returnType, language, {
7364
7875
  unwrapped: call.assignedUnwrap,
7365
7876
  tuple: call.assignedTuple,
7877
+ tupleIndex: call.assignedTupleIndex,
7366
7878
  selfClass,
7367
7879
  index,
7368
7880
  originFile: fromFile || filePath,
@@ -7676,8 +8188,11 @@ function _returnTypeNameNominal(text, language, opts = {}) {
7676
8188
  if (!opts.tuple) return undefined;
7677
8189
  const inner = t.slice(1, -1);
7678
8190
  if (inner.includes('func(') || inner.includes('func (')) return undefined;
7679
- const first = inner.split(',')[0].trim();
7680
- const parts = first.split(/\s+/);
8191
+ const position = Number.isInteger(opts.tupleIndex)
8192
+ ? opts.tupleIndex : 0;
8193
+ const item = _splitTopLevelGenericArgs(inner)[position]?.trim();
8194
+ if (!item) return undefined;
8195
+ const parts = item.split(/\s+/);
7681
8196
  t = parts[parts.length - 1]; // named return `n int` → int
7682
8197
  } else if (opts.tuple) {
7683
8198
  return undefined; // v, err := f() needs a multi-return producer
@@ -7768,8 +8283,10 @@ function _rejectedNominalFlowVia(text, language, opts = {}) {
7768
8283
  let raw = text.trim();
7769
8284
  if (language === 'go' && raw.startsWith('(')) {
7770
8285
  if (!opts.tuple) return null;
7771
- const first = raw.slice(1, -1).split(',')[0]?.trim();
7772
- raw = first ? first.split(/\s+/).pop() : '';
8286
+ const position = Number.isInteger(opts.tupleIndex)
8287
+ ? opts.tupleIndex : 0;
8288
+ const item = _splitTopLevelGenericArgs(raw.slice(1, -1))[position]?.trim();
8289
+ raw = item ? item.split(/\s+/).pop() : '';
7773
8290
  }
7774
8291
  const norm = _normalizeFieldTypeName(raw, language);
7775
8292
  if (!norm) return null;
@@ -7820,6 +8337,51 @@ function _rustBindingResolvedFiles(index, fileEntry, filePath, binding) {
7820
8337
  return resolvedFiles;
7821
8338
  }
7822
8339
 
8340
+ /**
8341
+ * Resolve a Rust `use path::Type as LocalType` alias to the indexed type it
8342
+ * names. Import aliases are value/type names, not Rust `type` declarations,
8343
+ * so they do not appear in the symbol table and cannot use `_pureAliasBase`.
8344
+ * Require every same-local-name binding to resolve to one identical type/file;
8345
+ * a resolver gap or scope collision remains unknown rather than guessed.
8346
+ */
8347
+ function _rustImportedTypeIdentity(index, filePath, localName) {
8348
+ const fileEntry = index.files.get(filePath);
8349
+ if (fileEntry?.language !== 'rust') return null;
8350
+ const bindings = (fileEntry.importBindings || []).filter(binding =>
8351
+ (binding.name === localName || binding.alias === localName) &&
8352
+ String(binding.module || '').includes('::'));
8353
+ if (bindings.length === 0) return null;
8354
+ const identities = [];
8355
+ for (const binding of bindings) {
8356
+ const parts = String(binding.module).split('::').filter(Boolean);
8357
+ const original = parts[parts.length - 1];
8358
+ if (!original || original === localName) return null;
8359
+ const definitions = (index.symbols.get(original) || []).filter(definition =>
8360
+ IDENTITY_TYPE_KINDS.has(definition.type) && definition.file);
8361
+ if (definitions.length === 0) return null;
8362
+ const starts = _rustBindingResolvedFiles(
8363
+ index, fileEntry, filePath, binding);
8364
+ if (starts.size === 0) return null;
8365
+ let reachable = definitions.filter(definition => [...starts].some(start =>
8366
+ definition.file === start ||
8367
+ _nameBindingReaches(
8368
+ index, start, original, new Set([definition.file])) === 'yes'));
8369
+ if (reachable.length === 0) {
8370
+ reachable = definitions.filter(definition => [...starts].some(start =>
8371
+ definition.file === start ||
8372
+ _importReaches(index, start, new Set([definition.file]))));
8373
+ }
8374
+ const files = new Set(reachable.map(definition => definition.file));
8375
+ if (files.size !== 1) return null;
8376
+ identities.push({ type: original, fromFile: [...files][0] });
8377
+ }
8378
+ if (new Set(identities.map(identity => identity.type)).size !== 1 ||
8379
+ new Set(identities.map(identity => identity.fromFile)).size !== 1) {
8380
+ return null;
8381
+ }
8382
+ return identities[0];
8383
+ }
8384
+
7823
8385
  /**
7824
8386
  * Pin a flow type name to its defining file from the PRODUCER's scope
7825
8387
  * (fix #207 — the #206 identity lesson applied to annotations: `Builder` in
@@ -7834,7 +8396,7 @@ function _rustBindingResolvedFiles(index, fileEntry, filePath, binding) {
7834
8396
  * trusted (a use/import of an external type can shadow it invisibly)
7835
8397
  * - no project type def at all: external name — safe, can't conflate
7836
8398
  */
7837
- function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
8399
+ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier = undefined) {
7838
8400
  const opCache = index._opFlowTypeOriginCache;
7839
8401
  const cacheKey = `${producerFile}\x00${typeName}\x00${qualifier || ''}`;
7840
8402
  if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
@@ -7898,6 +8460,36 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
7898
8460
  }
7899
8461
  }
7900
8462
  }
8463
+ // Structural module qualifier (fix #286e, flask-measured):
8464
+ // `app: flask.Flask` names the type through the imported module.
8465
+ // Resolve the module binding and pin the unique reachable
8466
+ // declaration — name-chase through modeled re-exports first, the
8467
+ // bounded file-level closure as fallback (the #277 rails). An
8468
+ // unresolvable qualifier is 'unknown', never proximity-guessed.
8469
+ if (fe && ['python', 'javascript', 'typescript', 'tsx'].includes(fe.language)) {
8470
+ const resolvedRels = new Set();
8471
+ const direct = fe.moduleResolved?.[qualifier];
8472
+ if (direct) resolvedRels.add(direct);
8473
+ for (const b of (fe.importBindings || [])) {
8474
+ if (b.name !== qualifier && b.alias !== qualifier) continue;
8475
+ const rel = fe.moduleResolved?.[b.module];
8476
+ if (rel) resolvedRels.add(rel);
8477
+ }
8478
+ for (const rel of resolvedRels) {
8479
+ const start = path.join(index.root, rel);
8480
+ const named = typeDefs.filter(d => d.file === start ||
8481
+ _nameBindingReaches(index, start, typeName, new Set([d.file])) === 'yes');
8482
+ if (new Set(named.map(d => d.file)).size === 1) {
8483
+ return finish({ fromFile: named[0].file });
8484
+ }
8485
+ const reachable = typeDefs.filter(d => d.file === start ||
8486
+ _importReaches(index, start, new Set([d.file])));
8487
+ if (new Set(reachable.map(d => d.file)).size === 1) {
8488
+ return finish({ fromFile: reachable[0].file });
8489
+ }
8490
+ }
8491
+ return finish(null);
8492
+ }
7901
8493
  const inPkg = fe && _qualifiedProducerDefs(index, fe, qualifier, typeDefs);
7902
8494
  if (inPkg && inPkg.length > 0 && new Set(inPkg.map(d => d.file)).size === 1) {
7903
8495
  return finish({ fromFile: inPkg[0].file });
@@ -8276,12 +8868,24 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
8276
8868
  // makes widgets.helper the local helper even if widgets imports a
8277
8869
  // different helper several hops below. Parser-provided localName
8278
8870
  // keeps this proof limited to syntactically-owned CJS exports;
8279
- // dynamic assignments remain unknown.
8871
+ // dynamic assignments remain unknown — including a COMPETING
8872
+ // record for the same name without static ownership
8873
+ // (`exports.run = run;` then `if (native) exports.run =
8874
+ // require('./native').run;`) and a whole-surface reassignment
8875
+ // (`module.exports = require('./impl')`): either one can rebind
8876
+ // the member at runtime, so the static record alone never proves
8877
+ // a dead end (fix #292).
8280
8878
  const localExports = (fe.exportDetails || []).filter(e =>
8281
- !e.source && (e.alias || e.name) === attr && e.localName);
8282
- if (localExports.some(e => (index.symbols.get(e.localName) || [])
8283
- .some(definition => definition.file === abs &&
8284
- !NON_CALLABLE_TYPES.has(definition.type)))) {
8879
+ !e.source && (e.alias || e.name) === attr);
8880
+ const staticOwner = localExports.some(e => e.localName &&
8881
+ (index.symbols.get(e.localName) || [])
8882
+ .some(definition => definition.file === abs &&
8883
+ !NON_CALLABLE_TYPES.has(definition.type)));
8884
+ const competingDynamic = localExports.some(e => !e.localName) ||
8885
+ (fe.exportDetails || []).some(e => !e.source &&
8886
+ e.defaultLike && e.type === 'module.exports' &&
8887
+ (e.alias || e.name) !== attr);
8888
+ if (staticOwner && !competingDynamic) {
8285
8889
  return 'no';
8286
8890
  }
8287
8891
 
@@ -8343,6 +8947,256 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
8343
8947
  return unknown ? 'unknown' : 'no';
8344
8948
  }
8345
8949
 
8950
+ /**
8951
+ * A CommonJS object export whose requested member has no statically-owned
8952
+ * local value cannot prove callable identity. For example,
8953
+ * `module.exports = { run: available ? nativeRun : fallback }` exposes the
8954
+ * local fallback only on one runtime branch. File-level import reachability
8955
+ * must not upgrade that conditional value to a confirmed edge.
8956
+ */
8957
+ function _cjsMemberOwnershipAmbiguous(index, file, memberName) {
8958
+ const fe = index.files.get(file);
8959
+ // Both CJS assignment families count: `module.exports = { x: cond ? a : b }`
8960
+ // records type 'module.exports'; `exports.x = <dynamic>` records type
8961
+ // 'exports' (fix #292 — the sequential native/fallback feature-detect idiom).
8962
+ return !!fe && (fe.exportDetails || []).some(exp =>
8963
+ (exp.type === 'module.exports' || exp.type === 'exports') &&
8964
+ !exp.defaultLike && !exp.source &&
8965
+ (exp.alias || exp.name) === memberName && !exp.localName);
8966
+ }
8967
+
8968
+ /**
8969
+ * Resolve a member reached through an exported ESM namespace object.
8970
+ *
8971
+ * Supported exact shapes:
8972
+ * import * as api from './impl'; export { api };
8973
+ * import * as api from './impl'; export default api;
8974
+ * export * as api from './impl';
8975
+ * export { api } from './barrel'; // recursively, when api is one above
8976
+ *
8977
+ * A plain exported object/value is deliberately not recognized. The caller
8978
+ * must remain in the ordinary structural receiver tier unless the export path
8979
+ * proves that the value is a module namespace exotic object.
8980
+ *
8981
+ * @returns {{ verdict: 'yes'|'no'|'unknown' }|null}
8982
+ */
8983
+ function _namespaceExportMemberReaches(
8984
+ index, startAbs, exportedName, memberName, targetFiles, maxDepth = 4,
8985
+ visited = new Set(), memberPath = []
8986
+ ) {
8987
+ if (maxDepth < 0) return { verdict: 'unknown' };
8988
+ const stateKey = `${startAbs}\x00${exportedName}\x00${memberPath.join('.')}\x00${memberName}`;
8989
+ if (visited.has(stateKey)) return { verdict: 'unknown' };
8990
+ visited.add(stateKey);
8991
+ const fe = index.files.get(startAbs);
8992
+ if (!fe) return null;
8993
+
8994
+ let recognized = false;
8995
+ let unknown = false;
8996
+ let explicitExport = false;
8997
+ const followMember = moduleName => {
8998
+ const rel = fe.moduleResolved?.[moduleName];
8999
+ if (!rel) {
9000
+ unknown = true;
9001
+ return 'unknown';
9002
+ }
9003
+ const nextAbs = path.join(index.root, rel);
9004
+ if (memberPath.length > 0) {
9005
+ const nested = _namespaceExportMemberReaches(
9006
+ index, nextAbs, memberPath[0], memberName, targetFiles,
9007
+ maxDepth - 1, new Set(visited), memberPath.slice(1));
9008
+ // The outer value is proven to be a namespace, but its requested
9009
+ // field is an ordinary/unmodeled value rather than another proven
9010
+ // namespace. That is uncertainty, never exclusion evidence.
9011
+ return nested?.verdict || 'unknown';
9012
+ }
9013
+ return _nameBindingReaches(index, nextAbs, memberName, targetFiles, maxDepth - 1);
9014
+ };
9015
+ const absorb = verdict => {
9016
+ if (verdict === 'yes') return true;
9017
+ if (verdict === 'unknown') unknown = true;
9018
+ return false;
9019
+ };
9020
+
9021
+ for (const exp of (fe.exportDetails || [])) {
9022
+ // `export * as api from './impl'` is direct namespace identity.
9023
+ if (exp.type === 're-export-all' && exp.alias === exportedName) {
9024
+ explicitExport = true;
9025
+ recognized = true;
9026
+ if (absorb(followMember(exp.source))) return { verdict: 'yes' };
9027
+ continue;
9028
+ }
9029
+
9030
+ const exposed = exp.type === 'default' ? 'default' : (exp.alias || exp.name);
9031
+ if (exposed !== exportedName) continue;
9032
+ explicitExport = true;
9033
+
9034
+ // `export { api } from './barrel'`: the source-side value may itself
9035
+ // be a namespace export. Recurse under its source-side name.
9036
+ if (exp.type === 're-export' && exp.source) {
9037
+ const rel = fe.moduleResolved?.[exp.source];
9038
+ if (!rel) {
9039
+ unknown = true;
9040
+ recognized = true;
9041
+ continue;
9042
+ }
9043
+ const nested = _namespaceExportMemberReaches(
9044
+ index, path.join(index.root, rel), exp.name, memberName,
9045
+ targetFiles, maxDepth - 1, new Set(visited), memberPath);
9046
+ if (nested) {
9047
+ recognized = true;
9048
+ if (nested.verdict === 'yes') return { verdict: 'yes' };
9049
+ if (nested.verdict === 'unknown') unknown = true;
9050
+ }
9051
+ continue;
9052
+ }
9053
+
9054
+ // `import * as api ...; export { api }` / `export default api`.
9055
+ if (!exp.source && (exp.type === 'named' || exp.type === 'default')) {
9056
+ const localName = exp.name;
9057
+ const namespaceBindings = (fe.importBindings || []).filter(binding =>
9058
+ (binding.alias || binding.name) === localName && binding.kind === 'namespace');
9059
+ for (const binding of namespaceBindings) {
9060
+ recognized = true;
9061
+ if (absorb(followMember(binding.module))) return { verdict: 'yes' };
9062
+ }
9063
+ }
9064
+ }
9065
+
9066
+ // `export * from './barrel'` forwards named namespace-object exports too.
9067
+ // Explicit local/named exports shadow star exports, so only chase stars
9068
+ // when this file has no explicit surface for the requested name. Default
9069
+ // is never forwarded by export-star.
9070
+ if (!explicitExport && exportedName !== 'default') {
9071
+ const starExports = (fe.exportDetails || []).filter(exp =>
9072
+ exp.type === 're-export-all' && !exp.alias && exp.source);
9073
+ const starVerdicts = [];
9074
+ for (const exp of starExports) {
9075
+ const rel = fe.moduleResolved?.[exp.source];
9076
+ if (!rel) {
9077
+ unknown = true;
9078
+ continue;
9079
+ }
9080
+ const nested = _namespaceExportMemberReaches(
9081
+ index, path.join(index.root, rel), exportedName, memberName,
9082
+ targetFiles, maxDepth - 1, new Set(visited), memberPath);
9083
+ if (!nested) continue;
9084
+ recognized = true;
9085
+ starVerdicts.push(nested.verdict);
9086
+ }
9087
+ if (starVerdicts.length > 0) {
9088
+ // With several export-star providers, another star may expose the
9089
+ // same name and make the ESM binding ambiguous. We currently do
9090
+ // not compute full `ResolveExport` sets, so multi-star barrels are
9091
+ // demotion-only even when one path reaches the target.
9092
+ if (starExports.length > 1) unknown = true;
9093
+ else if (starVerdicts.includes('yes')) return { verdict: 'yes' };
9094
+ if (starVerdicts.includes('unknown')) unknown = true;
9095
+ }
9096
+ }
9097
+
9098
+ if (!recognized) return null;
9099
+ return { verdict: unknown ? 'unknown' : 'no' };
9100
+ }
9101
+
9102
+ /**
9103
+ * Determine whether a structural receiver imported by name/default is a
9104
+ * statically exported namespace object, and if so whether its requested member
9105
+ * can reach the pinned target files. Multiple live bindings must agree before
9106
+ * a negative becomes exclusion-grade.
9107
+ */
9108
+ function _importedNamespaceMemberOwnership(index, fileEntry, call, targetFiles) {
9109
+ const receiver = call.receiver || call.receiverRoot;
9110
+ const memberPath = call.receiver
9111
+ ? [] : (call.receiverFields || (call.receiverField ? [call.receiverField] : []));
9112
+ const bindings = (fileEntry.importBindings || []).filter(binding =>
9113
+ (binding.alias || binding.name) === receiver &&
9114
+ (binding.kind === 'named' || binding.kind === 'default' || binding.kind === 'namespace'));
9115
+ if (bindings.length === 0) return null;
9116
+
9117
+ let recognized = 0;
9118
+ let unknown = false;
9119
+ for (const binding of bindings) {
9120
+ const rel = fileEntry.moduleResolved?.[binding.module];
9121
+ if (!rel) continue;
9122
+ const startAbs = path.join(index.root, rel);
9123
+ let result;
9124
+ if (binding.kind === 'namespace') {
9125
+ result = memberPath.length > 0
9126
+ ? _namespaceExportMemberReaches(
9127
+ index, startAbs, memberPath[0], call.name, targetFiles,
9128
+ 4, new Set(), memberPath.slice(1))
9129
+ : { verdict: _nameBindingReaches(index, startAbs, call.name, targetFiles) };
9130
+ } else {
9131
+ const exportedName = binding.kind === 'default' ? 'default' : binding.name;
9132
+ result = _namespaceExportMemberReaches(
9133
+ index, startAbs, exportedName, call.name, targetFiles,
9134
+ 4, new Set(), memberPath);
9135
+ }
9136
+ if (!result) continue;
9137
+ recognized++;
9138
+ if (result.verdict === 'yes') return { verdict: 'yes' };
9139
+ if (result.verdict === 'unknown') unknown = true;
9140
+ }
9141
+ if (recognized === 0) return null;
9142
+ if (recognized !== bindings.length) unknown = true;
9143
+ return { verdict: unknown ? 'unknown' : 'no' };
9144
+ }
9145
+
9146
+ /**
9147
+ * Ownership chase for a CommonJS default-like require binding:
9148
+ * `const local = require('./module')`. The local binding name says nothing
9149
+ * about the exporting file's symbol; the direct `module.exports = value`
9150
+ * record does. A locally defined callable is a definitive dead end for a
9151
+ * target in another file, an imported value is chased, and dynamic values
9152
+ * remain unknown. This is exclusion-grade only when every live path is known.
9153
+ */
9154
+ function _defaultBindingReaches(index, startAbs, targetFiles, maxDepth = 4, visited = new Set()) {
9155
+ if (targetFiles.has(startAbs)) return 'yes';
9156
+ if (maxDepth < 0 || visited.has(startAbs)) return 'unknown';
9157
+ visited.add(startAbs);
9158
+ const fe = index.files.get(startAbs);
9159
+ if (!fe) return 'unknown';
9160
+
9161
+ const defaults = (fe.exportDetails || []).filter(exp =>
9162
+ exp.type === 'module.exports' && exp.defaultLike);
9163
+ if (defaults.length === 0) return 'unknown';
9164
+
9165
+ let unknown = false;
9166
+ for (const exp of defaults) {
9167
+ const localName = exp.localName || exp.name;
9168
+ if (!localName) { unknown = true; continue; }
9169
+ // Without parser-proven syntactic ownership (localName), the record's
9170
+ // name falls back to the synthesized 'default' — which a DYNAMIC
9171
+ // reassignment (`module.exports = require('./impl')`) shares with an
9172
+ // earlier anonymous default. Only a local callable declared AT the
9173
+ // record's own line proves that this record is the local value
9174
+ // (fix #292 — a name-only match must never dead-end the live import
9175
+ // path of the competing dynamic record).
9176
+ const localCallable = (index.symbols.get(localName) || []).some(definition =>
9177
+ definition.file === startAbs && !NON_CALLABLE_TYPES.has(definition.type) &&
9178
+ (exp.localName || definition.startLine === exp.line ||
9179
+ (definition.startLine <= exp.line && definition.endLine >= exp.line)));
9180
+ if (localCallable) continue;
9181
+
9182
+ const bindings = (fe.importBindings || []).filter(binding =>
9183
+ binding.name === localName || binding.alias === localName);
9184
+ if (bindings.length === 0) { unknown = true; continue; }
9185
+ for (const binding of bindings) {
9186
+ const rel = fe.moduleResolved?.[binding.module];
9187
+ if (!rel) { unknown = true; continue; }
9188
+ const nextAbs = path.join(index.root, rel);
9189
+ const verdict = binding.defaultLike
9190
+ ? _defaultBindingReaches(
9191
+ index, nextAbs, targetFiles, maxDepth - 1, new Set(visited))
9192
+ : _nameBindingReaches(index, nextAbs, binding.name, targetFiles, maxDepth - 1);
9193
+ if (verdict === 'yes') return 'yes';
9194
+ if (verdict === 'unknown') unknown = true;
9195
+ }
9196
+ }
9197
+ return unknown ? 'unknown' : 'no';
9198
+ }
9199
+
8346
9200
  /**
8347
9201
  * From-import submodule receivers (fix #224): `from . import jobs` binds
8348
9202
  * jobs.py as a plain NAME — the parser can't mark it a module alias (a
@@ -8365,6 +9219,58 @@ function _submoduleReceiverModule(index, fileEntry, receiverName) {
8365
9219
  return null;
8366
9220
  }
8367
9221
 
9222
+ /**
9223
+ * Does a module-qualified member reference (`pkg.name`) reach the pinned
9224
+ * definition files? This is the value-position counterpart of structural
9225
+ * module-call routing. It deliberately accepts only parser-proven module
9226
+ * imports (or Python from-imports resolved as actual submodules), so an
9227
+ * imported class/value named `pkg` cannot borrow a module's exports.
9228
+ *
9229
+ * Returns `yes`, `no`, `unknown`, or null when the receiver is not a modeled
9230
+ * module binding.
9231
+ */
9232
+ function _moduleAttributeBindingReaches(index, filePath, receiverName, name,
9233
+ targetFiles, maxDepth = 8) {
9234
+ const fileEntry = index.files.get(filePath);
9235
+ if (!fileEntry || !receiverName || !name ||
9236
+ langTraits(fileEntry.language)?.typeSystem !== 'structural') return null;
9237
+
9238
+ const starts = new Set();
9239
+ let matchedBinding = false;
9240
+ let unknown = false;
9241
+ const submoduleRel = _submoduleReceiverModule(index, fileEntry, receiverName);
9242
+ if (submoduleRel) {
9243
+ matchedBinding = true;
9244
+ starts.add(path.isAbsolute(submoduleRel)
9245
+ ? submoduleRel : path.join(index.root, submoduleRel));
9246
+ }
9247
+
9248
+ for (const binding of (fileEntry.importBindings || [])) {
9249
+ if (binding.name !== receiverName && binding.alias !== receiverName) continue;
9250
+ // Python `import pkg [as p]` is a module binding. A `from pkg import
9251
+ // Value` binding is not, unless _submoduleReceiverModule proved that
9252
+ // Value is itself a project submodule above.
9253
+ if (fileEntry.language === 'python' && binding.kind !== 'import') continue;
9254
+ matchedBinding = true;
9255
+ const rel = fileEntry.moduleResolved?.[binding.module];
9256
+ if (!rel) {
9257
+ unknown = true;
9258
+ continue;
9259
+ }
9260
+ starts.add(path.isAbsolute(rel) ? rel : path.join(index.root, rel));
9261
+ }
9262
+
9263
+ if (!matchedBinding) return null;
9264
+ for (const start of starts) {
9265
+ const verdict = _nameBindingReaches(
9266
+ index, start, name, targetFiles, maxDepth);
9267
+ if (verdict === 'yes') return 'yes';
9268
+ if (verdict === 'unknown') unknown = true;
9269
+ }
9270
+ if (unknown || starts.size === 0) return 'unknown';
9271
+ return 'no';
9272
+ }
9273
+
8368
9274
  /**
8369
9275
  * Bounded-depth reachability over the import graph: can `fromAbs` reach any
8370
9276
  * target file through re-export/import chains? Barrel hierarchies routinely
@@ -8492,6 +9398,30 @@ function _isEnclosingGenericParam(index, filePath, line, typeName) {
8492
9398
  return false;
8493
9399
  }
8494
9400
 
9401
+ /**
9402
+ * Generic receivers normally have runtime-unknown ownership. For a trait
9403
+ * declaration pin, an explicit `F: Float` bound is nevertheless exact
9404
+ * compiler evidence that `f.exponent()` invokes Float's method slot (not any
9405
+ * one concrete implementation). Resolve the bound to the pinned trait file;
9406
+ * a same-named external or sibling trait remains unknown.
9407
+ */
9408
+ function _genericParamTraitTarget(index, filePath, line, typeName, targetDefs) {
9409
+ const enclosing = index.findEnclosingFunction(filePath, line, true);
9410
+ const bounds = enclosing?.genericBounds?.[typeName];
9411
+ if (!Array.isArray(bounds) || bounds.length === 0) return null;
9412
+ for (const target of targetDefs) {
9413
+ const owner = target.className ||
9414
+ (target.receiver || '').replace(/^[*&]\s*/, '');
9415
+ if (!owner || !bounds.includes(owner)) continue;
9416
+ const ownsTraitDeclaration = (index.symbols.get(owner) || []).some(definition =>
9417
+ definition.type === 'trait' && definition.file === target.file);
9418
+ if (!ownsTraitDeclaration) continue;
9419
+ const origin = _resolveFlowTypeOrigin(index, filePath, owner);
9420
+ if (origin?.fromFile === target.file) return owner;
9421
+ }
9422
+ return null;
9423
+ }
9424
+
8495
9425
  /**
8496
9426
  * Receiver-type identity guard shared by the parser-typed branch and the
8497
9427
  * local-inference fallback: a name is NOT usable as type identity when it is
@@ -8688,8 +9618,30 @@ function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs, li
8688
9618
  * CustomCommand extends click.Command), while parallel package versions may
8689
9619
  * reuse every class name (zod v3/v4 ZodArray -> ZodType).
8690
9620
  */
8691
- function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, targetDefs) {
8692
- const origin = _resolveFlowTypeOrigin(index, originFile, knownType);
9621
+ /**
9622
+ * Scope-correct same-file type-def selection (fix #300, attrs-measured):
9623
+ * when a type name has SEVERAL class-kind defs in one file (each test
9624
+ * function defining its own local `class C2Slots(...)`), the def the call
9625
+ * site actually sees is the one declared inside the call's own enclosing
9626
+ * function before the call line — Python/JS lexical scoping. Returns that
9627
+ * def, or null when the shape doesn't apply (single def, no scope info, no
9628
+ * local declaration in range) — null means "keep existing behavior".
9629
+ */
9630
+ function _scopedSameFileTypeDef(index, name, file, site) {
9631
+ if (!site || site.line == null ||
9632
+ site.scopeStart == null || site.scopeEnd == null) return null;
9633
+ const defs = (index.symbols.get(name) || []).filter(d =>
9634
+ IDENTITY_TYPE_KINDS.has(d.type) && d.file === file);
9635
+ if (defs.length <= 1) return null;
9636
+ const scoped = defs.filter(d =>
9637
+ d.startLine >= site.scopeStart && d.startLine <= site.scopeEnd &&
9638
+ d.startLine <= site.line);
9639
+ if (scoped.length === 0) return null;
9640
+ return scoped.reduce((a, b) => (b.startLine > a.startLine ? b : a));
9641
+ }
9642
+
9643
+ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, targetDefs, qualifier, site) {
9644
+ const origin = _resolveFlowTypeOrigin(index, originFile, knownType, qualifier);
8693
9645
  if (!origin?.fromFile) return 'unknown';
8694
9646
  const targetOwners = new Set(targetDefs
8695
9647
  .map(d => d.className || (d.receiver && d.receiver.replace(/^\*/, '')))
@@ -8761,7 +9713,16 @@ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, target
8761
9713
  return { name: parent, file: parentFile };
8762
9714
  };
8763
9715
 
8764
- const queue = [{ name: knownType, file: origin.fromFile }];
9716
+ // Scope-correct first hop (fix #300): when the receiver's type name has
9717
+ // several same-file class defs, seed the walk with the def the call site
9718
+ // lexically sees — its per-def extends entry, never a sibling def's.
9719
+ // Guard: the site's line ranges describe the CALL file — they only
9720
+ // select defs when the type actually resolves there (an imported type's
9721
+ // defining file has unrelated line geometry).
9722
+ const scopedFirst = (site && site.file === origin.fromFile)
9723
+ ? _scopedSameFileTypeDef(index, knownType, origin.fromFile, site) : null;
9724
+ const queue = [{ name: knownType, file: origin.fromFile,
9725
+ defStartLine: scopedFirst?.startLine }];
8765
9726
  const visited = new Set();
8766
9727
  let sawUnresolved = direct === 'unknown';
8767
9728
  while (queue.length > 0 && visited.size < 128) {
@@ -8773,7 +9734,9 @@ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, target
8773
9734
  // ancestor slot. In that case ancestry is evidence for the other
8774
9735
  // definition, not the pinned one.
8775
9736
  if (overridesPinnedSlot(cur.name, cur.file)) continue;
8776
- const parents = index._getInheritanceParents(cur.name, cur.file) || [];
9737
+ const parents = cur.defStartLine != null
9738
+ ? (index._getInheritanceParentsAt?.(cur.name, cur.file, cur.defStartLine) || [])
9739
+ : (index._getInheritanceParents(cur.name, cur.file) || []);
8777
9740
  for (const parent of parents) {
8778
9741
  const edge = parentOrigin(parent, cur.file);
8779
9742
  const verdict = ownerIdentity(edge.name, edge.file);
@@ -8786,6 +9749,50 @@ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, target
8786
9749
  return sawUnresolved ? 'unknown' : 'other';
8787
9750
  }
8788
9751
 
9752
+ /**
9753
+ * Trait-declaration pin gate (fix #296, serde-as_cast-measured): when the
9754
+ * pinned target is a TRAIT's own method declaration, a path call on a
9755
+ * concrete NON-target receiver (`u64::as_cast`, `Limb::as_cast`,
9756
+ * `Self::Unsigned::as_cast`) is not evidence against the edge — any type
9757
+ * (external primitives, macro-generated impls UCN cannot index, aliases)
9758
+ * can implement a project trait, and the call dispatches through the pinned
9759
+ * slot. Returns the trait's name when the gate applies: the caller routes
9760
+ * possible-dispatch "via <Recv> — trait implementor" instead of excluding
9761
+ * path-type-mismatch. Two refusals keep the exclusion sound elsewhere:
9762
+ * impl-member pins (a foreign-type path call binds a DIFFERENT slot member
9763
+ * — static dispatch, exclusion correct), and receivers resolving to a
9764
+ * project type whose only same-name members provably belong to another
9765
+ * surface (inherent members / other traits — Rust resolves inherent first).
9766
+ */
9767
+ function _traitDeclPinImplementorRoute(index, fileEntry, receiverSegment, targetDefs) {
9768
+ if (langTraits(fileEntry.language)?.typeQualifiedCallStyle !== 'path') return null;
9769
+ let traitName = null;
9770
+ for (const td of targetDefs || []) {
9771
+ if (!td.className || td.traitName) continue;
9772
+ const classDefs = index.symbols.get(td.className) || [];
9773
+ if (classDefs.some(d => d.type === 'trait' && d.file === td.file)) {
9774
+ traitName = td.className;
9775
+ break;
9776
+ }
9777
+ }
9778
+ if (!traitName) return null;
9779
+ // Receiver resolving to a project type: its indexed same-name members
9780
+ // decide. An inherent member or a different trait's impl owns the call
9781
+ // (keep the exclusion); a member implementing THIS trait — or no indexed
9782
+ // member at all (macro-generated impls are invisible) — routes visible.
9783
+ const recvTypeDefs = (index.symbols.get(receiverSegment) || [])
9784
+ .filter(d => IDENTITY_TYPE_KINDS.has(d.type));
9785
+ if (recvTypeDefs.length > 0) {
9786
+ const targetName = targetDefs[0] && targetDefs[0].name;
9787
+ const members = (index.symbols.get(targetName) || [])
9788
+ .filter(d => d.className === receiverSegment);
9789
+ if (members.length > 0 && members.every(m => m.traitName !== traitName)) {
9790
+ return null;
9791
+ }
9792
+ }
9793
+ return traitName;
9794
+ }
9795
+
8789
9796
  /**
8790
9797
  * Is typeName an ancestor (transitively) of any target definition's class?
8791
9798
  * Used by receiver-class disambiguation: a receiver typed as a SUPERTYPE of
@@ -9017,6 +10024,38 @@ function _closeCallableIdentityGroup(index, targetDefs, definitions) {
9017
10024
  expanded.push(candidate);
9018
10025
  }
9019
10026
  }
10027
+
10028
+ // C++ full specializations (`template <> bool f<bool>(...)`) are the SAME
10029
+ // compiler symbol as their primary template: name lookup finds the
10030
+ // template, and the specialization is selected by substitution, never by
10031
+ // overload resolution (fix #299). Closure requires exactly ONE primary
10032
+ // template with the owner identity — with several primaries the compiler
10033
+ // matches the specialization by signature substitution, which is not
10034
+ // grep-reliability evidence, so the group refuses and sites stay visible.
10035
+ for (const target of targetDefs) {
10036
+ if (cFamilyLanguage(target) !== 'cpp') continue;
10037
+ if (!target.isSpecialization && !target.templateDependent) continue;
10038
+ const related = definitions.filter(d =>
10039
+ !NON_CALLABLE_TYPES.has(d.type) &&
10040
+ cFamilyLanguage(d) === 'cpp' && sameOwner(target, d) &&
10041
+ (d.isSpecialization || (d.templateDependent && !d.isSignature)));
10042
+ const primaries = related.filter(d => !d.isSpecialization);
10043
+ const specializations = related.filter(d => d.isSpecialization);
10044
+ if (primaries.length !== 1 || specializations.length === 0) continue;
10045
+ if (!related.some(d => d === target ||
10046
+ (d.file === target.file && d.startLine === target.startLine))) {
10047
+ continue;
10048
+ }
10049
+ const primary = primaries[0];
10050
+ for (const candidate of related) {
10051
+ if (candidate !== primary &&
10052
+ !includesDeclaration(candidate, primary)) continue;
10053
+ if (targetDefs.includes(candidate) ||
10054
+ (expanded && expanded.includes(candidate))) continue;
10055
+ if (!expanded) expanded = [...targetDefs];
10056
+ expanded.push(candidate);
10057
+ }
10058
+ }
9020
10059
  return expanded || targetDefs;
9021
10060
  }
9022
10061
 
@@ -9158,6 +10197,40 @@ function _goQualifierNamesImport(index, fieldFile, qualifier) {
9158
10197
  // wins so import "k8s.io/client-go/kubernetes/scheme" prefers a def in
9159
10198
  // .../kubernetes/scheme/ over .../kubeadm/scheme/). Extracted for reuse:
9160
10199
  // the parser marks some package calls isMethod:false (fix #268).
10200
+ /**
10201
+ * External-producer attribution for a for-loop iterable call (fix #294).
10202
+ * Only module-qualified producers count: the module boundary is the
10203
+ * externality evidence (`importlib.metadata.entry_points(...)`,
10204
+ * `os.walk(...)`). Same externality test as #209/#222 — relative or
10205
+ * project-ish modules are resolver gaps, never externality evidence.
10206
+ * Returns the attribution string or null.
10207
+ */
10208
+ function _iterExternalProducerVia(index, fileEntry, call) {
10209
+ if (!fileEntry || langTraits(fileEntry.language)?.typeSystem === 'nominal') return null;
10210
+ const externalModule = (mod) => {
10211
+ if (!mod || mod.startsWith('.')) return false;
10212
+ if (fileEntry.moduleResolved?.[mod]) return false;
10213
+ const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
10214
+ return !(firstSeg && _projectTopLevelNames(index).has(firstSeg));
10215
+ };
10216
+ if (call.isMethod && call.receiverIsModule && call.receiver) {
10217
+ const binding = _structuralModuleBindings(fileEntry, call)[0];
10218
+ if (binding && externalModule(String(binding.module))) {
10219
+ return `${call.receiver}.${call.name}`;
10220
+ }
10221
+ return null;
10222
+ }
10223
+ if (call.receiverRoot && !call.receiverRootType) {
10224
+ const binding = (fileEntry.importBindings || []).find(b =>
10225
+ b && b.name === call.receiverRoot && b.kind === 'import');
10226
+ if (binding && externalModule(String(binding.module))) {
10227
+ const field = call.receiverField ? `.${call.receiverField}` : '';
10228
+ return `${call.receiverRoot}${field}.${call.name}`;
10229
+ }
10230
+ }
10231
+ return null;
10232
+ }
10233
+
9161
10234
  function _structuralModuleBindings(fileEntry, call) {
9162
10235
  if (call?.receiverModuleSpecifier) {
9163
10236
  return [{
@@ -9352,13 +10425,29 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
9352
10425
  };
9353
10426
 
9354
10427
  const details = fe.exportDetails || [];
10428
+ const localDetails = details.filter(e =>
10429
+ !e.source && (e.alias || e.name) === attr);
10430
+ const ambiguousCjsMember = localDetails.some(e =>
10431
+ (e.type === 'module.exports' || e.type === 'exports') &&
10432
+ !e.defaultLike && !e.localName);
9355
10433
  const localExposed = fe.language === 'python' ||
9356
- (fe.exports || []).includes(attr) ||
9357
- details.some(e => !e.source && (e.alias || e.name) === attr);
9358
- if (localExposed) {
9359
- for (const d of (index.symbols.get(attr) || [])) {
9360
- if (d.file === abs && shapeMatches(d)) {
9361
- matches.set(`${d.file}:${d.startLine}`, d);
10434
+ (fe.exports || []).includes(attr) || localDetails.length > 0;
10435
+ if (ambiguousCjsMember) {
10436
+ // `module.exports = { run: condition ? native : fallback }`
10437
+ // exposes no single callable identity. The caller direction
10438
+ // routes this visible; trace-down must make the same abstention
10439
+ // instead of selecting a same-named local fallback.
10440
+ unknown = true;
10441
+ } else if (localExposed) {
10442
+ const localNames = new Set([attr]);
10443
+ for (const detail of localDetails) {
10444
+ if (detail.localName) localNames.add(detail.localName);
10445
+ }
10446
+ for (const localName of localNames) {
10447
+ for (const d of (index.symbols.get(localName) || [])) {
10448
+ if (d.file === abs && shapeMatches(d)) {
10449
+ matches.set(`${d.file}:${d.startLine}`, d);
10450
+ }
9362
10451
  }
9363
10452
  }
9364
10453
  }
@@ -9368,8 +10457,9 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
9368
10457
  // never to namespace.member() routing.
9369
10458
  if (options.allowDefaultExport && depth === 0) {
9370
10459
  for (const e of details) {
9371
- if (e.type !== 'module.exports' || !e.name) continue;
9372
- for (const d of (index.symbols.get(e.name) || [])) {
10460
+ if (e.type !== 'module.exports' || !e.defaultLike || !e.name) continue;
10461
+ const localName = e.localName || e.name;
10462
+ for (const d of (index.symbols.get(localName) || [])) {
9373
10463
  if (d.file === abs && shapeMatches(d)) {
9374
10464
  matches.set(`${d.file}:${d.startLine}`, d);
9375
10465
  }
@@ -9467,7 +10557,8 @@ function _calleeOverloadSelect(index, call, matches, language) {
9467
10557
  applicable = _preferFixedArityOverloads(
9468
10558
  index, call, applicable, language);
9469
10559
  if (applicable.length > 1) {
9470
- const mostSpecific = _javaMostSpecificOverload(index, applicable, call);
10560
+ const mostSpecific = _javaMostSpecificOverload(
10561
+ index, applicable, call, language);
9471
10562
  if (mostSpecific) return { match: mostSpecific };
9472
10563
  }
9473
10564
  }
@@ -9708,7 +10799,10 @@ function _declaredFieldType(index, rootType, fieldName, language, info, rootName
9708
10799
  return null;
9709
10800
  }
9710
10801
  }
9711
- const t = _normalizeFieldTypeName(rawText, language);
10802
+ const localType = _normalizeFieldTypeName(rawText, language);
10803
+ const importedIdentity = language === 'rust' && f.file && localType
10804
+ ? _rustImportedTypeIdentity(index, f.file, localType) : null;
10805
+ const t = importedIdentity?.type || localType;
9712
10806
  if (t) normalized.add(t);
9713
10807
  else return null; // any un-normalizable declaration → no evidence
9714
10808
  }
@@ -9734,8 +10828,13 @@ function _declaredFieldType(index, rootType, fieldName, language, info, rootName
9734
10828
  const qualifier = language === 'java'
9735
10829
  ? _javaNestedTypeQualifier(rawText) : undefined;
9736
10830
  if (qualifier) namespaces.add(qualifier);
9737
- const origin = _resolveFlowTypeOrigin(
9738
- index, field.file, typeName, qualifier);
10831
+ const localType = _normalizeFieldTypeName(rawText, language);
10832
+ const importedIdentity = language === 'rust' && localType
10833
+ ? _rustImportedTypeIdentity(index, field.file, localType) : null;
10834
+ const origin = importedIdentity?.type === typeName
10835
+ ? importedIdentity
10836
+ : _resolveFlowTypeOrigin(
10837
+ index, field.file, typeName, qualifier);
9739
10838
  if (!origin?.fromFile) {
9740
10839
  complete = false;
9741
10840
  break;
@@ -9944,7 +11043,14 @@ function _namespaceContainedDef(index, fileEntry, callFileAbs, receiverName, cal
9944
11043
  if (nsDefs.length === 0) return null;
9945
11044
  const candidates = restrictDefs ||
9946
11045
  (index.symbols.get(calleeName) || []).filter(s => !NON_CALLABLE_TYPES.has(s.type));
11046
+ // Self-containment guard (fix #300, itertools-measured): when receiver
11047
+ // and callee share one name (`powerset::powerset`), the candidate list IS
11048
+ // the nsDefs list — a body-less `mod powerset;` declaration (range 1..1)
11049
+ // "contains" itself and was returned as the "other definition", excluding
11050
+ // the compiler-true module-qualified call. A container is never its own
11051
+ // contained member.
9947
11052
  const contained = candidates.filter(d => nsDefs.some(ns =>
11053
+ ns !== d &&
9948
11054
  ns.file === d.file && ns.startLine <= d.startLine &&
9949
11055
  (ns.endLine ?? Infinity) >= (d.endLine ?? d.startLine)));
9950
11056
  if (contained.length === 0) return null;
@@ -10159,9 +11265,28 @@ function _calleeLanguageCompatible(index, def, callerLanguage) {
10159
11265
  * receiver typed Child legitimately reaches methods defined on Base (the
10160
11266
  * #198 ancestor rule, callee direction). Includes the type itself.
10161
11267
  */
10162
- function _receiverTypeAncestors(index, typeName, maxHops = 6) {
11268
+ function _receiverTypeAncestors(index, typeName, maxHops = 6, scopeRef = null) {
10163
11269
  const seen = new Set([typeName]);
10164
11270
  let frontier = [typeName];
11271
+ // Scope-correct FIRST hop (fix #300, attrs callee arm): with several
11272
+ // same-file same-name class defs (function-local test subclasses), the
11273
+ // name-granular parent lookup returns the first def's parents for all of
11274
+ // them — the callee walk then lands on the WRONG base's method. Seed
11275
+ // hop 0 from the def the call site lexically sees.
11276
+ if (scopeRef?.file) {
11277
+ const scoped = _scopedSameFileTypeDef(index, typeName, scopeRef.file, scopeRef);
11278
+ if (scoped) {
11279
+ const parents = index._getInheritanceParentsAt?.(
11280
+ typeName, scopeRef.file, scoped.startLine) || [];
11281
+ const next = [];
11282
+ for (const p of parents) {
11283
+ const pName = typeof p === 'string' ? p : p?.name;
11284
+ if (pName && !seen.has(pName)) { seen.add(pName); next.push(pName); }
11285
+ }
11286
+ frontier = next;
11287
+ maxHops -= 1;
11288
+ }
11289
+ }
10165
11290
  for (let hop = 0; hop < maxHops && frontier.length; hop++) {
10166
11291
  const next = [];
10167
11292
  for (const cls of frontier) {
@@ -10187,7 +11312,7 @@ function _receiverTypeAncestors(index, typeName, maxHops = 6) {
10187
11312
  * never confirmed by bare-name resolution
10188
11313
  * null — receiver type unknown; existing heuristics decide
10189
11314
  */
10190
- function _calleeReceiverTypeRoute(index, call, localTypes, language) {
11315
+ function _calleeReceiverTypeRoute(index, call, localTypes, language, scopeRef = null) {
10191
11316
  const raw = call.receiverType || localTypes?.get(call.receiver);
10192
11317
  if (!raw || typeof raw !== 'string') return null;
10193
11318
  const head = _structuralTypeHead(raw, { index, language }) || raw;
@@ -10197,7 +11322,7 @@ function _calleeReceiverTypeRoute(index, call, localTypes, language) {
10197
11322
  _calleeLanguageCompatible(index, d, language));
10198
11323
  let matches = defs.filter(d => d.className === head || d.className === norm);
10199
11324
  if (matches.length === 0 && defs.length > 0) {
10200
- const ancestors = _receiverTypeAncestors(index, head);
11325
+ const ancestors = _receiverTypeAncestors(index, head, 6, scopeRef);
10201
11326
  matches = defs.filter(d => ancestors.has(d.className));
10202
11327
  }
10203
11328
  if (matches.length === 1) return { resolve: matches[0] };
@@ -10273,7 +11398,12 @@ function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, fl
10273
11398
  const head = _structuralTypeHead(call.receiverType, { index, language }) || call.receiverType;
10274
11399
  const norm = head;
10275
11400
  if (head !== owner && norm !== owner &&
10276
- !_receiverTypeAncestors(index, head).has(owner)) return null;
11401
+ !_receiverTypeAncestors(index, head, 6, {
11402
+ file: def.file,
11403
+ line: call.line,
11404
+ scopeStart: def.startLine,
11405
+ scopeEnd: def.endLine,
11406
+ }).has(owner)) return null;
10277
11407
  }
10278
11408
  if (traits?.typeSystem === 'nominal' && call.argCount != null &&
10279
11409
  !_callArityCompatible(call, ownerDefs, language)) return null;
@@ -10488,6 +11618,28 @@ const JAVA_FINAL_REFERENCE_TYPES = new Set([
10488
11618
  'Integer', 'Long', 'Float', 'Double', 'Void',
10489
11619
  ]);
10490
11620
 
11621
+ // Complete reference-supertype sets for JDK value types (fix #299D): these
11622
+ // types are final (or effectively closed) with a fixed, fully-known
11623
+ // ancestry, so an argument statically typed as one of them can bind ONLY a
11624
+ // parameter type in this list (plus Object/generics, handled earlier). A
11625
+ // project class shadowing one of these simple names disables the denial —
11626
+ // the guard is tDefs.length === 0 at the use site. Modern-JDK marker
11627
+ // interfaces (Constable, ConstantDesc) are included so their params never
11628
+ // get falsely denied.
11629
+ const JAVA_PLATFORM_VALUE_ANCESTRY = new Map([
11630
+ ['Integer', ['Number', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11631
+ ['Long', ['Number', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11632
+ ['Short', ['Number', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11633
+ ['Byte', ['Number', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11634
+ ['Float', ['Number', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11635
+ ['Double', ['Number', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11636
+ ['Character', ['Comparable', 'Serializable', 'Constable']],
11637
+ ['Boolean', ['Comparable', 'Serializable', 'Constable']],
11638
+ ['String', ['CharSequence', 'Comparable', 'Serializable', 'Constable', 'ConstantDesc']],
11639
+ ['BigInteger', ['Number', 'Comparable', 'Serializable']],
11640
+ ['BigDecimal', ['Number', 'Comparable', 'Serializable']],
11641
+ ]);
11642
+
10491
11643
  // Which parameter types a call-site literal kind can bind (Java overload
10492
11644
  // resolution: identity, widening, boxing — plus the boxed types' interfaces).
10493
11645
  // Anything not provably incompatible MATCHES: only certainty excludes.
@@ -10499,8 +11651,37 @@ const JAVA_KIND_TYPES = {
10499
11651
  float: ['float', 'double', 'Float', 'Number', 'Comparable', 'Serializable'],
10500
11652
  double: ['double', 'Double', 'Number', 'Comparable', 'Serializable'],
10501
11653
  boolean: ['boolean', 'Boolean', 'Comparable', 'Serializable'],
11654
+ // Not literal kinds (no short/byte literals in Java) — these entries
11655
+ // serve the value-typed conversion checks (fix #299D).
11656
+ short: ['short', 'int', 'long', 'float', 'double', 'Short', 'Number', 'Comparable', 'Serializable'],
11657
+ byte: ['byte', 'short', 'int', 'long', 'float', 'double', 'Byte', 'Number', 'Comparable', 'Serializable'],
10502
11658
  };
10503
11659
 
11660
+ // Unboxing is defined for exactly these wrapper types (JLS 5.1.8).
11661
+ const JAVA_WRAPPER_PRIMITIVES = new Map([
11662
+ ['Integer', 'int'], ['Long', 'long'], ['Short', 'short'], ['Byte', 'byte'],
11663
+ ['Character', 'char'], ['Boolean', 'boolean'],
11664
+ ['Float', 'float'], ['Double', 'double'],
11665
+ ]);
11666
+
11667
+ // Declared types of the JDK numeric-constant fields (fix #299D): exact
11668
+ // compiler contracts, consulted only when no project field shadows the
11669
+ // owner/field pair.
11670
+ const JAVA_PLATFORM_FIELD_TYPES = new Map([
11671
+ ['Float.NaN', 'float'], ['Float.POSITIVE_INFINITY', 'float'],
11672
+ ['Float.NEGATIVE_INFINITY', 'float'], ['Float.MIN_VALUE', 'float'],
11673
+ ['Float.MAX_VALUE', 'float'], ['Float.MIN_NORMAL', 'float'],
11674
+ ['Double.NaN', 'double'], ['Double.POSITIVE_INFINITY', 'double'],
11675
+ ['Double.NEGATIVE_INFINITY', 'double'], ['Double.MIN_VALUE', 'double'],
11676
+ ['Double.MAX_VALUE', 'double'], ['Double.MIN_NORMAL', 'double'],
11677
+ ['Integer.MAX_VALUE', 'int'], ['Integer.MIN_VALUE', 'int'],
11678
+ ['Long.MAX_VALUE', 'long'], ['Long.MIN_VALUE', 'long'],
11679
+ ['Short.MAX_VALUE', 'short'], ['Short.MIN_VALUE', 'short'],
11680
+ ['Byte.MAX_VALUE', 'byte'], ['Byte.MIN_VALUE', 'byte'],
11681
+ ['Character.MAX_VALUE', 'char'], ['Character.MIN_VALUE', 'char'],
11682
+ ['Boolean.TRUE', 'Boolean'], ['Boolean.FALSE', 'Boolean'],
11683
+ ]);
11684
+
10504
11685
  // Exact subset of C# implicit conversions for compiler-owned closed types.
10505
11686
  // Returns null when user-defined conversions or external ancestry could
10506
11687
  // matter. A false verdict is therefore exclusion-grade evidence.
@@ -10558,6 +11739,19 @@ function _csharpKnownTypeAssignable(actualRaw, expectedRaw) {
10558
11739
  // name-based guesses. They close the common portable-AST gap where overload
10559
11740
  // resolution depends on the return type of a library call.
10560
11741
  const JAVA_PLATFORM_METHOD_RETURNS = new Map([
11742
+ // Exact JDK factory contracts (fix #299D): valueOf on the boxed and
11743
+ // arbitrary-precision types returns the owner type itself.
11744
+ ['java.math.BigInteger#valueOf', 'java.math.BigInteger'],
11745
+ ['java.math.BigDecimal#valueOf', 'java.math.BigDecimal'],
11746
+ ['java.lang.Integer#valueOf', 'java.lang.Integer'],
11747
+ ['java.lang.Long#valueOf', 'java.lang.Long'],
11748
+ ['java.lang.Short#valueOf', 'java.lang.Short'],
11749
+ ['java.lang.Byte#valueOf', 'java.lang.Byte'],
11750
+ ['java.lang.Float#valueOf', 'java.lang.Float'],
11751
+ ['java.lang.Double#valueOf', 'java.lang.Double'],
11752
+ ['java.lang.Boolean#valueOf', 'java.lang.Boolean'],
11753
+ ['java.lang.Character#valueOf', 'java.lang.Character'],
11754
+ ['java.lang.String#valueOf', 'java.lang.String'],
10561
11755
  ['java.lang.Object#getClass', 'java.lang.Class'],
10562
11756
  ['java.lang.Class#getEnclosingClass', 'java.lang.Class'],
10563
11757
  ['java.lang.Class#getDeclaringClass', 'java.lang.Class'],
@@ -10807,7 +12001,13 @@ function _javaOwnerFieldType(index, owner, fieldName) {
10807
12001
  definition.className === ownerSimple &&
10808
12002
  (definition.type === 'field' || definition.memberType === 'field') &&
10809
12003
  definition.fieldType);
10810
- if (fields.length === 0) return null;
12004
+ if (fields.length === 0) {
12005
+ // JDK constant-field contracts (fix #299D): Float.NaN is a float,
12006
+ // Boolean.TRUE a Boolean. Project fields, checked above, always
12007
+ // outrank the platform table.
12008
+ return JAVA_PLATFORM_FIELD_TYPES.get(`${ownerSimple}.${fieldName}`) ||
12009
+ null;
12010
+ }
10811
12011
 
10812
12012
  const types = new Set();
10813
12013
  for (const field of fields) {
@@ -10922,6 +12122,27 @@ function _javaArgKindMatches(index, kind, paramType, language) {
10922
12122
  if (tSimple === bare) return true;
10923
12123
  const platformAssignable = _javaPlatformAssignable(t, bare);
10924
12124
  if (platformAssignable !== null) return platformAssignable;
12125
+ // A primitive argument type has closed conversion targets (fix
12126
+ // #299D): identity, widening, its own box, and the box's interfaces
12127
+ // — exactly the literal-kind table. This must precede the
12128
+ // final-reference denial below, which is boxing-blind (char DOES
12129
+ // bind a Character parameter). No project class can shadow a
12130
+ // primitive name, so no tDefs guard is needed.
12131
+ if (language === 'java' && JAVA_PRIMITIVES.has(tSimple) &&
12132
+ JAVA_KIND_TYPES[tSimple]) {
12133
+ return JAVA_KIND_TYPES[tSimple].includes(bare);
12134
+ }
12135
+ // A reference-typed argument reaches a primitive parameter only
12136
+ // through unboxing, defined for exactly the eight wrapper types
12137
+ // (then widening). Any other reference type — project, JDK, or
12138
+ // unknown — provably cannot bind a primitive parameter (fix #299D).
12139
+ if (language === 'java' && JAVA_PRIMITIVES.has(bare) &&
12140
+ !JAVA_PRIMITIVES.has(tSimple) && !/^[A-Z][0-9]?$/.test(tSimple)) {
12141
+ const unboxed = JAVA_WRAPPER_PRIMITIVES.get(tSimple);
12142
+ return unboxed != null &&
12143
+ (unboxed === bare ||
12144
+ JAVA_KIND_TYPES[unboxed]?.includes(bare) === true);
12145
+ }
10925
12146
  // These java.lang types are final. A statically known different type
10926
12147
  // can never bind their overload, even when the argument type's own
10927
12148
  // ancestry ends in external Object and is therefore incomplete.
@@ -10944,6 +12165,14 @@ function _javaArgKindMatches(index, kind, paramType, language) {
10944
12165
  d.modifiers?.includes('sealed'))) {
10945
12166
  return false;
10946
12167
  }
12168
+ if (language === 'java' && tDefs.length === 0) {
12169
+ // JDK value types have a complete, closed ancestry (fix #299D):
12170
+ // an Integer-typed argument can never bind add(JsonElement).
12171
+ // The no-project-def guard keeps a project class shadowing the
12172
+ // simple name in normal resolution.
12173
+ const valueAncestry = JAVA_PLATFORM_VALUE_ANCESTRY.get(tSimple);
12174
+ if (valueAncestry) return valueAncestry.includes(bare);
12175
+ }
10947
12176
  if (tDefs.length === 0) return true; // external arg type — unknowable
10948
12177
  const asTarget = [{ className: tSimple, file: tDefs[0].file }];
10949
12178
  if (_isDispatchAncestor(index, bare, asTarget)) return true;
@@ -10963,6 +12192,7 @@ function _cppTypeCategory(type) {
10963
12192
  const unqualified = original
10964
12193
  .replace(/\b(const|volatile|constexpr|typename|struct|class)\b/g, ' ')
10965
12194
  .replace(/&&|\.\.\.|[&*]/g, ' ')
12195
+ .replace(/\[[^\]]*\]/g, ' ')
10966
12196
  .replace(/\s+/g, ' ')
10967
12197
  .trim();
10968
12198
  const headText = unqualified.split('<')[0].trim();
@@ -11082,7 +12312,8 @@ function _cppArgKindMatches(kind, paramType) {
11082
12312
  if (kind === 'null') {
11083
12313
  return !['number', 'bool', 'character'].includes(expected.kind);
11084
12314
  }
11085
- if (kind.startsWith('type:') || kind.startsWith('call:')) {
12315
+ if (kind.startsWith('type:') || kind.startsWith('call:') ||
12316
+ kind.startsWith('bcall:')) {
11086
12317
  const actualType = kind.slice(kind.indexOf(':') + 1);
11087
12318
  const actual = _cppTypeCategory(actualType);
11088
12319
  if (actual.head && expected.head && actual.head === expected.head) return true;
@@ -11197,7 +12428,7 @@ function _javaTypeAtLeastAsSpecific(index, subType, superType, subDef) {
11197
12428
  // when every argument position is at least as specific as every competing
11198
12429
  // candidate and at least one position is strictly more specific. Unknown
11199
12430
  // relationships stay ambiguous.
11200
- function _javaMostSpecificOverload(index, applicable, call) {
12431
+ function _javaMostSpecificOverload(index, applicable, call, language) {
11201
12432
  const argCount = call.argCount;
11202
12433
  if (!Number.isInteger(argCount) || applicable.length < 2) return null;
11203
12434
  // Compiler-visible exact static types outrank candidates whose external
@@ -11233,6 +12464,64 @@ function _javaMostSpecificOverload(index, applicable, call) {
11233
12464
  if (winners.length === 1) return winners[0].definition;
11234
12465
  }
11235
12466
  }
12467
+ // JLS 15.12.2 phase discipline (fix #299D): when EVERY argument's static
12468
+ // type is a known primitive, strict-invocation candidates (identity or
12469
+ // primitive widening, fixed arity — phase 1) exclude boxing candidates
12470
+ // (phase 2) from the race entirely; among widening targets the narrowest
12471
+ // is most specific (an int literal binds value(long), not value(float)/
12472
+ // value(double)/value(Number)). Varargs params are phase 3 and never
12473
+ // count as strict. Any non-primitive or unknown position refuses — phase
12474
+ // membership must be provable for every argument.
12475
+ if (language === 'java' && Array.isArray(call.argKinds) &&
12476
+ call.argKinds.length >= argCount && argCount > 0) {
12477
+ const primOf = (kind) => {
12478
+ if (!kind || typeof kind !== 'string') return null;
12479
+ if (JAVA_PRIMITIVES.has(kind)) return kind; // literal kinds
12480
+ const staticType = _javaStaticTypeForKind(index, kind);
12481
+ const simple = staticType ? staticType.split('.').pop() : null;
12482
+ return simple && JAVA_PRIMITIVES.has(simple) ? simple : null;
12483
+ };
12484
+ const prims = [];
12485
+ for (let i = 0; i < argCount; i++) prims.push(primOf(call.argKinds[i]));
12486
+ if (prims.every(Boolean)) {
12487
+ const strictAt = (prim, definition, i) => {
12488
+ const param = _javaParamAt(definition, i, call);
12489
+ if (!param || param.rest) return false;
12490
+ const bare = _javaBareParamType(param);
12491
+ return bare !== null && JAVA_PRIMITIVES.has(bare) &&
12492
+ (prim === bare ||
12493
+ JAVA_KIND_TYPES[prim]?.includes(bare) === true);
12494
+ };
12495
+ const strict = applicable.filter(definition => {
12496
+ for (let i = 0; i < argCount; i++) {
12497
+ if (!strictAt(prims[i], definition, i)) return false;
12498
+ }
12499
+ return true;
12500
+ });
12501
+ if (strict.length === 1) return strict[0];
12502
+ if (strict.length > 1) {
12503
+ const dominatesPrim = (a, b) => {
12504
+ let strictly = false;
12505
+ for (let i = 0; i < argCount; i++) {
12506
+ const at = _javaBareParamType(_javaParamAt(a, i, call));
12507
+ const bt = _javaBareParamType(_javaParamAt(b, i, call));
12508
+ if (at === bt) continue;
12509
+ if (!JAVA_PRIMITIVES.has(bt) ||
12510
+ JAVA_KIND_TYPES[at]?.includes(bt) !== true) {
12511
+ return false;
12512
+ }
12513
+ strictly = true;
12514
+ }
12515
+ return strictly;
12516
+ };
12517
+ const winners = strict.filter(a =>
12518
+ strict.every(b => a === b || dominatesPrim(a, b)));
12519
+ if (winners.length === 1) return winners[0];
12520
+ return null; // strict phase engaged but unranked — visible
12521
+ }
12522
+ // no strict candidate: boxing phase — general ranking decides
12523
+ }
12524
+ }
11236
12525
  const dominates = (a, b) => {
11237
12526
  let strict = false;
11238
12527
  for (let i = 0; i < argCount; i++) {
@@ -11394,16 +12683,142 @@ function _cppQualifiedPathOwnsTarget(index, callerFile, call, targetDefs) {
11394
12683
  return targetDefs.some(definition => {
11395
12684
  if (!definition.file) return false;
11396
12685
  const namespace = String(definition.namespace || '');
11397
- if (namespace === call.receiver ||
11398
- namespace.startsWith(`${call.receiver}::`) ||
11399
- namespace.endsWith(`::${call.receiver}`)) {
11400
- return true;
12686
+ if (namespace) {
12687
+ // An AST-recorded namespace is stronger than the portable-header
12688
+ // path heuristic. `fmt::vformat_to` cannot name
12689
+ // `detail::vformat_to`; only the exact namespace (or the same
12690
+ // namespace expressed relative to an enclosing scope) owns it.
12691
+ return namespace === call.receiver ||
12692
+ String(call.receiver).endsWith(`::${namespace}`) ||
12693
+ namespace.endsWith(`::${call.receiver}`);
11401
12694
  }
11402
12695
  const relative = path.relative(index.root, definition.file);
11403
12696
  return relative.split(path.sep).includes(namespaceRoot);
11404
12697
  });
11405
12698
  }
11406
12699
 
12700
+ function _cppMacroParamOutcomes(index, callerFile, macroName, argIndex,
12701
+ depth = 0, seen = new Set()) {
12702
+ if (!macroName || depth > 8) return new Set(['unknown']);
12703
+ let cache;
12704
+ let cacheKey;
12705
+ if (depth === 0) {
12706
+ if (!index._cppMacroParamOutcomesCache) {
12707
+ Object.defineProperty(index, '_cppMacroParamOutcomesCache', {
12708
+ value: new Map(),
12709
+ configurable: true,
12710
+ });
12711
+ }
12712
+ cache = index._cppMacroParamOutcomesCache;
12713
+ cacheKey = `${callerFile}\0${macroName}\0${argIndex}`;
12714
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
12715
+ }
12716
+ const key = `${macroName}:${argIndex}`;
12717
+ if (seen.has(key)) return new Set(['unknown']);
12718
+ const nextSeen = new Set(seen);
12719
+ nextSeen.add(key);
12720
+ const visible = _cppVisibleFiles(index, callerFile);
12721
+ const definitions = (index.symbols.get(macroName) || []).filter(definition =>
12722
+ definition.type === 'macro' && definition.functionLike !== false &&
12723
+ definition.file && visible.has(definition.file));
12724
+ if (definitions.length === 0) {
12725
+ const result = new Set(['unknown']);
12726
+ if (cache) cache.set(cacheKey, result);
12727
+ return result;
12728
+ }
12729
+ const outcomes = new Set();
12730
+ for (const definition of definitions) {
12731
+ const effects = (definition.macroParamEffects || [])
12732
+ .filter(effect => effect.paramIndex === argIndex);
12733
+ if (effects.length === 0) {
12734
+ outcomes.add('preserve');
12735
+ continue;
12736
+ }
12737
+ for (const effect of effects) {
12738
+ if (effect.kind === 'qualified' && effect.qualifier) {
12739
+ outcomes.add(`qualified:${effect.qualifier}`);
12740
+ } else if (effect.kind === 'forwarded' && effect.macro &&
12741
+ Number.isInteger(effect.argIndex)) {
12742
+ for (const outcome of _cppMacroParamOutcomes(
12743
+ index, callerFile, effect.macro, effect.argIndex,
12744
+ depth + 1, nextSeen)) {
12745
+ outcomes.add(outcome);
12746
+ }
12747
+ } else {
12748
+ outcomes.add('unknown');
12749
+ }
12750
+ }
12751
+ }
12752
+ const result = outcomes.size > 0 ? outcomes : new Set(['unknown']);
12753
+ if (cache) cache.set(cacheKey, result);
12754
+ return result;
12755
+ }
12756
+
12757
+ function _cppMacroQualifierCanNameTarget(qualifier, targetDefs) {
12758
+ if (!qualifier || !Array.isArray(targetDefs) || targetDefs.length === 0) {
12759
+ return false;
12760
+ }
12761
+ if (qualifier === 'global') {
12762
+ return targetDefs.some(definition =>
12763
+ !definition.className && !definition.receiver && !definition.namespace);
12764
+ }
12765
+ return targetDefs.some(definition => {
12766
+ const owner = definition.className || definition.receiver;
12767
+ const namespace = definition.namespace;
12768
+ return owner === qualifier || namespace === qualifier ||
12769
+ String(owner || '').endsWith(`::${qualifier}`) ||
12770
+ String(namespace || '').endsWith(`::${qualifier}`);
12771
+ });
12772
+ }
12773
+
12774
+ /**
12775
+ * Whether a macro transforms this source call's argument into a qualified
12776
+ * callable. Replacement-list effects are parser-derived and followed through
12777
+ * forwarding macros; conditional definitions are unioned, so disagreement
12778
+ * routes visible rather than becoming false exclusion evidence.
12779
+ */
12780
+ function _cppMacroTargetDisposition(index, callerFile, call, targetDefs) {
12781
+ if (!Array.isArray(call?.macroArguments) ||
12782
+ call.macroArguments.length === 0) return null;
12783
+ let uncertain = false;
12784
+ let qualified = null;
12785
+ let qualifiedAway = false;
12786
+ for (const wrapper of call.macroArguments) {
12787
+ const outcomes = _cppMacroParamOutcomes(
12788
+ index, callerFile, wrapper.name, wrapper.argIndex);
12789
+ const qualifiers = [...outcomes]
12790
+ .filter(outcome => outcome.startsWith('qualified:'))
12791
+ .map(outcome => outcome.slice('qualified:'.length));
12792
+ if (qualifiers.length === 0) continue;
12793
+ const targetMatches = qualifiers.map(qualifier =>
12794
+ _cppMacroQualifierCanNameTarget(qualifier, targetDefs));
12795
+ const canNameTarget = targetMatches.some(Boolean);
12796
+ const allNameTarget = targetMatches.every(Boolean);
12797
+ const hasUnknown = outcomes.has('unknown') || outcomes.has('preserve');
12798
+ if (canNameTarget) {
12799
+ const unique = new Set(qualifiers);
12800
+ if (!hasUnknown && allNameTarget && unique.size === 1) {
12801
+ const qualifier = [...unique][0];
12802
+ if (qualified && qualified !== qualifier) uncertain = true;
12803
+ else qualified = qualifier;
12804
+ } else {
12805
+ // Multiple conditional definitions, a preserve path, or a
12806
+ // mixture of target and non-target qualifiers means the
12807
+ // preprocessor can change identity by build configuration.
12808
+ uncertain = true;
12809
+ }
12810
+ continue;
12811
+ }
12812
+ if (!hasUnknown) qualifiedAway = true;
12813
+ else uncertain = true;
12814
+ }
12815
+ if (uncertain || (qualified && qualifiedAway)) {
12816
+ return { kind: 'uncertain' };
12817
+ }
12818
+ if (qualifiedAway) return { kind: 'other' };
12819
+ return qualified ? { kind: 'qualified', qualifier: qualified } : null;
12820
+ }
12821
+
11407
12822
  function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11408
12823
  const targetOwners = new Set(targetDefs.map(d => d.className).filter(Boolean));
11409
12824
  const targetLanguage = targetDefs[0]?.file && index.files.get(targetDefs[0].file)?.language;
@@ -11430,12 +12845,22 @@ function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11430
12845
  targetDefs.every(d => !d.className && !d.receiver) &&
11431
12846
  _cppTargetVisibleFrom(index, callerFile, targetDefs)) {
11432
12847
  const visible = _cppVisibleFiles(index, callerFile);
11433
- const callerHostsPin = targetDefs.some(d => d.file === callerFile);
12848
+ const pathCanName = definition => {
12849
+ if (!call.isPathCall || !call.receiver) return true;
12850
+ const namespace = String(definition.namespace || '');
12851
+ // Missing namespace metadata is not negative evidence: namespace
12852
+ // macros (FMT_BEGIN_NAMESPACE and peers) are intentionally opaque
12853
+ // to the portable tree. A recorded namespace, however, must match
12854
+ // the qualifier's exact/relative namespace identity.
12855
+ if (!namespace) return true;
12856
+ return namespace === call.receiver ||
12857
+ String(call.receiver).endsWith(`::${namespace}`) ||
12858
+ namespace.endsWith(`::${call.receiver}`);
12859
+ };
11434
12860
  family = definitions.filter(d =>
11435
12861
  !NON_CALLABLE_TYPES.has(d.type) &&
11436
12862
  !d.className && !d.receiver &&
11437
- (!call.isPathCall || d.file !== callerFile || callerHostsPin ||
11438
- pinnedKeys.has(`${d.file}:${d.startLine}`)) &&
12863
+ pathCanName(d) &&
11439
12864
  (visible.has(d.file) ||
11440
12865
  pinnedKeys.has(`${d.file}:${d.startLine}`)));
11441
12866
  } else {
@@ -11471,6 +12896,17 @@ function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11471
12896
  // constrained templates can share an identical function signature while
11472
12897
  // remaining distinct compiler overloads.
11473
12898
  const pinSigs = new Set(targetDefs.map(typeSig).filter(s => s !== null));
12899
+ // Occupied dispatch slots (fix #299D): an ancestor def whose signature
12900
+ // matches ANY family member of the descendant class is that member's
12901
+ // override/hiding slot — the same bindable method, never an extra
12902
+ // sibling. Without this, JsonTreeWriter's fully-overridden value(...)
12903
+ // family double-counted every JsonWriter overload and the exact-type
12904
+ // winner was never unique (two value(String) "candidates").
12905
+ const familySigs = new Set(pinSigs);
12906
+ for (const d of family) {
12907
+ const sig = typeSig(d);
12908
+ if (sig !== null) familySigs.add(sig);
12909
+ }
11474
12910
  const pinFile = targetDefs.find(d => d.file)?.file;
11475
12911
  const seenCls = new Set(targetOwners);
11476
12912
  const queue = [...targetOwners];
@@ -11487,13 +12923,42 @@ function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11487
12923
  for (const d of definitions) {
11488
12924
  if (NON_CALLABLE_TYPES.has(d.type) || d.className !== pName) continue;
11489
12925
  const sig = typeSig(d);
11490
- if (sig !== null && pinSigs.has(sig)) continue; // override slot
12926
+ if (sig !== null && familySigs.has(sig)) continue; // occupied slot
12927
+ if (sig !== null) familySigs.add(sig);
11491
12928
  family.push(d);
11492
12929
  }
11493
12930
  }
11494
12931
  }
12932
+ if (targetLanguage === 'cpp' && call.isPathCall && family.length > 0 &&
12933
+ !family.some(definition =>
12934
+ pinnedKeys.has(`${definition.file}:${definition.startLine}`))) {
12935
+ // The qualifier selects a modeled namespace family that does not
12936
+ // contain the pin (`fmt::vformat_to` versus
12937
+ // `detail::vformat_to`). This is exact negative identity evidence
12938
+ // even when that family has only one overload.
12939
+ return 'other-overload';
12940
+ }
11495
12941
  if (family.length <= 1) return null;
11496
12942
  if (family.every(d => pinnedKeys.has(`${d.file}:${d.startLine}`))) return null;
12943
+ // Producer-return argument typing (fix #299B): a bare-identifier
12944
+ // producer (`paint(tint(3))`) types its argument position from the
12945
+ // project's declared return type — the #199/#207 return-type-flow rail
12946
+ // at the argument site. Resolution demands a unique project identity
12947
+ // with full return-text agreement; anything less keeps the opaque
12948
+ // `bcall:` kind (matches everything, no decision either way).
12949
+ if (targetLanguage === 'cpp' && Array.isArray(call.argKinds) &&
12950
+ call.argKinds.some(kind => typeof kind === 'string' &&
12951
+ kind.startsWith('bcall:'))) {
12952
+ const resolvedKinds = call.argKinds.map(kind => {
12953
+ if (typeof kind !== 'string' || !kind.startsWith('bcall:')) return kind;
12954
+ const producerReturn = _cppBareProducerReturnType(
12955
+ index, kind.slice('bcall:'.length));
12956
+ return producerReturn ? `type:${producerReturn}` : kind;
12957
+ });
12958
+ if (resolvedKinds.some((kind, i) => kind !== call.argKinds[i])) {
12959
+ call = { ...call, argKinds: resolvedKinds };
12960
+ }
12961
+ }
11497
12962
  let applicable = family.filter(d => _overloadApplicable(index, call, d));
11498
12963
  if (applicable.length === 0) return null; // shape fits nothing we model — no claim
11499
12964
  if (targetLanguage === 'java' || targetLanguage === 'csharp') {
@@ -11502,7 +12967,8 @@ function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11502
12967
  }
11503
12968
  const mostSpecific = (targetLanguage === 'java' ||
11504
12969
  targetLanguage === 'csharp')
11505
- ? _javaMostSpecificOverload(index, applicable, call) : null;
12970
+ ? _javaMostSpecificOverload(index, applicable, call, targetLanguage)
12971
+ : null;
11506
12972
  if (mostSpecific) {
11507
12973
  return pinnedKeys.has(`${mostSpecific.file}:${mostSpecific.startLine}`)
11508
12974
  ? null : 'other-overload';
@@ -11514,6 +12980,13 @@ function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11514
12980
  return null;
11515
12981
  }
11516
12982
  if (applicable.length === 1) return null; // uniquely the pinned overload
12983
+ if (targetLanguage === 'cpp') {
12984
+ const winner = _cppExactOverloadWinner(call, applicable);
12985
+ if (winner) {
12986
+ return pinnedKeys.has(`${winner.file}:${winner.startLine}`)
12987
+ ? null : 'other-overload';
12988
+ }
12989
+ }
11517
12990
  const compileTimeDispatch = targetLanguage === 'cpp';
11518
12991
  const templateOnly = compileTimeDispatch &&
11519
12992
  applicable.every(definition => definition.templateDependent);
@@ -11529,6 +13002,121 @@ function _overloadDiscipline(index, call, targetDefs, definitions, callerFile) {
11529
13002
  };
11530
13003
  }
11531
13004
 
13005
+ // C++ exact-static-type overload selection (fix #299C): when every argument
13006
+ // position carries a concrete static type (`type:X` from declared locals or
13007
+ // casts) and exactly one NON-template candidate matches every position at
13008
+ // Exact Match rank, the compiler selects it — identity conversion outranks
13009
+ // every other conversion sequence, and non-template beats template on the
13010
+ // tie-break. Template winners are refused (their param text can name their
13011
+ // own template parameters, and SFINAE can disable them invisibly); literal
13012
+ // and `expr` kinds refuse the whole selection (integer literals rank equally
13013
+ // against sibling integer widths). Exactness is deliberately narrower than
13014
+ // the compiler's Exact Match class: by-value top-level const is ignored and
13015
+ // `const X&` binding accepted, but non-const refs (cast rvalues can't bind),
13016
+ // rvalue refs, and pointer-pointee qualification changes all refuse — a
13017
+ // missed exact match keeps the site visible, a wrong one would exclude a
13018
+ // true caller.
13019
+ function _cppNormalizeTypeText(text) {
13020
+ if (!text) return null;
13021
+ return String(text).replace(/\s+/g, ' ')
13022
+ .replace(/\s*([*&])\s*/g, ' $1').trim() || null;
13023
+ }
13024
+
13025
+ function _cppStripTopLevelConst(text) {
13026
+ if (!text || /[*&]$/.test(text)) return text;
13027
+ return text.replace(/^const /, '').replace(/^volatile /, '');
13028
+ }
13029
+
13030
+ function _cppExactParamMatch(paramType, argType) {
13031
+ const param = _cppNormalizeTypeText(paramType);
13032
+ const arg = _cppStripTopLevelConst(_cppNormalizeTypeText(argType));
13033
+ if (!param || !arg) return false;
13034
+ return param === arg ||
13035
+ _cppStripTopLevelConst(param) === arg ||
13036
+ param === `const ${arg} &`;
13037
+ }
13038
+
13039
+ // Resolve a bare-identifier producer name to its declared return type
13040
+ // (fix #299B). Discipline mirrors the #199/#207 return-type-flow rails:
13041
+ // every callable def of the name project-wide must live in a C-family file
13042
+ // and agree on ONE full normalized return text. Type defs sharing the name
13043
+ // refuse (the bare call may be a constructor), template producers refuse
13044
+ // (their substituted return is instantiation-dependent), `auto` without a
13045
+ // recorded trailing type refuses, and return-less defs are ignored only
13046
+ // when they are declarations whose parameter shape matches a return-bearing
13047
+ // def — a C++ declaration's return type must match its definition, so a
13048
+ // ret-less signature there is a parse gap on the SAME entity, never a
13049
+ // hidden disagreeing overload.
13050
+ function _cppBareProducerReturnType(index, producerName) {
13051
+ if (!producerName) return null;
13052
+ const defs = (index.symbols.get(producerName) || []);
13053
+ if (defs.length === 0) return null;
13054
+ const paramKey = (def) => Array.isArray(def.paramsStructured)
13055
+ ? def.paramsStructured.map(param =>
13056
+ _cppNormalizeTypeText(param?.type) || '?').join('\0')
13057
+ : null;
13058
+ const returnBearing = [];
13059
+ const returnLess = [];
13060
+ for (const def of defs) {
13061
+ const language = index.files.get(def.file)?.language;
13062
+ if (language !== 'c' && language !== 'cpp') return null;
13063
+ if (NON_CALLABLE_TYPES.has(def.type)) return null; // constructor risk
13064
+ if (def.isSpecialization) continue; // same entity as its primary
13065
+ if (def.returnType) returnBearing.push(def);
13066
+ else returnLess.push(def);
13067
+ }
13068
+ if (returnBearing.length === 0) return null;
13069
+ for (const def of returnLess) {
13070
+ if (!def.isSignature) return null;
13071
+ const key = paramKey(def);
13072
+ if (key === null ||
13073
+ !returnBearing.some(other => paramKey(other) === key)) {
13074
+ return null;
13075
+ }
13076
+ }
13077
+ let agreed = null;
13078
+ for (const def of returnBearing) {
13079
+ if (def.templateDependent) return null;
13080
+ const returnText = _cppNormalizeTypeText(def.returnType);
13081
+ if (!returnText || /^auto\b/.test(returnText)) return null;
13082
+ if (agreed === null) agreed = returnText;
13083
+ else if (agreed !== returnText) return null;
13084
+ }
13085
+ return agreed;
13086
+ }
13087
+
13088
+ function _cppExactOverloadWinner(call, applicable) {
13089
+ const kinds = call.argKinds;
13090
+ if (!Array.isArray(kinds) || !Number.isInteger(call.argCount) ||
13091
+ call.argCount === 0 || kinds.length < call.argCount) return null;
13092
+ const argTypes = [];
13093
+ for (let i = 0; i < call.argCount; i++) {
13094
+ const kind = kinds[i];
13095
+ if (typeof kind !== 'string' || !kind.startsWith('type:')) return null;
13096
+ const argType = kind.slice('type:'.length);
13097
+ if (!argType) return null;
13098
+ argTypes.push(argType);
13099
+ }
13100
+ let winner = null;
13101
+ for (const def of applicable) {
13102
+ if (def.templateDependent || def.isSpecialization) continue;
13103
+ const ps = def.paramsStructured;
13104
+ if (!Array.isArray(ps) || ps.length < call.argCount) continue;
13105
+ if (ps.some(param => param && (param.rest || param.variadic))) continue;
13106
+ let exact = true;
13107
+ for (let i = 0; i < call.argCount; i++) {
13108
+ if (!_cppExactParamMatch(ps[i]?.type, argTypes[i])) {
13109
+ exact = false;
13110
+ break;
13111
+ }
13112
+ }
13113
+ if (!exact) continue;
13114
+ if (winner) return null; // two exact candidates — refuse
13115
+ winner = def;
13116
+ }
13117
+ return winner;
13118
+ }
13119
+
11532
13120
  /**
11533
13121
  * Build the target type set for receiver-class disambiguation: target
11534
13122
  * classes/receivers + their non-overriding subtypes (transitively). A Child
@@ -12174,6 +13762,31 @@ function _builtinMethodReturnType(language, receiverType, methodName) {
12174
13762
  * the PRODUCER's scope (_resolveFlowTypeOrigin). External producer packages
12175
13763
  * and reject-set returns stay untyped — no evidence either way.
12176
13764
  */
13765
+ function _goBuiltinChainedReceiverType(index, fileEntry, filePath, call) {
13766
+ if (fileEntry?.language !== 'go' || !call.receiverCallResultType) return null;
13767
+ const type = call.receiverCallResultType;
13768
+ const qualifier = call.receiverCallResultTypeQualifier;
13769
+ if (qualifier) {
13770
+ const qualified = _goQualifiedReceiverType(
13771
+ index, fileEntry, qualifier, type);
13772
+ if (!qualified) return null;
13773
+ if (qualified.kind !== 'project') {
13774
+ return {
13775
+ externalVia: qualified.via,
13776
+ externalConcrete: true,
13777
+ };
13778
+ }
13779
+ if (qualified.defs.length === 0 ||
13780
+ new Set(qualified.defs.map(definition => definition.file)).size !== 1) {
13781
+ return null;
13782
+ }
13783
+ return { type, fromFile: qualified.defs[0].file };
13784
+ }
13785
+ const origin = _resolveFlowTypeOrigin(index, filePath, type);
13786
+ if (!origin?.fromFile) return null;
13787
+ return { type, fromFile: origin.fromFile };
13788
+ }
13789
+
12177
13790
  function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
12178
13791
  const language = fileEntry.language;
12179
13792
  const defs = (index.symbols.get(call.receiverCall) || [])
@@ -12859,18 +14472,26 @@ function _rustClosureReceiverType(index, fileEntry, filePath, call, ctx) {
12859
14472
  function _typeOfCallResultFold(index, fileEntry, filePath, record, ctx, consumerAwaited) {
12860
14473
  if (ctx.memo.has(record)) return ctx.memo.get(record);
12861
14474
  // Real builder APIs routinely exceed 64 hops (clap's benchmark command
12862
- // has ~160). Keep a generous hard safety bound; failed results are not
12863
- // memoized because a depth-budget failure is contextual — caching it
12864
- // poisoned shorter suffix queries later in the same operation.
12865
- if (ctx.visiting.has(record) || ctx.visiting.size > 256) return null;
14475
+ // has ~160). Keep a generous hard safety bound. A cycle/depth refusal is
14476
+ // CONTEXTUAL (it depends on the visiting path), so any null whose subtree
14477
+ // tripped the bound must not be cached — caching it poisoned shorter
14478
+ // suffix queries later in the same operation. A null with NO trip in its
14479
+ // subtree is universal (the producer shape is untypeable regardless of
14480
+ // path) and IS cached: without that, an untypeable chain re-walks its
14481
+ // whole producer prefix per consumer (clap `unwrap`: 76s -> linear).
14482
+ if (ctx.visiting.has(record) || ctx.visiting.size > 256) {
14483
+ ctx.foldTrips = (ctx.foldTrips || 0) + 1;
14484
+ return null;
14485
+ }
12866
14486
  ctx.visiting.add(record);
14487
+ const tripsBefore = ctx.foldTrips || 0;
12867
14488
  let out;
12868
14489
  try {
12869
14490
  out = _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, consumerAwaited);
12870
14491
  } finally {
12871
14492
  ctx.visiting.delete(record);
12872
14493
  }
12873
- if (out) ctx.memo.set(record, out);
14494
+ if (out || (ctx.foldTrips || 0) === tripsBefore) ctx.memo.set(record, out ?? null);
12874
14495
  return out;
12875
14496
  }
12876
14497
 
@@ -12967,6 +14588,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
12967
14588
  const cands = (index.symbols.get(name) || [])
12968
14589
  .filter(d => !NON_CALLABLE_TYPES.has(d.type) && d.returnType && !d.className);
12969
14590
  let matches = cands.filter(d => d.file === modFile);
14591
+ // A named/default import may itself be an exported namespace object
14592
+ // (`import { z } from 'zod/v3'; z.string()`). Resolve the producer
14593
+ // name through that exact namespace identity before the older
14594
+ // one-hop barrel fallback. Exported callable-alias symbols then carry
14595
+ // the captured class member's declared return type into the chain.
14596
+ if (matches.length === 0) {
14597
+ matches = cands.filter(definition =>
14598
+ _importedNamespaceMemberOwnership(
14599
+ index, fileEntry, record, new Set([definition.file]))?.verdict === 'yes');
14600
+ }
12970
14601
  if (matches.length === 0) {
12971
14602
  const hop = index.importGraph.get(modFile);
12972
14603
  if (hop) matches = cands.filter(d => hop.has(d.file));
@@ -13022,7 +14653,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
13022
14653
  // to the one-hop project-wide agreement rule when the receiver stays
13023
14654
  // untyped.
13024
14655
  if (record.isMethod) {
13025
- let rt = null;
14656
+ let rt = _goBuiltinChainedReceiverType(
14657
+ index, fileEntry, filePath, record);
13026
14658
  if (record.receiverType && !record.receiverIsChainRoot) {
13027
14659
  const origin = nominal
13028
14660
  ? _resolveFlowTypeOrigin(index, filePath, record.receiverType,
@@ -13423,4 +15055,4 @@ function findCallbackUsages(index, name) {
13423
15055
  return usages;
13424
15056
  }
13425
15057
 
13426
- module.exports = { getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup };
15058
+ module.exports = { getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable };