ucn 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/callers.js CHANGED
@@ -12,7 +12,7 @@ const { detectLanguage, getParser, getLanguageModule, langTraits } = require('..
12
12
  const { isTestFile } = require('./discovery');
13
13
  const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath } = require('./shared');
14
14
  const { scoreEdge, tierForResolution, TIER } = require('./confidence');
15
- const { findGoModule } = require('./imports');
15
+ const { findGoModule, resolveRustImport } = require('./imports');
16
16
 
17
17
  /** Set.some() helper — like Array.some() but for Sets */
18
18
  function setSome(set, predicate) {
@@ -172,6 +172,20 @@ function findCallers(index, name, options = {}) {
172
172
  definitionLines.add(`${def.file}:${def.startLine}`);
173
173
  }
174
174
 
175
+ // Overload-signature identity (fix #265, zustand-measured): a pin on any
176
+ // member of an overload group targets the WHOLE group — the signatures
177
+ // and the implementation declare one function, so a call binding the
178
+ // first signature's bindingId IS a call to the pinned implementation
179
+ // (useStore's only caller was excluded other-definition under the
180
+ // implementation pin, a false zero-caller answer).
181
+ if (options.targetDefinitions && options.targetDefinitions.length > 0 &&
182
+ definitions.length > options.targetDefinitions.length) {
183
+ const closed = _closeOverloadGroup(options.targetDefinitions, definitions);
184
+ if (closed !== options.targetDefinitions) {
185
+ options = { ...options, targetDefinitions: closed };
186
+ }
187
+ }
188
+
175
189
  // Possible-dispatch tiering inputs (nominal contract surface) — all fixed
176
190
  // per query, computed lazily once. targetTypes mirrors the receiver-class
177
191
  // disambiguation set (target classes + non-overriding subtypes); owner
@@ -338,7 +352,7 @@ function findCallers(index, name, options = {}) {
338
352
  pendingByFile.get(filePath).push({
339
353
  call, fileEntry, callerSymbol: null,
340
354
  isMethod: call.isMethod || false,
341
- isFunctionReference: !!call.isFunctionReference,
355
+ isFunctionReference: !!(call.isFunctionReference || call.isJsxComponent),
342
356
  receiver: call.receiver, receiverType: call.receiverType,
343
357
  calledAs,
344
358
  _tier: TIER.UNVERIFIED, _reason: reason, _meta: meta,
@@ -357,6 +371,7 @@ function findCallers(index, name, options = {}) {
357
371
  const needsTotal = !!options.needsTotal;
358
372
  const localTypeCache = new Map(); // `${filePath}:${startLine}` -> localTypes Map or null
359
373
  const returnFlowCache = new Map(); // filePath -> return-type-flow map (see _buildReturnTypeFlowMap)
374
+ const foldCtxCache = new Map(); // filePath -> chained-receiver fold context (fix #258)
360
375
 
361
376
  // Use inverted callee index to skip files that don't contain calls to this name
362
377
  let calleeFiles = index.getCalleeFiles(name);
@@ -409,7 +424,9 @@ function findCallers(index, name, options = {}) {
409
424
  // Copy-on-enrich: cached call objects stay parser-pure — the flow
410
425
  // type derives from OTHER files' annotations, so it must never be
411
426
  // persisted with this file's calls.
412
- if (call.isMethod && call.receiver && !call.receiverType &&
427
+ if (call.isMethod && call.receiver &&
428
+ (!call.receiverType || call.receiverTypeGuessed) &&
429
+ !call.receiverIsChainRoot &&
413
430
  (langTraits(fileEntry.language)?.typeSystem === 'structural' ||
414
431
  (collectAccount && !call.isPotentialCallback && !call.isPathCall &&
415
432
  langTraits(fileEntry.language)?.typeSystem === 'nominal'))) {
@@ -423,9 +440,21 @@ function findCallers(index, name, options = {}) {
423
440
  // External producer (fix #220) — typed outside the
424
441
  // project; blocks single-owner confirmation, routes
425
442
  // possible-dispatch in the gate. Nominal-only entries.
443
+ // A parser GUESS for the same variable (fix #266) is
444
+ // noise next to the flow verdict — dropped with it.
426
445
  call = { ...call, receiverExternalFlow: flowEntry.externalVia };
446
+ if (call.receiverTypeGuessed) {
447
+ call = { ...call, receiverType: undefined, receiverTypeGuessed: undefined };
448
+ }
427
449
  } else if (flowEntry) {
450
+ // Compiler-checked return annotation outranks the
451
+ // parser's New*-prefix name guess (fix #266,
452
+ // viper-measured: registry := NewCodecRegistry()
453
+ // guessed 'CodecRegistry' — the interface — while the
454
+ // annotation says *DefaultCodecRegistry; the guess
455
+ // excluded all three true RegisterCodec callers).
428
456
  call = { ...call, receiverType: flowEntry.type,
457
+ receiverTypeGuessed: undefined,
429
458
  ...(flowEntry.fromFile && { receiverTypeFlowFile: flowEntry.fromFile }) };
430
459
  }
431
460
  }
@@ -441,17 +470,58 @@ function findCallers(index, name, options = {}) {
441
470
  // their chained calls stay under the #204 dispatch tiering
442
471
  // (visible, honest) until a measured family justifies the
443
472
  // #207 origin-pinning rails there.
444
- if (call.isMethod && !call.receiver && !call.receiverType && call.receiverCall &&
473
+ if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
474
+ !call.receiverType && call.receiverCall &&
445
475
  langTraits(fileEntry.language)?.typeSystem === 'structural') {
446
- const chainedType = _chainedReceiverType(index, call, fileEntry.language);
447
- if (chainedType) call = { ...call, receiverType: chainedType };
448
- } else if (call.isMethod && !call.receiver && !call.receiverType && call.receiverCall &&
476
+ // Fold first (fix #258 — multi-hop builder chains typed
477
+ // from the producer link), one-hop agreement as fallback.
478
+ let foldCtx = foldCtxCache.get(filePath);
479
+ if (!foldCtx) {
480
+ foldCtx = { memo: new Map(), visiting: new Set(), records: calls,
481
+ getFlowMap: () => {
482
+ let fm = returnFlowCache.get(filePath);
483
+ if (fm === undefined) {
484
+ fm = _buildReturnTypeFlowMap(index, filePath, calls);
485
+ returnFlowCache.set(filePath, fm);
486
+ }
487
+ return fm;
488
+ } };
489
+ foldCtxCache.set(filePath, foldCtx);
490
+ }
491
+ const folded = _foldChainedReceiverType(index, fileEntry, filePath, call, foldCtx);
492
+ if (folded && folded.type) {
493
+ call = { ...call, receiverType: folded.type };
494
+ } else if (folded && folded.externalVia) {
495
+ call = { ...call, receiverExternalFlow: folded.externalVia };
496
+ } else {
497
+ const chainedType = _chainedReceiverType(index, call, fileEntry.language);
498
+ if (chainedType) call = { ...call, receiverType: chainedType };
499
+ }
500
+ } else if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
501
+ !call.receiverType && call.receiverCall &&
449
502
  collectAccount && !call.isPotentialCallback &&
450
503
  langTraits(fileEntry.language)?.typeSystem === 'nominal') {
451
504
  // Nominal chained receivers (fix #220, cobra-measured):
452
505
  // account-gated like the #207 nominal flow — mismatch
453
506
  // reroutes are account-only; legacy would silently drop.
454
- const flowEntry = _nominalChainedReceiverType(index, call, fileEntry, filePath);
507
+ // The fold (fix #258) runs first — Command::new("x")
508
+ // .author(a).arg(b) types hop-by-hop where the one-hop
509
+ // agreement rule dies on multi-owner `Self` returns.
510
+ let foldCtx = foldCtxCache.get(filePath);
511
+ if (!foldCtx) {
512
+ foldCtx = { memo: new Map(), visiting: new Set(), records: calls,
513
+ getFlowMap: () => {
514
+ let fm = returnFlowCache.get(filePath);
515
+ if (fm === undefined) {
516
+ fm = _buildReturnTypeFlowMap(index, filePath, calls);
517
+ returnFlowCache.set(filePath, fm);
518
+ }
519
+ return fm;
520
+ } };
521
+ foldCtxCache.set(filePath, foldCtx);
522
+ }
523
+ const flowEntry = _foldChainedReceiverType(index, fileEntry, filePath, call, foldCtx)
524
+ || _nominalChainedReceiverType(index, call, fileEntry, filePath);
455
525
  if (flowEntry && flowEntry.externalVia) {
456
526
  call = { ...call, receiverExternalFlow: flowEntry.externalVia };
457
527
  } else if (flowEntry) {
@@ -523,6 +593,22 @@ function findCallers(index, name, options = {}) {
523
593
  }
524
594
  }
525
595
 
596
+ // A member access on a typed receiver whose member of this
597
+ // name is a provably non-callable FIELD denotes the field,
598
+ // never a same-name symbol elsewhere (fix #266,
599
+ // viper-measured: `delete(v.override, alias)` on Viper's
600
+ // map field scope-confirmed against the test-file function
601
+ // `override` — the #231(2) callee physics, caller side).
602
+ // Only certainty excludes: _nonCallableFieldMember demands
603
+ // every same-type member be a field with a present
604
+ // non-function type. Guess-grade receiver types (#266)
605
+ // never carry it.
606
+ if (call.isMethod && call.receiverType && !call.receiverTypeGuessed &&
607
+ _nonCallableFieldMember(index, call.receiverType, call.name, fileEntry.language)) {
608
+ recordExcluded(filePath, call.line, 'member-reference');
609
+ continue;
610
+ }
611
+
526
612
  // A bare identifier can never denote a METHOD where bare
527
613
  // names don't reach methods (fix #220, grpc-go-measured:
528
614
  // `balancer.Get(Name)` references each package's const
@@ -580,7 +666,10 @@ function findCallers(index, name, options = {}) {
580
666
  // the ground line surfaced as call-not-resolved
581
667
  // (grpc-go/cursive-measured).
582
668
  if (collectAccount) {
583
- if (_dispatchCapableSupertype(index, fileEntry.language, call.receiverType, cbTargetDefs, definitions)) {
669
+ // Guess-grade types (fix #266) route, never
670
+ // exclude — convention is not compiler truth.
671
+ if (call.receiverTypeGuessed ||
672
+ _dispatchCapableSupertype(index, fileEntry.language, call.receiverType, cbTargetDefs, definitions)) {
584
673
  routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
585
674
  dispatchVia: call.receiverType,
586
675
  dispatchCandidates: countDispatchCandidates(call.receiverType),
@@ -1066,7 +1155,16 @@ function findCallers(index, name, options = {}) {
1066
1155
  }
1067
1156
  if (call.isMethod && !call.receiverType && call.receiverField && fieldHopRootType &&
1068
1157
  !resolvedBySameClass) {
1069
- fieldHopType = _declaredFieldType(index, fieldHopRootType, call.receiverField, fileEntry.language);
1158
+ const hopInfo = {};
1159
+ fieldHopType = _declaredFieldType(index, fieldHopRootType, call.receiverField, fileEntry.language, hopInfo);
1160
+ // Provably-external declared field type (fix #268,
1161
+ // chi-measured: `inner http.Handler` on a pin named
1162
+ // Handler elsewhere in the project) — rides the #220(6)
1163
+ // external-flow rail: defeats single-owner confirmation,
1164
+ // routes possible-dispatch, never excludes.
1165
+ if (!fieldHopType && hopInfo.externalVia && !call.receiverExternalFlow) {
1166
+ call = { ...call, receiverExternalFlow: hopInfo.externalVia };
1167
+ }
1070
1168
  }
1071
1169
  // Dispatch attribution (contract surface only): a field DECLARED
1072
1170
  // as a project interface/trait carries no exclusion evidence
@@ -1120,7 +1218,26 @@ function findCallers(index, name, options = {}) {
1120
1218
  // receiver-class disambiguation below).
1121
1219
  const fieldHopMatchesTarget = fieldHopType && targetDefs.some(d =>
1122
1220
  (d.className || (d.receiver || '').replace(/^\*/, '')) === fieldHopType);
1123
- if (!fieldHopMatchesTarget) {
1221
+ // Java arity-overload groups are ONE bindable name (fix
1222
+ // #268, javapoet-measured): the bare delegation
1223
+ // `defaultValue(Code.of(...))` inside Builder name-bound
1224
+ // the same-class FIELD (or the sibling overload) and was
1225
+ // excluded other-definition — a false zero-caller answer.
1226
+ // When the resolved binding's def sits in the pin's
1227
+ // same-class callable overload family, binding identity
1228
+ // proves nothing; the #205 overload discipline below
1229
+ // adjudicates (fits/sibling/ambiguous). Fields never
1230
+ // receive Java calls (separate namespaces), so a
1231
+ // field-binding hit is treated the same way.
1232
+ let overloadGroupBinding = false;
1233
+ if (!fieldHopMatchesTarget &&
1234
+ langTraits(fileEntry.language)?.hasArityOverloads && !call.receiver) {
1235
+ const boundDef = definitions.find(d => d.bindingId === bindingId);
1236
+ overloadGroupBinding = !!(boundDef && boundDef.className &&
1237
+ targetDefs.some(d => d.className === boundDef.className &&
1238
+ d.file === boundDef.file));
1239
+ }
1240
+ if (!fieldHopMatchesTarget && !overloadGroupBinding) {
1124
1241
  recordExcluded(filePath, call.line, 'other-definition');
1125
1242
  continue;
1126
1243
  }
@@ -1142,7 +1259,23 @@ function findCallers(index, name, options = {}) {
1142
1259
  if (!bindingId && !call.isMethod && !calledAs &&
1143
1260
  langTraits(fileEntry.language)?.typeSystem === 'structural' &&
1144
1261
  (fileEntry.importBindings || []).length > 0) {
1145
- const nameBindings = fileEntry.importBindings.filter(b => b.name === call.name);
1262
+ // Renamed destructures look up by the RESOLVED name (fix
1263
+ // #269, fastify-measured): `const { validate:
1264
+ // validateSchema } = require('./validation')` stores the
1265
+ // binding under 'validate' while the record's name is the
1266
+ // local alias — the empty lookup skipped the whole
1267
+ // ownership chase and the call scope-confirmed against
1268
+ // hooks.js's unrelated validate.
1269
+ const lookupName = call.resolvedName || call.name;
1270
+ let nameBindings = fileEntry.importBindings.filter(b => b.name === lookupName);
1271
+ // Exact alias pairing (fix #269): two renames of one
1272
+ // source name from DIFFERENT modules — the record's local
1273
+ // alias (call.name) picks its own binding; source-name
1274
+ // matching alone over-follows into the other module.
1275
+ if (call.resolvedName && nameBindings.some(b => b.alias)) {
1276
+ const paired = nameBindings.filter(b => b.alias === call.name);
1277
+ if (paired.length > 0) nameBindings = paired;
1278
+ }
1146
1279
  const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
1147
1280
  // fix #215 (rich-measured: 225 builtin `print(...)` calls
1148
1281
  // confirmed against rich's def via file-level import edges):
@@ -1215,6 +1348,20 @@ function findCallers(index, name, options = {}) {
1215
1348
  recordExcluded(filePath, call.line, 'other-definition-import');
1216
1349
  continue;
1217
1350
  }
1351
+ // Paired-rename records (fix #269, fastify-measured):
1352
+ // `{ validate: validateSchema } = require('./validation')`
1353
+ // pins the alias to ONE module. An un-modelable chain
1354
+ // (CJS export surface) blocks exclusion but is not
1355
+ // confirmation evidence either — the call routes
1356
+ // visible instead of scope-confirming a pin its own
1357
+ // module never provably reaches (hooks.js's unrelated
1358
+ // validate confirmed at scope-match 0.65).
1359
+ if (!reaches && undetermined && collectAccount &&
1360
+ call.resolvedName && nameBindings.length > 0 &&
1361
+ nameBindings.every(b => b.alias === call.name)) {
1362
+ routeUnverified(filePath, fileEntry, call, 'ambiguous-binding', calledAs);
1363
+ continue;
1364
+ }
1218
1365
  }
1219
1366
  }
1220
1367
 
@@ -1607,9 +1754,17 @@ function findCallers(index, name, options = {}) {
1607
1754
  !(index.symbols.get(knownType) || []).some(d =>
1608
1755
  d.type === 'class' || d.type === 'struct' || d.type === 'interface' || d.type === 'trait') &&
1609
1756
  _targetAncestryFullyResolved(index, targetDefs));
1757
+ // A New*-prefix name GUESS (fix #266) is convention,
1758
+ // not compiler truth — never exclusion evidence.
1759
+ // Account mode routes the mismatch visible through
1760
+ // the dispatch gate below; legacy keeps its drop
1761
+ // (drop-vs-route asymmetry, the #213 pattern).
1762
+ const receiverGuessGrade = !!(call.receiverTypeGuessed ||
1763
+ (viaFieldHop && call.receiverRootTypeGuessed));
1610
1764
  const exclusionTrusted = (!structural ||
1611
1765
  _receiverTypeTrustedForExclusion(index, knownType)) && fieldHopDefinesMethod &&
1612
- !receiverTypeUnresolved; // unresolvable identity is not positive evidence either way
1766
+ !receiverTypeUnresolved && // unresolvable identity is not positive evidence either way
1767
+ !(collectAccount && receiverGuessGrade);
1613
1768
  if (!matchesTarget && exclusionTrusted) {
1614
1769
  // Known type doesn't match target — positive evidence the
1615
1770
  // call targets a DIFFERENT symbol. Under the account contract
@@ -1639,6 +1794,48 @@ function findCallers(index, name, options = {}) {
1639
1794
  continue;
1640
1795
  }
1641
1796
  }
1797
+ // A guessed type that mismatches ROUTES visible
1798
+ // (fix #266): the pre-guard flow reached the
1799
+ // dispatch-capable reroute INSIDE the exclusion
1800
+ // branch, so denying exclusionTrusted alone let
1801
+ // the call fall through to the arity/scope
1802
+ // machinery below (grpc-go: stream.SendMsg on a
1803
+ // guessed 'Stream' fell to arity-mismatch — a
1804
+ // harsher wrong exclusion). Convention-grade
1805
+ // evidence attributes dispatch; it never excludes
1806
+ // and never confirms a mismatch.
1807
+ if (!matchesTarget && !exclusionTrusted && collectAccount && receiverGuessGrade) {
1808
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
1809
+ dispatchVia: knownType,
1810
+ dispatchCandidates: countDispatchCandidates(knownType),
1811
+ });
1812
+ continue;
1813
+ }
1814
+ // Unmatched-but-untrusted field-hop type (fix #265,
1815
+ // hono-measured): the hop PROVED the receiver is the
1816
+ // field's declared type — not the enclosing class —
1817
+ // yet the head earns no exclusion trust (external/
1818
+ // unresolvable name). Falling through let the class-
1819
+ // member name binding confirm `this.store.keys()`
1820
+ // as MockCache.keys ('exact-binding'): bindings
1821
+ // don't model receivers (#202). Route visible,
1822
+ // attributed via the field's type; never confirm,
1823
+ // never exclude. Structural only — nominal hops
1824
+ // keep their #202 promotion/embedding fall-through
1825
+ // (no measured family).
1826
+ if (!matchesTarget && !exclusionTrusted && viaFieldHop && structural) {
1827
+ if (collectAccount) {
1828
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
1829
+ dispatchVia: knownType,
1830
+ dispatchCandidates: countDispatchCandidates(knownType),
1831
+ });
1832
+ continue;
1833
+ }
1834
+ if (!options.includeUncertain) {
1835
+ if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
1836
+ continue;
1837
+ }
1838
+ }
1642
1839
  } else {
1643
1840
  // No parser-inferred type — try local type inference
1644
1841
  // for Go/Java/Rust (nominal type systems)
@@ -1665,6 +1862,12 @@ function findCallers(index, name, options = {}) {
1665
1862
  let inferredType = localTypes.get(call.receiver);
1666
1863
  if (inferredType && _isGenericParamReceiverType(
1667
1864
  index, filePath, call.line, inferredType)) inferredType = null;
1865
+ // New*-guess entries (fix #266) may confirm
1866
+ // but never exclude: a mismatch routes as
1867
+ // unresolved under the account contract
1868
+ // (legacy keeps the drop — #213 asymmetry).
1869
+ const inferredGuessed = !!(inferredType &&
1870
+ localTypes.guessedVars?.has(call.receiver));
1668
1871
  if (inferredType) {
1669
1872
  if (targetTypes.has(inferredType)) {
1670
1873
  // Identity discipline (fix #206) — same as the
@@ -1679,12 +1882,14 @@ function findCallers(index, name, options = {}) {
1679
1882
  inferredMatch = true;
1680
1883
  nominalInferredMatch = true;
1681
1884
  } else if (identity === 'other') {
1682
- inferredMismatch = true;
1885
+ if (collectAccount && inferredGuessed) receiverTypeUnresolved = true;
1886
+ else inferredMismatch = true;
1683
1887
  } else {
1684
1888
  receiverTypeUnresolved = true;
1685
1889
  }
1686
1890
  } else {
1687
- inferredMismatch = true;
1891
+ if (collectAccount && inferredGuessed) receiverTypeUnresolved = true;
1892
+ else inferredMismatch = true;
1688
1893
  }
1689
1894
  }
1690
1895
  }
@@ -2063,15 +2268,106 @@ function findCallers(index, name, options = {}) {
2063
2268
  recordExcluded(filePath, call.line, 'path-type-mismatch');
2064
2269
  continue;
2065
2270
  }
2271
+ // Module-qualified path calls (fix #260, clap-measured):
2272
+ // a LOWERCASE path qualifier names a MODULE, and the
2273
+ // module owns the name (#206 ownership) — clap_mangen's
2274
+ // `render::version(&self.cmd)` is render.rs's function,
2275
+ // never Command::version, yet bare-name scope evidence
2276
+ // confirmed it against the method pin. The qualifier's
2277
+ // last segment resolves through this file's import/mod
2278
+ // edges (basename match, mod.rs-aware): pin inside the
2279
+ // module → normal confirmation; module defines the name
2280
+ // elsewhere → excluded other-definition; resolver gap or
2281
+ // unresolvable qualifier → visible, never scope-confirmed
2282
+ // (#206(4): a qualified call never earns the target's
2283
+ // bare-identifier scope evidence). crate/self/super are
2284
+ // scope keywords, not module names — exempt.
2285
+ if (call.isPathCall && call.receiver) {
2286
+ const _modSeg = String(call.receiver).split('::').pop();
2287
+ if (_modSeg && !/^[A-Z]/.test(_modSeg) &&
2288
+ !['crate', 'self', 'super'].includes(_modSeg)) {
2289
+ // Inline module container first (fix #260b —
2290
+ // the #254 machinery): `convert::string(...)`
2291
+ // where `mod convert { pub fn string }` lives
2292
+ // in the SAME file (ripgrep defs.rs).
2293
+ // Containment is identity evidence: the pin
2294
+ // inside the module confirms; a same-name def
2295
+ // inside it that ISN'T the pin excludes.
2296
+ const _nsPin = _namespaceContainedDef(index, fileEntry, filePath,
2297
+ _modSeg, name, targetDefs2);
2298
+ if (_nsPin) {
2299
+ call = { ...call, moduleOwnedPath: true };
2300
+ } else if (_namespaceContainedDef(index, fileEntry, filePath,
2301
+ _modSeg, name, null)) {
2302
+ recordExcluded(filePath, call.line, 'other-definition');
2303
+ continue;
2304
+ } else {
2305
+ const _edges = index.importGraph.get(filePath);
2306
+ const _modFiles = [];
2307
+ if (_edges) {
2308
+ for (const e of _edges) {
2309
+ const base = path.basename(e).replace(/\.rs$/, '');
2310
+ if (base === _modSeg ||
2311
+ (base === 'mod' && path.basename(path.dirname(e)) === _modSeg)) {
2312
+ _modFiles.push(e);
2313
+ }
2314
+ }
2315
+ }
2316
+ // Scope-rooted qualifiers (fix #260b,
2317
+ // ripgrep-measured): crate::messages::
2318
+ // set_errored() needs no use statement, so
2319
+ // there is no import edge — resolve the full
2320
+ // module path with the machinery imports use
2321
+ // (crate::/super::/self::, own-package names,
2322
+ // mod siblings, manifest target roots).
2323
+ if (_modFiles.length === 0) {
2324
+ try {
2325
+ const r = resolveRustImport(String(call.receiver), filePath, index.root);
2326
+ if (r && index.files.has(r)) _modFiles.push(r);
2327
+ } catch { /* resolver gap — never exclusion evidence */ }
2328
+ }
2329
+ const _pinnedIn = _modFiles.length > 0 &&
2330
+ targetDefs2.some(d => _modFiles.includes(d.file));
2331
+ if (!_pinnedIn) {
2332
+ const _ownsName = _modFiles.some(f => {
2333
+ const fe2 = index.files.get(f);
2334
+ return fe2 && fe2.symbols && fe2.symbols.some(s =>
2335
+ s.name === name && !NON_CALLABLE_TYPES.has(s.type));
2336
+ });
2337
+ if (_ownsName) {
2338
+ recordExcluded(filePath, call.line, 'other-definition');
2339
+ continue;
2340
+ }
2341
+ routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
2342
+ dispatchCandidates: methodOwnerKeys().size,
2343
+ });
2344
+ continue;
2345
+ }
2346
+ // Pinned target lives in the qualifier's
2347
+ // module — ownership is compiler-grade
2348
+ // name-level evidence (Rust resolves
2349
+ // crate::util::helper to util.rs's helper,
2350
+ // period): carry it as import-grade
2351
+ // evidence so a pure path-only usage
2352
+ // confirms even with no use-statement edge.
2353
+ call = { ...call, moduleOwnedPath: true };
2354
+ }
2355
+ }
2356
+ }
2066
2357
  const knownDispatchType = call.receiverType || fieldHopType || fieldDispatchType;
2067
- if (knownDispatchType && !tTypes.has(knownDispatchType)) {
2358
+ // Module-owned qualified calls (fix #260b) are resolved
2359
+ // by OWNERSHIP — the dispatch-ambiguity routing below is
2360
+ // receiver physics for method calls and must not demote
2361
+ // them (ripgrep: `convert::string(...)` tripped the
2362
+ // multi-owner check via unrelated `string` methods).
2363
+ if (!call.moduleOwnedPath && knownDispatchType && !tTypes.has(knownDispatchType)) {
2068
2364
  routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
2069
2365
  dispatchVia: knownDispatchType,
2070
2366
  dispatchCandidates: countDispatchCandidates(knownDispatchType),
2071
2367
  });
2072
2368
  continue;
2073
2369
  }
2074
- if (!knownDispatchType && methodOwnerKeys().size > 1) {
2370
+ if (!call.moduleOwnedPath && !knownDispatchType && methodOwnerKeys().size > 1) {
2075
2371
  routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
2076
2372
  dispatchCandidates: methodOwnerKeys().size,
2077
2373
  });
@@ -2091,6 +2387,19 @@ function findCallers(index, name, options = {}) {
2091
2387
  });
2092
2388
  continue;
2093
2389
  }
2390
+ // java.lang.Object universal names (fix #265 — the
2391
+ // structural gate's twin): toString/equals/hashCode
2392
+ // are satisfied by EVERY value via Object, marker or
2393
+ // not — unique project ownership is not identity
2394
+ // evidence for a receiver-evidence-free call.
2395
+ if (!knownDispatchType &&
2396
+ _universalMethodName(fileEntry.language, call.name)) {
2397
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
2398
+ dispatchVia: `${_universalRootName(fileEntry.language)} — builtin contract`,
2399
+ externalContract: true,
2400
+ });
2401
+ continue;
2402
+ }
2094
2403
  // Wrong-arity call that fits no project def (fix #229
2095
2404
  // carve-out marker): the single-owner rule presumes no
2096
2405
  // other candidate exists, but the arity disproves the
@@ -2317,6 +2626,19 @@ function findCallers(index, name, options = {}) {
2317
2626
  });
2318
2627
  continue;
2319
2628
  }
2629
+ // Universal-contract names (fix #265): Object.prototype
2630
+ // (JS) / object dunders (Python) — every external value
2631
+ // satisfies the call, so unique project ownership below
2632
+ // is not receiver evidence. Demote-only.
2633
+ if (!typeQualifiedReceiver && _universalMethodName(fileEntry.language, call.name)) {
2634
+ const univVia = call.receiverType || fieldHopType || fieldDispatchType;
2635
+ routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
2636
+ dispatchVia: univVia ||
2637
+ `${_universalRootName(fileEntry.language)} — builtin contract`,
2638
+ externalContract: true,
2639
+ });
2640
+ continue;
2641
+ }
2320
2642
  if (!typeQualifiedReceiver && methodOwnerKeys().size > 1) {
2321
2643
  const knownDispatchType = call.receiverType || fieldHopType || fieldDispatchType;
2322
2644
  if (knownDispatchType) {
@@ -2377,8 +2699,11 @@ function findCallers(index, name, options = {}) {
2377
2699
  isMethod: call.isMethod || false,
2378
2700
  // Function references can resolve through the plain binding
2379
2701
  // path too (e.g. JS `arr.map(helper)` with a local binding) —
2380
- // surface the parser's marker on the edge (fix #221).
2381
- isFunctionReference: !!call.isFunctionReference,
2702
+ // surface the parser's marker on the edge (fix #221). JSX
2703
+ // component usage is the same shape (fix #265): `<App />`
2704
+ // compiles to jsx(App, ...) — a reference the runtime
2705
+ // invokes, not direct call syntax.
2706
+ isFunctionReference: !!(call.isFunctionReference || call.isJsxComponent),
2382
2707
  receiver: call.receiver,
2383
2708
  receiverType: call.receiverType,
2384
2709
  calledAs,
@@ -2394,7 +2719,7 @@ function findCallers(index, name, options = {}) {
2394
2719
  : !!call.receiverType,
2395
2720
  hasReceiverEvidence: !!(call.receiver &&
2396
2721
  (fileEntry.bindings || []).some(b => b.name === call.receiver)),
2397
- hasImportEvidence: !!bindingId || hasImportLink,
2722
+ hasImportEvidence: !!bindingId || hasImportLink || !!call.moduleOwnedPath,
2398
2723
  ...(typeMismatch && { typeMismatch: true }),
2399
2724
  }
2400
2725
  });
@@ -2690,6 +3015,16 @@ function findCallees(index, def, options = {}) {
2690
3015
  if (_flowMap === undefined) _flowMap = _buildReturnTypeFlowMap(index, def.file, calls);
2691
3016
  return _flowMap;
2692
3017
  };
3018
+ // Chained-receiver fold context (fix #268 — the #258 rails, callee
3019
+ // direction): built lazily, shared across this def's records.
3020
+ let calleeFoldCtx = null;
3021
+ const foldCtx = () => {
3022
+ if (!calleeFoldCtx) {
3023
+ calleeFoldCtx = { memo: new Map(), visiting: new Set(), records: calls,
3024
+ getFlowMap: () => flowMap() };
3025
+ }
3026
+ return calleeFoldCtx;
3027
+ };
2693
3028
 
2694
3029
  let siteOrdinal = -1;
2695
3030
  for (const call of calls) {
@@ -2720,13 +3055,17 @@ function findCallees(index, def, options = {}) {
2720
3055
  // to nothing). _declaredFieldType's guards apply: interface/
2721
3056
  // trait-typed and generic-param fields return null.
2722
3057
  let fieldHopType = null;
3058
+ let fieldHopInfo = null;
2723
3059
  if (call.isMethod && !call.receiverType && call.receiverField) {
2724
3060
  let hopRoot = call.receiverRootType;
2725
3061
  if (!hopRoot && call.receiverRoot === 'this' &&
2726
3062
  langTraits(language)?.typeSystem === 'structural') {
2727
3063
  hopRoot = index.findEnclosingFunction(def.file, call.line, true)?.className;
2728
3064
  }
2729
- if (hopRoot) fieldHopType = _declaredFieldType(index, hopRoot, call.receiverField, language);
3065
+ if (hopRoot) {
3066
+ fieldHopInfo = {};
3067
+ fieldHopType = _declaredFieldType(index, hopRoot, call.receiverField, language, fieldHopInfo);
3068
+ }
2730
3069
  }
2731
3070
 
2732
3071
  // Go package-qualified receiver: resolve the import module up
@@ -2734,9 +3073,14 @@ function findCallees(index, def, options = {}) {
2734
3073
  // type-qualified method expressions (fix #236 — a receiver that
2735
3074
  // is neither stays eligible for type-qualified resolution below).
2736
3075
  let goImportModule = null;
2737
- if (call.isMethod && call.receiver && langTraits(language)?.hasReceiverPackageCalls) {
3076
+ if (call.receiver && langTraits(language)?.hasReceiverPackageCalls) {
2738
3077
  const goImports = fileEntry?.imports || [];
2739
3078
  // Handle Go version suffixes: k8s.io/klog/v2 → klog, not v2
3079
+ // fix #268 (chi-measured): computed for NON-method records
3080
+ // too — the parser marks `context.WithValue(...)` isMethod:
3081
+ // false, which used to skip the whole package-ownership
3082
+ // dispatch and confirm the bare name against the project's
3083
+ // only WithValue def (middleware/value.go).
2740
3084
  goImportModule = goImports.find(mod => {
2741
3085
  const parts = mod.split('/');
2742
3086
  const last = parts[parts.length - 1];
@@ -2758,6 +3102,30 @@ function findCallees(index, def, options = {}) {
2758
3102
  typeQual = _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language);
2759
3103
  }
2760
3104
 
3105
+ // Package-qualified NON-method records (fix #268, chi-measured):
3106
+ // the parser marks `context.WithValue(...)` isMethod:false, so it
3107
+ // used to skip the package-ownership dispatch entirely and
3108
+ // confirm as a bare name (the project's only WithValue def).
3109
+ // The qualifier owns the name (#206/#209): resolve into the
3110
+ // imported package or route external — never bare-name scope.
3111
+ if (!call.isMethod && goImportModule && !call.isFunctionReference) {
3112
+ const match = _calleeGoPackageMatch(index, call, goImportModule);
3113
+ if (match) {
3114
+ const key = match.bindingId || `${call.receiver}.${call.name}`;
3115
+ const existing = callees.get(key);
3116
+ if (existing) {
3117
+ existing.count += 1;
3118
+ if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
3119
+ } else {
3120
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1,
3121
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
3122
+ }
3123
+ } else {
3124
+ noteSite(siteId, 'external', null, call);
3125
+ }
3126
+ continue;
3127
+ }
3128
+
2761
3129
  // Smart method call handling:
2762
3130
  // - Go: include all method calls (Go doesn't use this/self/cls)
2763
3131
  // - self/this.method(): resolve to same-class method (handled below)
@@ -2780,23 +3148,29 @@ function findCallees(index, def, options = {}) {
2780
3148
  const symbols = index.symbols.get(call.name);
2781
3149
  const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
2782
3150
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
2783
- let match = symbols?.find(s =>
3151
+ const onClass = (cls) => (symbols || []).filter(s =>
2784
3152
  isCallable(s) && (
2785
- s.className === typeName ||
2786
- (s.receiver && s.receiver.replace(/^\*/, '') === typeName)));
3153
+ s.className === cls ||
3154
+ (s.receiver && s.receiver.replace(/^\*/, '') === cls)));
3155
+ // Same-class overloads select by static call shape —
3156
+ // arity, then Java argKinds (fix #268); an undecidable
3157
+ // family routes visible instead of binding defs[0].
3158
+ let sel = _calleeOverloadSelect(index, call, onClass(typeName), language);
2787
3159
  // Walk embedding/inheritance chain if no direct match (nominal type systems)
2788
- if (!match && langTraits(language)?.typeSystem === 'nominal') {
3160
+ if (!sel.match && !sel.ambiguous && langTraits(language)?.typeSystem === 'nominal') {
2789
3161
  const parentNames = index._getInheritanceParents?.(typeName, def.file);
2790
3162
  if (parentNames) {
2791
3163
  for (const pName of parentNames) {
2792
- match = symbols?.find(s =>
2793
- isCallable(s) && (
2794
- s.className === pName ||
2795
- (s.receiver && s.receiver.replace(/^\*/, '') === pName)));
2796
- if (match) break;
3164
+ sel = _calleeOverloadSelect(index, call, onClass(pName), language);
3165
+ if (sel.match || sel.ambiguous) break;
2797
3166
  }
2798
3167
  }
2799
3168
  }
3169
+ if (sel.ambiguous) {
3170
+ noteUnverified(siteId, call, 'overload-ambiguous');
3171
+ continue;
3172
+ }
3173
+ const match = sel.match;
2800
3174
  if (match) {
2801
3175
  const key = match.bindingId || `${typeName}.${call.name}`;
2802
3176
  const existing = callees.get(key);
@@ -2815,13 +3189,17 @@ function findCallees(index, def, options = {}) {
2815
3189
  // CacheService's map-typed field, which shadows any
2816
3190
  // same-named project function through this receiver).
2817
3191
  noteSite(siteId, 'excluded', 'member-reference', call);
3192
+ } else if (_calleeZeroCandidateName(index, call)) {
3193
+ // fix #261: the type defines no such method AND the
3194
+ // name has zero project defs anywhere — external.
3195
+ noteSite(siteId, 'external', null, call);
2818
3196
  } else if (collectAccount) {
2819
3197
  // Locally-typed receiver, but the type defines no such
2820
3198
  // method in the index — visible, never silently dropped.
2821
3199
  noteUnverified(siteId, call, 'uncertain-receiver');
2822
3200
  }
2823
3201
  continue;
2824
- } else if (call.receiverType || fieldHopType) {
3202
+ } else if (call.receiverType || fieldHopType || fieldHopInfo?.externalVia) {
2825
3203
  // Use parser-inferred receiverType for method resolution
2826
3204
  // Go/Java/Rust: from param/receiver type declarations
2827
3205
  // JS/TS: from `new Foo()` assignments or TypeScript type annotations
@@ -2829,27 +3207,45 @@ function findCallees(index, def, options = {}) {
2829
3207
  // fieldHopType: the declared type of a one-hop field
2830
3208
  // receiver (fix #231 — tm.service.Save() resolves Save
2831
3209
  // through the `service *DataService` declaration)
3210
+ if (!call.receiverType && !fieldHopType && fieldHopInfo?.externalVia) {
3211
+ // Provably-external declared field type (fix #268,
3212
+ // chi-measured: `pool *sync.Pool`, `inner
3213
+ // http.Handler`) — never bare-name identity. Visible
3214
+ // when project owners of the name exist (an external
3215
+ // interface may dispatch into a project impl),
3216
+ // external otherwise.
3217
+ if ((index.symbols.get(call.name) || []).some(s => !NON_CALLABLE_TYPES.has(s.type))) {
3218
+ noteUnverified(siteId, call, 'possible-dispatch');
3219
+ } else {
3220
+ noteSite(siteId, 'external', null, call);
3221
+ }
3222
+ continue;
3223
+ }
2832
3224
  const typeName = call.receiverType || fieldHopType;
2833
3225
  const symbols = index.symbols.get(call.name);
2834
3226
  const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
2835
3227
  (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
2836
- let match = symbols?.find(s =>
3228
+ const onClassRT = (cls) => (symbols || []).filter(s =>
2837
3229
  isCallableRT(s) && (
2838
- (s.receiver && s.receiver.replace(/^\*/, '') === typeName) ||
2839
- s.className === typeName));
3230
+ (s.receiver && s.receiver.replace(/^\*/, '') === cls) ||
3231
+ s.className === cls));
3232
+ // Same-class overload selection by static call shape (fix #268)
3233
+ let sel = _calleeOverloadSelect(index, call, onClassRT(typeName), language);
2840
3234
  // Walk embedding/inheritance chain if no direct match (nominal type systems)
2841
- if (!match && langTraits(language)?.typeSystem === 'nominal') {
3235
+ if (!sel.match && !sel.ambiguous && langTraits(language)?.typeSystem === 'nominal') {
2842
3236
  const parentNames = index._getInheritanceParents?.(typeName, def.file);
2843
3237
  if (parentNames) {
2844
3238
  for (const pName of parentNames) {
2845
- match = symbols?.find(s =>
2846
- isCallableRT(s) && (
2847
- (s.receiver && s.receiver.replace(/^\*/, '') === pName) ||
2848
- s.className === pName));
2849
- if (match) break;
3239
+ sel = _calleeOverloadSelect(index, call, onClassRT(pName), language);
3240
+ if (sel.match || sel.ambiguous) break;
2850
3241
  }
2851
3242
  }
2852
3243
  }
3244
+ if (sel.ambiguous) {
3245
+ noteUnverified(siteId, call, 'overload-ambiguous');
3246
+ continue;
3247
+ }
3248
+ const match = sel.match;
2853
3249
  if (match) {
2854
3250
  const key = match.bindingId || `${typeName}.${call.name}`;
2855
3251
  const existing = callees.get(key);
@@ -2875,55 +3271,99 @@ function findCallees(index, def, options = {}) {
2875
3271
  noteSite(siteId, 'external', null, call);
2876
3272
  continue;
2877
3273
  }
3274
+ // An ATTEMPTED field hop that resolves to a type defining
3275
+ // no such member never falls through to name/binding
3276
+ // resolution (fix #268 — the #265(C) rule, callee
3277
+ // direction: `mx.pool.Get()` hop-missed and the same-file
3278
+ // Mux.Get binding confirmed it). Zero-candidate names are
3279
+ // external (#261); anything else stays visible.
3280
+ if (fieldHopType) {
3281
+ if (_calleeZeroCandidateName(index, call)) {
3282
+ noteSite(siteId, 'external', null, call);
3283
+ } else {
3284
+ noteUnverified(siteId, call, 'uncertain-receiver');
3285
+ }
3286
+ continue;
3287
+ }
2878
3288
  // No match found with inferred type — fall through to include as unresolved
2879
- } else if (goImportModule) {
2880
- // Go package-qualified calls: klog.Infof(), wait.UntilWithContext()
2881
- // The receiver is an import alias (resolved above) — find
2882
- // definitions from that package.
2883
- const importModule = goImportModule;
2884
- {
2885
- // Receiver is an import aliasresolve to definitions from that package
3289
+ } else if (call.receiverCall && !call.receiver && !call.isConstructor &&
3290
+ langTraits(language)?.typeSystem === 'nominal') {
3291
+ // Chained receiver, nominal callee direction (fix #268,
3292
+ // chi-measured): `m.NotFound().ServeHTTP(w, r)` /
3293
+ // `r.Context().Value(k)` the producer's compiler-checked
3294
+ // return annotation types the receiver (#207 rails), and
3295
+ // an untypable chain routes visiblenever bare-name or
3296
+ // binding resolution (the same-file Mux.ServeHTTP binding
3297
+ // confirmed a self-edge; a single-def project `Value`
3298
+ // confirmed for context.Context's).
3299
+ let chained = _foldChainedReceiverType(index, fileEntry, def.file, call, foldCtx());
3300
+ if (!chained || (!chained.type && !chained.externalVia)) {
3301
+ chained = _nominalChainedReceiverType(index, call, fileEntry, def.file);
3302
+ }
3303
+ if (chained?.type) {
2886
3304
  const symbols = index.symbols.get(call.name);
2887
- if (symbols) {
2888
- // Match by checking if the definition's directory path matches the import path suffix.
2889
- // Pick the symbol with the LONGEST suffix match to avoid false positives
2890
- // (e.g., import "k8s.io/client-go/kubernetes/scheme" should prefer a definition
2891
- // in .../client-go/kubernetes/scheme/ over one in .../kubeadm/scheme/).
2892
- const importParts = importModule.split('/');
2893
- let bestMatch = null;
2894
- let bestMatchLen = 0;
2895
- for (const s of symbols) {
2896
- const sDir = path.dirname(s.relativePath || path.relative(index.root, s.file));
2897
- for (let i = 0; i < importParts.length; i++) {
2898
- const suffix = importParts.slice(i).join('/');
2899
- if (sDir === suffix || sDir.endsWith('/' + suffix)) {
2900
- const matchLen = importParts.length - i;
2901
- if (matchLen > bestMatchLen) {
2902
- bestMatchLen = matchLen;
2903
- bestMatch = s;
2904
- }
2905
- break; // this symbol's best suffix found, try next
2906
- }
3305
+ const isCallableCh = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
3306
+ (s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
3307
+ const onClassCh = (cls) => (symbols || []).filter(s =>
3308
+ isCallableCh(s) && (
3309
+ (s.receiver && s.receiver.replace(/^\*/, '') === cls) ||
3310
+ s.className === cls));
3311
+ let sel = _calleeOverloadSelect(index, call, onClassCh(chained.type), language);
3312
+ if (!sel.match && !sel.ambiguous) {
3313
+ const parentNames = index._getInheritanceParents?.(chained.type, def.file);
3314
+ if (parentNames) {
3315
+ for (const pName of parentNames) {
3316
+ sel = _calleeOverloadSelect(index, call, onClassCh(pName), language);
3317
+ if (sel.match || sel.ambiguous) break;
2907
3318
  }
2908
3319
  }
2909
- const match = bestMatch;
2910
- if (match) {
2911
- const key = match.bindingId || `${call.receiver}.${call.name}`;
2912
- const existing = callees.get(key);
2913
- if (existing) {
2914
- existing.count += 1;
2915
- if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
2916
- } else {
2917
- callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1,
2918
- ...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
2919
- }
2920
- continue;
3320
+ }
3321
+ if (sel.match) {
3322
+ const match = sel.match;
3323
+ const key = match.bindingId || `${chained.type}.${call.name}`;
3324
+ const existing = callees.get(key);
3325
+ if (existing) {
3326
+ existing.count += 1;
3327
+ if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
3328
+ } else {
3329
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1,
3330
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
2921
3331
  }
3332
+ continue;
3333
+ }
3334
+ if (sel.ambiguous) {
3335
+ noteUnverified(siteId, call, 'overload-ambiguous');
3336
+ continue;
2922
3337
  }
2923
- // Import resolved but no project definition matches — external call, skip
3338
+ }
3339
+ if (_calleeZeroCandidateName(index, call)) {
2924
3340
  noteSite(siteId, 'external', null, call);
3341
+ } else if (chained?.externalVia) {
3342
+ noteUnverified(siteId, call, 'possible-dispatch');
3343
+ } else {
3344
+ noteUnverified(siteId, call, 'uncertain-receiver');
3345
+ }
3346
+ continue;
3347
+ } else if (goImportModule) {
3348
+ // Go package-qualified calls: klog.Infof(), wait.UntilWithContext()
3349
+ // The receiver is an import alias (resolved above) — find
3350
+ // definitions from that package.
3351
+ const match = _calleeGoPackageMatch(index, call, goImportModule);
3352
+ if (match) {
3353
+ const key = match.bindingId || `${call.receiver}.${call.name}`;
3354
+ const existing = callees.get(key);
3355
+ if (existing) {
3356
+ existing.count += 1;
3357
+ if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
3358
+ } else {
3359
+ callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1,
3360
+ ...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
3361
+ }
2925
3362
  continue;
2926
3363
  }
3364
+ // Import resolved but no project definition matches — external call, skip
3365
+ noteSite(siteId, 'external', null, call);
3366
+ continue;
2927
3367
  } else if (typeQual) {
2928
3368
  // Type-qualified receiver (fix #236): the receiver NAMES a
2929
3369
  // type, so the type owns the call — Foo::new() is Foo's
@@ -3030,14 +3470,19 @@ function findCallees(index, def, options = {}) {
3030
3470
  let uncertainReason = null; // account-mode reason for the unverified bucket
3031
3471
  if (!call.bindingId && fileEntry?.bindings) {
3032
3472
  let bindings = fileEntry.bindings.filter(b => b.name === call.name);
3033
- // For Go, also check sibling files in same directory (same package scope)
3473
+ // For Go, also check sibling files in same directory (same
3474
+ // package scope). dirToFiles is rebuilt on every build and
3475
+ // cache load in canonical path order — the same iteration
3476
+ // order as the full index.files scan it replaces (this loop
3477
+ // runs per zero-binding call record, so the full scan was
3478
+ // quadratic on large repos — 1037-file grpc-go measured).
3034
3479
  if (bindings.length === 0 && langTraits(language)?.packageScope === 'directory') {
3035
3480
  const dir = path.dirname(def.file);
3036
- for (const [fp, fe] of index.files) {
3037
- if (fp !== def.file && path.dirname(fp) === dir) {
3038
- const sibling = (fe.bindings || []).filter(b => b.name === call.name);
3039
- bindings = bindings.concat(sibling);
3040
- }
3481
+ for (const fp of index.dirToFiles?.get(dir) || []) {
3482
+ if (fp === def.file) continue;
3483
+ const fe = index.files.get(fp);
3484
+ const sibling = (fe?.bindings || []).filter(b => b.name === call.name);
3485
+ if (sibling.length > 0) bindings = bindings.concat(sibling);
3041
3486
  }
3042
3487
  }
3043
3488
  // Method call with no binding for the method name:
@@ -3252,6 +3697,14 @@ function findCallees(index, def, options = {}) {
3252
3697
  }
3253
3698
 
3254
3699
  if (isUncertain) {
3700
+ if (_calleeZeroCandidateName(index, call)) {
3701
+ // fix #261: zero project definitions of the name — the
3702
+ // unverified band exists to keep POSSIBLE project edges
3703
+ // visible, and with no def anywhere no edge is possible.
3704
+ // Same verdict the symbol-table check gives bare names.
3705
+ noteSite(siteId, 'external', null, call);
3706
+ continue;
3707
+ }
3255
3708
  if (collectAccount) {
3256
3709
  // Contract mode: uncertain callee edges are never silently
3257
3710
  // dropped NOR silently confirmed — visible unverified
@@ -3295,6 +3748,13 @@ function findCallees(index, def, options = {}) {
3295
3748
  if (selfAttrCalls && def.className && options.includeMethods !== false) {
3296
3749
  const attrTypes = getInstanceAttributeTypes(index, def.file, def.className);
3297
3750
  for (const { call, siteId } of selfAttrCalls) {
3751
+ // fix #261: `self.items.append(x)` — a method name with
3752
+ // zero project defs cannot resolve to project code no
3753
+ // matter what the attribute's type turns out to be.
3754
+ if (_calleeZeroCandidateName(index, call)) {
3755
+ noteSite(siteId, 'external', null, call);
3756
+ continue;
3757
+ }
3298
3758
  let targetClass = attrTypes ? attrTypes.get(call.selfAttribute) : null;
3299
3759
  // Unique method heuristic: if attr type unknown but method exists on exactly one class
3300
3760
  if (!targetClass) {
@@ -3337,6 +3797,7 @@ function findCallees(index, def, options = {}) {
3337
3797
  // claim the sites so the account stays conserved.
3338
3798
  for (const { call, siteId } of selfAttrCalls) {
3339
3799
  if (options.includeMethods === false) noteSite(siteId, 'filtered', 'method-calls-excluded', call);
3800
+ else if (_calleeZeroCandidateName(index, call)) noteSite(siteId, 'external', null, call);
3340
3801
  else noteUnverified(siteId, call, 'self-attr-unresolved');
3341
3802
  }
3342
3803
  }
@@ -3347,7 +3808,13 @@ function findCallees(index, def, options = {}) {
3347
3808
  if (selfMethodCalls && def.className && options.includeMethods !== false) {
3348
3809
  for (const { call, siteId } of selfMethodCalls) {
3349
3810
  const symbols = index.symbols.get(call.name);
3350
- if (!symbols) { noteUnverified(siteId, call, 'inherited-unresolved'); continue; }
3811
+ if (!symbols || symbols.length === 0) {
3812
+ // fix #261: `this.push(x)` in a class extending a builtin
3813
+ // (or an external base) — zero project defs of the name
3814
+ // means the inherited method is external, not unresolved.
3815
+ noteSite(siteId, 'external', null, call);
3816
+ continue;
3817
+ }
3351
3818
 
3352
3819
  // For super().method(), skip same-class — start from parent
3353
3820
  let match = call.receiver === 'super'
@@ -3394,6 +3861,7 @@ function findCallees(index, def, options = {}) {
3394
3861
  } else if (selfMethodCalls && collectAccount) {
3395
3862
  for (const { call, siteId } of selfMethodCalls) {
3396
3863
  if (options.includeMethods === false) noteSite(siteId, 'filtered', 'method-calls-excluded', call);
3864
+ else if (_calleeZeroCandidateName(index, call)) noteSite(siteId, 'external', null, call);
3397
3865
  else noteUnverified(siteId, call, 'inherited-unresolved');
3398
3866
  }
3399
3867
  }
@@ -4073,6 +4541,13 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
4073
4541
  const imported = typeDefs.filter(d => imports.has(d.file));
4074
4542
  if (imported.length === 1) return { fromFile: imported[0].file };
4075
4543
  }
4544
+ // Re-export chains (fix #258, the #209 lesson brought to identity
4545
+ // resolution): `use clap::Command` lands the import edge on the crate's
4546
+ // lib.rs, not the type's defining file — chase bounded re-export hops
4547
+ // (depth 4) and pin only when exactly ONE same-name type is reachable.
4548
+ const reachable = typeDefs.filter(d =>
4549
+ _importReaches(index, producerFile, new Set([d.file])));
4550
+ if (reachable.length === 1) return { fromFile: reachable[0].file };
4076
4551
  return null;
4077
4552
  }
4078
4553
 
@@ -4135,6 +4610,39 @@ const BUILTIN_RECEIVER_TYPES = new Set([
4135
4610
  'string', 'number', 'boolean', 'bigint', 'symbol',
4136
4611
  ]);
4137
4612
 
4613
+ // Universal-contract method names (fix #265, hono-measured: 183 untyped
4614
+ // `x.toString()` calls confirmed against JSXNode.toString via the single-
4615
+ // owner rule): names EVERY value satisfies through the language's root
4616
+ // object — Object.prototype (JS/TS), java.lang.Object (Java), object's
4617
+ // dunders (Python, protocol names by construction). Unique project
4618
+ // ownership of such a name is not identity evidence for a receiver-
4619
+ // evidence-free call — the #210 external-contract physics without needing
4620
+ // an override marker, because overriding these IS overriding the external
4621
+ // root. Demote-only (possible-dispatch); typed/validated/same-class
4622
+ // receivers keep normal physics. Go has no universal root; Rust's
4623
+ // trait-provided universals (to_string via Display) carry #210 markers.
4624
+ const _UNIVERSAL_METHOD_NAMES_JS = new Set([
4625
+ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty',
4626
+ 'isPrototypeOf', 'propertyIsEnumerable',
4627
+ ]);
4628
+ const _UNIVERSAL_METHOD_NAMES_JAVA = new Set([
4629
+ 'toString', 'equals', 'hashCode', 'getClass', 'clone',
4630
+ 'notify', 'notifyAll', 'wait', 'finalize',
4631
+ ]);
4632
+ function _universalMethodName(language, name) {
4633
+ if (language === 'python') return /^__[A-Za-z0-9_]+__$/.test(name);
4634
+ if (language === 'java') return _UNIVERSAL_METHOD_NAMES_JAVA.has(name);
4635
+ if (['javascript', 'typescript', 'tsx', 'html'].includes(language)) {
4636
+ return _UNIVERSAL_METHOD_NAMES_JS.has(name);
4637
+ }
4638
+ return false;
4639
+ }
4640
+ function _universalRootName(language) {
4641
+ if (language === 'python') return 'object';
4642
+ if (language === 'java') return 'Object';
4643
+ return 'Object.prototype';
4644
+ }
4645
+
4138
4646
  /**
4139
4647
  * Can this receiverType justify EXCLUDING a caller (structural languages)?
4140
4648
  * True for builtins and names that resolve to a project class/struct — types
@@ -4388,11 +4896,22 @@ function _projectTopLevelNames(index) {
4388
4896
  if (index._projectTopLevelNames) return index._projectTopLevelNames;
4389
4897
  const names = new Set();
4390
4898
  for (const [, fe] of index.files) {
4391
- const seg = (fe.relativePath || '').split(/[\\/]/)[0];
4899
+ const segs = (fe.relativePath || '').split(/[\\/]/);
4900
+ const seg = segs[0];
4392
4901
  if (!seg) continue;
4393
4902
  names.add(seg);
4394
4903
  const dot = seg.lastIndexOf('.');
4395
4904
  if (dot > 0) names.add(seg.slice(0, dot)); // utils.py → utils
4905
+ // PEP-517 src layout (fix #269): packages under a top-level src/
4906
+ // are importable by their SECOND segment — `import click` names
4907
+ // src/click. Without this, the externality test proved the
4908
+ // project's own package external (a resolver gap is not exclusion
4909
+ // evidence, #209).
4910
+ if (seg === 'src' && segs[1]) {
4911
+ names.add(segs[1]);
4912
+ const d2 = segs[1].lastIndexOf('.');
4913
+ if (d2 > 0) names.add(segs[1].slice(0, d2));
4914
+ }
4396
4915
  }
4397
4916
  index._projectTopLevelNames = names;
4398
4917
  return names;
@@ -4586,23 +5105,206 @@ function _shareProjectDescendant(index, className, targetClasses) {
4586
5105
  * classes disagree, or the type is a trait/interface (dynamic dispatch —
4587
5106
  * a trait-typed field is not evidence against any implementor).
4588
5107
  */
4589
- function _declaredFieldType(index, rootType, fieldName, language) {
5108
+ /**
5109
+ * Overload-signature identity (fix #265, zustand-measured): TS overload
5110
+ * signatures and their implementation — and Python @overload stubs and their
5111
+ * body — declare ONE function: `export function useStore(a): T;` ×2 plus the
5112
+ * implementation are the same symbol, so a call binding any member IS a call
5113
+ * to the pinned one (the #208 alias-identity principle for callables).
5114
+ * Closes the pin over same-file, same-class, same-nesting callable defs when
5115
+ * the group contains a signature — the overload idiom's proof. Plain
5116
+ * same-name redefinition (JS rebinding, Python last-wins shadowing) has no
5117
+ * signature member and never closes. `definitions` is the same-name def list,
5118
+ * so the group key needs no name component.
5119
+ */
5120
+ function _closeOverloadGroup(targetDefs, definitions) {
5121
+ const keyOf = (d) => `${d.file}\0${d.className || ''}\0${d.isNested ? 1 : 0}`;
5122
+ const groups = new Map();
5123
+ for (const d of definitions) {
5124
+ if (NON_CALLABLE_TYPES.has(d.type)) continue;
5125
+ const k = keyOf(d);
5126
+ if (!groups.has(k)) groups.set(k, []);
5127
+ groups.get(k).push(d);
5128
+ }
5129
+ let expanded = null;
5130
+ for (const t of targetDefs) {
5131
+ const group = groups.get(keyOf(t));
5132
+ if (!group || group.length <= 1 || !group.some(d => d.isSignature)) continue;
5133
+ for (const d of group) {
5134
+ if (targetDefs.includes(d) || (expanded && expanded.includes(d))) continue;
5135
+ if (!expanded) expanded = [...targetDefs];
5136
+ expanded.push(d);
5137
+ }
5138
+ }
5139
+ return expanded || targetDefs;
5140
+ }
5141
+
5142
+ /**
5143
+ * Pure-alias base resolution (fix #265 — the #208 identity for declared
5144
+ * types): when EVERY type-kind definition of a name is an alias agreeing on
5145
+ * one base, the name IS the base type (`type StoreMap = Map<...>` types a
5146
+ * `store: StoreMap` field as Map). Chases alias-of-alias chains bounded and
5147
+ * cycle-guarded; mixed or disagreeing names never resolve.
5148
+ */
5149
+ function _pureAliasBase(index, typeName) {
5150
+ let current = typeName;
5151
+ const seen = new Set([current]);
5152
+ for (let hop = 0; hop < 4; hop++) {
5153
+ const defs = index.symbols.get(current);
5154
+ if (!defs || defs.length === 0) return current === typeName ? null : current;
5155
+ let base = null;
5156
+ for (const d of defs) {
5157
+ if (d.type !== 'type' && !IDENTITY_TYPE_KINDS.has(d.type)) continue;
5158
+ if (d.type === 'type' && d.aliasOf) {
5159
+ if (base === null) base = d.aliasOf;
5160
+ else if (base !== d.aliasOf) return null; // disagreeing aliases
5161
+ } else {
5162
+ // a real type of this name exists — the name is not purely an alias
5163
+ return current === typeName ? null : current;
5164
+ }
5165
+ }
5166
+ if (!base) return current === typeName ? null : current;
5167
+ if (seen.has(base)) return null; // cycle
5168
+ seen.add(base);
5169
+ current = base;
5170
+ }
5171
+ return current;
5172
+ }
5173
+
5174
+ // Does the qualifier of a Go pkg.Type field annotation name one of the
5175
+ // declaring file's imports? (last-segment match, version-suffix aware —
5176
+ // the goImportModule convention.) Presence + no project resolution above
5177
+ // makes the field's type provably external (fix #268).
5178
+ function _goQualifierNamesImport(index, fieldFile, qualifier) {
5179
+ const fe = index.files.get(fieldFile);
5180
+ if (!fe || !Array.isArray(fe.imports)) return false;
5181
+ return fe.imports.some(mod => {
5182
+ const parts = String(mod).split('/');
5183
+ const last = parts[parts.length - 1];
5184
+ const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
5185
+ return pkgName === qualifier;
5186
+ });
5187
+ }
5188
+
5189
+ // Same-class overload selection, callee direction (fix #268, jsoup-measured:
5190
+ // all 71 tb.process sites confirmed to the 1-arg overload — the .find()
5191
+ // pick was defs[0] in canonical order). Arity narrows first; Java argKinds
5192
+ // refine same-arity families; exactly one survivor binds, anything else is
5193
+ // statically undecidable and routes visible.
5194
+ // Resolve a Go package-qualified call to a definition in the imported
5195
+ // package (dir-path suffix match against the import path; LONGEST suffix
5196
+ // wins so import "k8s.io/client-go/kubernetes/scheme" prefers a def in
5197
+ // .../kubernetes/scheme/ over .../kubeadm/scheme/). Extracted for reuse:
5198
+ // the parser marks some package calls isMethod:false (fix #268).
5199
+ function _calleeGoPackageMatch(index, call, importModule) {
5200
+ const symbols = index.symbols.get(call.name);
5201
+ if (!symbols) return null;
5202
+ // Self-module imports (fix #268, cobra-measured — the #220(8) go.mod
5203
+ // identity): `import "github.com/spf13/cobra"` from doc/ names the
5204
+ // module's ROOT package, whose relative dir is '.' and can never
5205
+ // path-suffix-match the import string. Compose the effective package
5206
+ // dir from the module line (nested subpackage paths compose too).
5207
+ const goMod = findGoModule(index.root);
5208
+ if (goMod && goMod.modulePath &&
5209
+ (importModule === goMod.modulePath || importModule.startsWith(goMod.modulePath + '/'))) {
5210
+ const sub = importModule === goMod.modulePath ? '.' :
5211
+ importModule.slice(goMod.modulePath.length + 1);
5212
+ const selfMatch = symbols.find(s => {
5213
+ const sDir = path.dirname(s.relativePath || path.relative(index.root, s.file));
5214
+ return sDir === sub;
5215
+ });
5216
+ if (selfMatch) return selfMatch;
5217
+ }
5218
+ const importParts = importModule.split('/');
5219
+ let bestMatch = null;
5220
+ let bestMatchLen = 0;
5221
+ for (const s of symbols) {
5222
+ const sDir = path.dirname(s.relativePath || path.relative(index.root, s.file));
5223
+ for (let i = 0; i < importParts.length; i++) {
5224
+ const suffix = importParts.slice(i).join('/');
5225
+ if (sDir === suffix || sDir.endsWith('/' + suffix)) {
5226
+ const matchLen = importParts.length - i;
5227
+ if (matchLen > bestMatchLen) {
5228
+ bestMatchLen = matchLen;
5229
+ bestMatch = s;
5230
+ }
5231
+ break; // this symbol's best suffix found, try next
5232
+ }
5233
+ }
5234
+ }
5235
+ return bestMatch;
5236
+ }
5237
+
5238
+ function _calleeOverloadSelect(index, call, matches, language) {
5239
+ if (matches.length <= 1) return { match: matches[0] || null };
5240
+ if (call.argCount == null || call.argSpread) return { ambiguous: true };
5241
+ const fits = matches.filter(d => _callArityCompatible(call, [d], language));
5242
+ if (fits.length === 1) return { match: fits[0] };
5243
+ if (fits.length === 0) return { match: null }; // fits nothing we model — no claim
5244
+ const applicable = fits.filter(d => _overloadApplicable(index, call, d));
5245
+ if (applicable.length === 1) return { match: applicable[0] };
5246
+ return { ambiguous: true };
5247
+ }
5248
+
5249
+ function _declaredFieldType(index, rootType, fieldName, language, info) {
4590
5250
  const defs = index.symbols.get(fieldName);
4591
5251
  if (!defs) return null;
4592
5252
  // 'private field' (JS #-fields, fix #219): equally compiler-true, and
4593
- // safer — nothing outside the class can rebind them.
5253
+ // safer — nothing outside the class can rebind them. Getters and Python
5254
+ // @property members (fix #265, hono-measured: Context.req is `get req():
5255
+ // HonoRequest`) type the hop through their declared RETURN annotation —
5256
+ // the same compiler-checked evidence in accessor clothing; the annotation
5257
+ // is what the member-access expression evaluates to (value position).
5258
+ const isAccessor = (d) => d.type === 'get' || d.memberType === 'get' ||
5259
+ d.type === 'property' || d.memberType === 'property';
4594
5260
  const fields = defs.filter(d =>
4595
- (d.type === 'field' || d.memberType === 'field' || d.memberType === 'private field') &&
4596
- d.className === rootType && d.fieldType);
4597
- if (fields.length === 0) return null;
5261
+ ((d.type === 'field' || d.memberType === 'field' || d.memberType === 'private field') && d.fieldType) ||
5262
+ (isAccessor(d) && d.returnType));
5263
+ const onType = fields.filter(d => d.className === rootType);
5264
+ if (onType.length === 0) return null;
4598
5265
  const normalized = new Set();
4599
- for (const f of fields) {
4600
- const t = _normalizeFieldTypeName(f.fieldType, language);
5266
+ for (const f of onType) {
5267
+ const rawText = isAccessor(f) ? f.returnType : f.fieldType;
5268
+ // Qualified declared types resolve through the FIELD-DECLARING file's
5269
+ // imports or not at all (fix #268, chi-measured — the #206 identity
5270
+ // discipline): `inner http.Handler` is net/http's Handler, never a
5271
+ // project class that happens to share the bare name (an _examples
5272
+ // type named Handler confirmed mx.handler.ServeHTTP). Go only —
5273
+ // its pkg.Type field shape carries a checkable qualifier; Rust/Java
5274
+ // qualified heads keep their current physics (no measured family).
5275
+ if (language === 'go' && f.file) {
5276
+ const qm = String(rawText).trim().replace(/^\*+/, '')
5277
+ .match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
5278
+ if (qm) {
5279
+ const [, qualifier, bare] = qm;
5280
+ const hasProjectType = (index.symbols.get(bare) || [])
5281
+ .some(d => IDENTITY_TYPE_KINDS.has(d.type));
5282
+ const origin = hasProjectType &&
5283
+ _resolveFlowTypeOrigin(index, f.file, bare, qualifier);
5284
+ if (origin && origin.fromFile) { normalized.add(bare); continue; }
5285
+ // The qualifier names an import that resolves to no project
5286
+ // type — the field's type is provably external. Surfaced via
5287
+ // `info` so the callee side can route external/visible; the
5288
+ // head stays null (never bare-name identity, demote-only).
5289
+ if (info && _goQualifierNamesImport(index, f.file, qualifier)) {
5290
+ info.externalVia = `${qualifier}.${bare}`;
5291
+ }
5292
+ return null;
5293
+ }
5294
+ }
5295
+ const t = _normalizeFieldTypeName(rawText, language);
4601
5296
  if (t) normalized.add(t);
4602
5297
  else return null; // any un-normalizable declaration → no evidence
4603
5298
  }
4604
5299
  if (normalized.size !== 1) return null; // same-named classes disagree
4605
- const typeName = [...normalized][0];
5300
+ let typeName = [...normalized][0];
5301
+ // Pure-alias heads resolve to their base (fix #265 — the #208 identity
5302
+ // for declared types): `store: StoreMap` where `type StoreMap = Map<...>`
5303
+ // types the field as Map, which the trust gate can judge (builtin →
5304
+ // exclusion-grade; a project-class base validates normally). Mixed or
5305
+ // disagreeing alias names stay as-is and fail the trust gate downstream.
5306
+ const aliasBase = _pureAliasBase(index, typeName);
5307
+ if (aliasBase) typeName = aliasBase;
4606
5308
  // Generic type parameters by convention (T, K, V1 — fix #220,
4607
5309
  // cursive-measured): `view: T` declares the field as WHATEVER the
4608
5310
  // instantiation chose — not a type identity. Without this, the hop
@@ -4916,6 +5618,34 @@ function _calleeReceiverTypeRoute(index, call, localTypes, language) {
4916
5618
  return { uncertain: true }; // known non-builtin type with no project method
4917
5619
  }
4918
5620
 
5621
+ /**
5622
+ * Zero-candidate call names (fix #261): a call whose name has ZERO
5623
+ * definitions anywhere in the index cannot be a project call — there is no
5624
+ * def an edge could land on. The confirmed path already gives bare names
5625
+ * this exact verdict (the symbol-table check routes them external); method
5626
+ * calls were diverted to the unverified band by receiver uncertainty FIRST,
5627
+ * so `parts.push(...)` / `names.join(...)` sat as [unverified]
5628
+ * uncertain-receiver noise in every trace. Dynamic property assignment does
5629
+ * not defeat this: `obj.push = function() {...}` indexes a def named `push`
5630
+ * (property-assignment naming, all shapes — probed), so any project that
5631
+ * dynamically defines the method keeps its calls visible; a monkey-patched
5632
+ * alias of an EXISTING function (`Foo.push = helper`) surfaces its edge at
5633
+ * the assignment site as a function reference of `helper` (fix #221/#252),
5634
+ * never at the invocation. Checks every name the call could resolve to
5635
+ * (parser alias resolution).
5636
+ */
5637
+ function _calleeZeroCandidateName(index, call) {
5638
+ const names = [call.name];
5639
+ if (call.resolvedName) names.push(call.resolvedName);
5640
+ if (call.resolvedNames) names.push(...call.resolvedNames);
5641
+ for (const n of names) {
5642
+ if (!n) continue;
5643
+ const defs = index.symbols.get(n);
5644
+ if (defs && defs.length > 0) return false;
5645
+ }
5646
+ return true;
5647
+ }
5648
+
4919
5649
  function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, flowEntry) {
4920
5650
  if (JS_GLOBAL_RECEIVERS.has(call.receiver)) return null;
4921
5651
  if (call.receiverIsModule) return null;
@@ -5089,6 +5819,38 @@ function _overloadDiscipline(index, call, targetDefs, definitions) {
5089
5819
  if (targetOwners.size === 0) return null;
5090
5820
  const family = definitions.filter(d => !NON_CALLABLE_TYPES.has(d.type) &&
5091
5821
  d.className && targetOwners.has(d.className));
5822
+ // Inherited sibling overloads (fix #268, javapoet-measured): the pin's
5823
+ // dispatch surface includes ancestor same-name methods — ClassName's
5824
+ // annotated(List) coexists with TypeName's FINAL annotated(Spec...), and
5825
+ // a 1-arg call may bind either; receiver-hint evidence said "some
5826
+ // annotated overload", not the pinned one (the #205 jdtls lesson).
5827
+ // Identical type signatures in an ancestor are the OVERRIDE SLOT the pin
5828
+ // occupies — the same virtual method, never a sibling.
5829
+ const typeSig = (d) => Array.isArray(d.paramsStructured)
5830
+ ? d.paramsStructured.map(p => String(p?.type ?? '').replace(/\s+/g, '')).join(',')
5831
+ : null;
5832
+ const pinSigs = new Set(targetDefs.map(typeSig).filter(s => s !== null));
5833
+ const pinFile = targetDefs.find(d => d.file)?.file;
5834
+ const seenCls = new Set(targetOwners);
5835
+ const queue = [...targetOwners];
5836
+ let hops = 0;
5837
+ while (queue.length > 0 && hops++ < 32) {
5838
+ const cls = queue.pop();
5839
+ const parents = index._getInheritanceParents?.(cls, pinFile);
5840
+ if (!parents) continue;
5841
+ for (const p of parents) {
5842
+ const pName = typeof p === 'string' ? p : p?.name;
5843
+ if (!pName || seenCls.has(pName)) continue;
5844
+ seenCls.add(pName);
5845
+ queue.push(pName);
5846
+ for (const d of definitions) {
5847
+ if (NON_CALLABLE_TYPES.has(d.type) || d.className !== pName) continue;
5848
+ const sig = typeSig(d);
5849
+ if (sig !== null && pinSigs.has(sig)) continue; // override slot
5850
+ family.push(d);
5851
+ }
5852
+ }
5853
+ }
5092
5854
  if (family.length <= 1) return null;
5093
5855
  const pinnedKeys = new Set(targetDefs.map(d => `${d.file}:${d.startLine}`));
5094
5856
  if (family.every(d => pinnedKeys.has(`${d.file}:${d.startLine}`))) return null;
@@ -5626,6 +6388,348 @@ function _chainedReceiverType(index, call, language) {
5626
6388
  return head;
5627
6389
  }
5628
6390
 
6391
+ // ── Chained-receiver fold (fix #258, clap-measured) ─────────────────────────
6392
+ // Builder chains (`Command::new("x").author(a).arg(b).arg(c)`) defeat the
6393
+ // one-hop agreement rules: `arg` has two owners (Command and ArgGroup), both
6394
+ // returning `Self` — which resolves to DIFFERENT types, so project-wide
6395
+ // agreement fails and 1600+ oracle-true clap edges sat method-ambiguous.
6396
+ // The fold types the chain hop by hop from a typed root instead: the parser
6397
+ // links each chained call to its producer's OWN record (receiverCallLine),
6398
+ // the root types via the existing #207 producer rails (path/static/package-
6399
+ // qualified producers, annotated variables, module-qualified roots), and each
6400
+ // hop resolves the producer method ON THE CURRENT TYPE — `Self`/`this` map to
6401
+ // that type, a hop returning a different type re-roots the chain there, and
6402
+ // any unresolvable hop (missing annotation, foreign same-name type, sibling
6403
+ // disagreement) stops the fold: untyped, visible, honest. Per-hop identity
6404
+ // keeps the #206/#207 discipline (owner defs co-located with the pinned type
6405
+ // when the type name is ambiguous project-wide; origins re-pinned from the
6406
+ // defining file). Results feed the existing receiverType machinery — the
6407
+ // fold adds evidence, never new routing.
6408
+
6409
+ const _FOLD_TYPE_KINDS = new Set(['class', 'struct', 'enum', 'trait', 'interface', 'record', 'type', 'namespace']);
6410
+
6411
+ /**
6412
+ * Resolve method `methodName` on type `typeName` (identity-pinned to
6413
+ * `fromFile` when known) and return its resolved return-type head as
6414
+ * { type, fromFile } — or null when the resolution is not compiler-grade.
6415
+ * Walks declared ancestors (bounded) when the type itself doesn't define the
6416
+ * method; `Self`/`this` return annotations resolve to the RECEIVER's type
6417
+ * (dynamic-Self semantics — sound because the chain's static type is T).
6418
+ */
6419
+ function _methodReturnOnType(index, typeName, fromFile, methodName, language, opts = {}) {
6420
+ const nominal = langTraits(language)?.typeSystem === 'nominal';
6421
+ const norm = s => (s || '').replace(/^\*/, '').replace(/\[.*$/, '').replace(/<.*$/, '');
6422
+ const depth = opts.depth || 0;
6423
+ if (depth > 8) return null;
6424
+ const all = (index.symbols.get(methodName) || [])
6425
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type));
6426
+ let owned = all.filter(d =>
6427
+ (d.className && norm(d.className) === typeName) ||
6428
+ (!d.className && d.receiver && norm(d.receiver) === typeName));
6429
+ // Identity discipline (#206c): with several same-name TYPES in the
6430
+ // project, an owner-name match is only THE type when co-located with the
6431
+ // pinned defining file; with no pin, refuse.
6432
+ const typeDefs = (index.symbols.get(typeName) || []).filter(d => _FOLD_TYPE_KINDS.has(d.type));
6433
+ if (typeDefs.length > 1 && owned.length > 0) {
6434
+ if (!fromFile) return null;
6435
+ const dir = path.dirname(fromFile);
6436
+ owned = owned.filter(d => d.file === fromFile || (d.file && path.dirname(d.file) === dir));
6437
+ }
6438
+ if (owned.length === 0) {
6439
+ // Inheritance walk: resolve on a declared ancestor; Self/this still
6440
+ // resolve to the RECEIVER's type (passed through selfType).
6441
+ const ctxFile = fromFile || opts.filePath;
6442
+ const parents = index._getInheritanceParents
6443
+ ? (index._getInheritanceParents(typeName, ctxFile) || []) : [];
6444
+ for (const parent of parents) {
6445
+ const pFile = index._resolveClassFile
6446
+ ? (index._resolveClassFile(parent, ctxFile) || undefined) : undefined;
6447
+ const up = _methodReturnOnType(index, norm(parent), pFile, methodName, language, {
6448
+ ...opts, depth: depth + 1, selfType: opts.selfType || typeName,
6449
+ });
6450
+ if (up) return up;
6451
+ }
6452
+ return null;
6453
+ }
6454
+ const selfType = opts.selfType || typeName;
6455
+ if (nominal) {
6456
+ if (!owned.every(d => d.returnType)) return null;
6457
+ if (new Set(owned.map(d => d.returnType)).size !== 1) return null;
6458
+ const def = owned[0];
6459
+ const parsed = _returnTypeNameNominal(def.returnType, language, { selfClass: selfType });
6460
+ if (!parsed) return null;
6461
+ const origin = _resolveFlowTypeOrigin(index, def.file || opts.filePath, parsed.name, parsed.qualifier);
6462
+ if (!origin) return null;
6463
+ return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
6464
+ }
6465
+ // Structural: heads must agree; `this`/`Self` are the receiver's type
6466
+ // (checked BEFORE the reject set — with a known owner they ARE identity);
6467
+ // un-awaited async producers stay untyped (the value is a coroutine).
6468
+ if (language === 'python' && !opts.consumerAwaited && owned.some(d => d.isAsync)) return null;
6469
+ const heads = new Set();
6470
+ for (const d of owned) {
6471
+ if (!d.returnType) return null;
6472
+ let h = _structuralTypeHead(d.returnType, { unwrapAsync: opts.consumerAwaited });
6473
+ if (h === 'this' || h === 'Self') h = selfType;
6474
+ if (!h) return null;
6475
+ heads.add(h);
6476
+ if (heads.size > 1) return null;
6477
+ }
6478
+ const head = [...heads][0];
6479
+ if (/^[A-Z][A-Z0-9]?$/.test(head)) return null; // generic type param
6480
+ if (_STRUCTURAL_FLOW_REJECT.has(head)) return null;
6481
+ return { type: head };
6482
+ }
6483
+
6484
+ /**
6485
+ * Type of the VALUE a call record produces — { type, fromFile },
6486
+ * { externalVia } (compiler-grade evidence the value was typed outside the
6487
+ * project), or null. Mirrors the #207 flow-map producer rails per shape, and
6488
+ * recurses through the producer link for chained producers (memoized,
6489
+ * cycle-guarded).
6490
+ */
6491
+ function _typeOfCallResultFold(index, fileEntry, filePath, record, ctx, consumerAwaited) {
6492
+ if (ctx.memo.has(record)) return ctx.memo.get(record);
6493
+ if (ctx.visiting.has(record) || ctx.visiting.size > 64) return null;
6494
+ ctx.visiting.add(record);
6495
+ let out = null;
6496
+ try {
6497
+ out = _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, consumerAwaited);
6498
+ } finally {
6499
+ ctx.visiting.delete(record);
6500
+ }
6501
+ ctx.memo.set(record, out);
6502
+ return out;
6503
+ }
6504
+
6505
+ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, consumerAwaited) {
6506
+ if (record.isMacro || record.inMacro) return null; // token-tree records carry no chain physics
6507
+ const language = fileEntry.language;
6508
+ const traits = langTraits(language);
6509
+ const nominal = traits?.typeSystem === 'nominal';
6510
+ const name = record.name;
6511
+
6512
+ // Path producer (Rust): Command::new(...) — the last path segment names
6513
+ // the impl type (flow-map rails: module-path producers stay untyped);
6514
+ // Self::new() resolves through the enclosing impl. The type's identity
6515
+ // is pinned from THIS file's scope (#206 discipline — clap's derive
6516
+ // tests define dozens of local `struct Command` fixtures; the pin keeps
6517
+ // the fold on the imported one).
6518
+ if (nominal && record.isPathCall && record.receiver) {
6519
+ const segs = String(record.receiver).split('::');
6520
+ let seg = segs.pop();
6521
+ if (seg === 'Self') {
6522
+ const enclosing = index.findEnclosingFunction(filePath, record.line, true);
6523
+ seg = enclosing && enclosing.className;
6524
+ if (!seg) return null;
6525
+ }
6526
+ if (!seg || !/^[A-Z]/.test(seg)) return null;
6527
+ if (/^[A-Z][A-Z0-9]?$/.test(seg)) return null; // generic-param convention (#220)
6528
+ const qual = segs.length > 0 ? segs[segs.length - 1] : undefined;
6529
+ const origin = _resolveFlowTypeOrigin(index, filePath, seg,
6530
+ qual && !['crate', 'self', 'super'].includes(qual) ? qual : undefined);
6531
+ if (!origin) return null;
6532
+ return _methodReturnOnType(index, seg, origin.fromFile, name, language,
6533
+ { filePath, consumerAwaited });
6534
+ }
6535
+ // Java static factory: Config.parse(...) — static call style only (#206:
6536
+ // a Go receiver named like a type is a VARIABLE).
6537
+ if (nominal && record.isMethod && record.receiver && !record.receiverIsChainRoot &&
6538
+ traits?.typeQualifiedCallStyle === 'static' && /^[A-Z]/.test(record.receiver) &&
6539
+ !/^[A-Z][A-Z0-9]?$/.test(record.receiver)) {
6540
+ const r = _methodReturnOnType(index, record.receiver, undefined, name, language,
6541
+ { filePath, consumerAwaited });
6542
+ if (r) return r;
6543
+ // fall through: a capitalized Java receiver may still be a variable
6544
+ }
6545
+ // Go package-qualified plain producer: pkg.Get(...) — strict import
6546
+ // resolution; unresolved packages typed the value OUTSIDE the project.
6547
+ if (nominal && !record.isMethod && record.receiver && traits?.hasReceiverPackageCalls) {
6548
+ const cands = (index.symbols.get(name) || [])
6549
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type) && d.returnType);
6550
+ const inPkg = _qualifiedProducerDefs(index, fileEntry, record.receiver, cands);
6551
+ if (!inPkg || inPkg.length === 0 || new Set(inPkg.map(d => d.returnType)).size !== 1) {
6552
+ return { externalVia: `${record.receiver}.${name}` };
6553
+ }
6554
+ const def = inPkg[0];
6555
+ const parsed = _returnTypeNameNominal(def.returnType, language, {});
6556
+ if (!parsed) return null;
6557
+ const origin = _resolveFlowTypeOrigin(index, def.file || filePath, parsed.name, parsed.qualifier);
6558
+ if (!origin) return null;
6559
+ return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
6560
+ }
6561
+ // Structural module-qualified producer: z.string() — resolve through the
6562
+ // file's import bindings (flow-map rails, incl. the #222 externality test).
6563
+ if (!nominal && record.isMethod && record.receiver && record.receiverIsModule) {
6564
+ const binding = (fileEntry?.importBindings || []).find(b => b.name === record.receiver);
6565
+ const rel = binding && fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
6566
+ if (binding && !rel) {
6567
+ const mod = String(binding.module);
6568
+ const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
6569
+ if (!mod.startsWith('.') &&
6570
+ !(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
6571
+ return { externalVia: `${record.receiver}.${name}` };
6572
+ }
6573
+ return null;
6574
+ }
6575
+ if (!rel) return null;
6576
+ const modFile = path.join(index.root, rel);
6577
+ const cands = (index.symbols.get(name) || [])
6578
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type) && d.returnType && !d.className);
6579
+ let matches = cands.filter(d => d.file === modFile);
6580
+ if (matches.length === 0) {
6581
+ const hop = index.importGraph.get(modFile);
6582
+ if (hop) matches = cands.filter(d => hop.has(d.file));
6583
+ }
6584
+ if (matches.length === 0) return null;
6585
+ if (language === 'python' && !consumerAwaited && matches.some(d => d.isAsync)) return null;
6586
+ const heads = new Set();
6587
+ for (const d of matches) {
6588
+ const h = _structuralTypeHead(d.returnType, { unwrapAsync: consumerAwaited });
6589
+ if (!h) return null;
6590
+ heads.add(h);
6591
+ }
6592
+ if (heads.size !== 1) return null;
6593
+ const head = [...heads][0];
6594
+ if (/^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
6595
+ return { type: head };
6596
+ }
6597
+ // self/this/cls receiver: resolve through the enclosing class (+ walk).
6598
+ if (record.isMethod && ['self', 'this', 'cls'].includes(record.receiver)) {
6599
+ const enclosing = index.findEnclosingFunction(filePath, record.line, true);
6600
+ let cls = enclosing && enclosing.className;
6601
+ let ctxFile = filePath;
6602
+ const visited = new Set();
6603
+ while (cls && !visited.has(cls)) {
6604
+ visited.add(cls);
6605
+ const r = _methodReturnOnType(index, cls, ctxFile, name, language,
6606
+ { filePath, consumerAwaited, selfType: enclosing.className });
6607
+ if (r) return r;
6608
+ const parents = index._getInheritanceParents(cls, ctxFile) || [];
6609
+ const next = parents[0];
6610
+ if (next && index._resolveClassFile) {
6611
+ ctxFile = index._resolveClassFile(next, ctxFile) || ctxFile;
6612
+ }
6613
+ cls = next;
6614
+ }
6615
+ return null;
6616
+ }
6617
+ // Method producer: type its OWN receiver (parser annotation → chain
6618
+ // recursion → flow map), then resolve the method on that type. Falls back
6619
+ // to the one-hop project-wide agreement rule when the receiver stays
6620
+ // untyped.
6621
+ if (record.isMethod) {
6622
+ let rt = null;
6623
+ if (record.receiverType && !record.receiverIsChainRoot) {
6624
+ rt = { type: record.receiverType };
6625
+ }
6626
+ if (!rt && record.receiverCall && (!record.receiver || record.receiverIsChainRoot)) {
6627
+ rt = _foldChainedReceiverType(index, fileEntry, filePath, record, ctx);
6628
+ }
6629
+ if (!rt && record.receiver && !record.receiverIsChainRoot) {
6630
+ const flowMap = ctx.getFlowMap();
6631
+ const fe = flowMap && _lookupReturnTypeFlow(flowMap, record);
6632
+ if (fe && fe.externalVia) return { externalVia: fe.externalVia };
6633
+ if (fe && fe.type) rt = { type: fe.type, ...(fe.fromFile && { fromFile: fe.fromFile }) };
6634
+ }
6635
+ if (rt && rt.externalVia) return rt;
6636
+ if (rt && rt.type) {
6637
+ return _methodReturnOnType(index, rt.type, rt.fromFile, name, language,
6638
+ { filePath, consumerAwaited });
6639
+ }
6640
+ // One-hop agreement (the #207/#219 discipline, one level deeper):
6641
+ // every method owner project-wide annotated and agreeing.
6642
+ const methodDefs = (index.symbols.get(name) || [])
6643
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type) && (d.className || d.receiver));
6644
+ if (methodDefs.length === 0) return null;
6645
+ if (!methodDefs.every(d => d.returnType)) return null;
6646
+ if (nominal) {
6647
+ if (new Set(methodDefs.map(d => d.returnType)).size !== 1) return null;
6648
+ const classes = new Set(methodDefs.map(d =>
6649
+ d.className || (d.receiver || '').replace(/^\*/, '')));
6650
+ const selfClass = classes.size === 1 ? [...classes][0] : undefined;
6651
+ const parsed = _returnTypeNameNominal(methodDefs[0].returnType, language, { selfClass });
6652
+ if (!parsed) return null;
6653
+ const origin = _resolveFlowTypeOrigin(index, methodDefs[0].file || filePath, parsed.name, parsed.qualifier);
6654
+ if (!origin) return null;
6655
+ return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
6656
+ }
6657
+ if (language === 'python' && !consumerAwaited && methodDefs.some(d => d.isAsync)) return null;
6658
+ const heads = new Set();
6659
+ for (const d of methodDefs) {
6660
+ const h = _structuralTypeHead(d.returnType, { unwrapAsync: consumerAwaited });
6661
+ if (!h) return null;
6662
+ heads.add(h);
6663
+ if (heads.size > 1) return null;
6664
+ }
6665
+ const head = [...heads][0];
6666
+ if (/^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
6667
+ return { type: head };
6668
+ }
6669
+ // Plain producer: Go same-package only; others unique-project-def with
6670
+ // same-file narrowing; Java bare calls reach the enclosing class first.
6671
+ if (traits?.bareCallReachesMethods) {
6672
+ const enclosing = index.findEnclosingFunction(filePath, record.line, true);
6673
+ if (enclosing?.className) {
6674
+ const r = _methodReturnOnType(index, enclosing.className, filePath, name, language,
6675
+ { filePath, consumerAwaited });
6676
+ if (r) return r;
6677
+ }
6678
+ }
6679
+ const defs = (index.symbols.get(name) || [])
6680
+ .filter(d => !NON_CALLABLE_TYPES.has(d.type) && !(d.className || d.receiver));
6681
+ let chosen = null;
6682
+ if (traits?.packageScope === 'directory') {
6683
+ const dir = path.dirname(filePath);
6684
+ const samePkg = defs.filter(d => d.file && path.dirname(d.file) === dir);
6685
+ if (samePkg.length === 1) chosen = samePkg[0];
6686
+ } else if (defs.length === 1) {
6687
+ chosen = defs[0];
6688
+ } else {
6689
+ const sameFile = defs.filter(d => d.file === filePath);
6690
+ if (sameFile.length === 1) chosen = sameFile[0];
6691
+ }
6692
+ if (!chosen || !chosen.returnType) return null;
6693
+ if (nominal) {
6694
+ const parsed = _returnTypeNameNominal(chosen.returnType, language, {});
6695
+ if (!parsed) return null;
6696
+ const origin = _resolveFlowTypeOrigin(index, chosen.file || filePath, parsed.name, parsed.qualifier);
6697
+ if (!origin) return null;
6698
+ return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
6699
+ }
6700
+ if (language === 'python' && !consumerAwaited && chosen.isAsync) return null;
6701
+ let head = _structuralTypeHead(chosen.returnType, { unwrapAsync: consumerAwaited });
6702
+ if (!head || /^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
6703
+ return { type: head };
6704
+ }
6705
+
6706
+ /**
6707
+ * Type the RECEIVER of a chained call from its producer's own record
6708
+ * (fix #258). Producer records are matched by (receiverCallLine, name) —
6709
+ * when several match (one-line chains like `a.arg(1).arg(2)`), ALL must fold
6710
+ * to the same type or the receiver stays untyped. Returns { type, fromFile },
6711
+ * { externalVia }, or null (fall back to the legacy one-hop helpers).
6712
+ */
6713
+ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
6714
+ if (!call.receiverCall || !call.receiverCallLine || !ctx.records) return null;
6715
+ const prods = ctx.records.filter(r =>
6716
+ r !== call && r.line === call.receiverCallLine && r.name === call.receiverCall &&
6717
+ !r.isMacro && !r.inMacro &&
6718
+ // Kind match: the consumer knows whether its producer was a method-
6719
+ // shaped call — `.arg(arg("x"))` has both a chained method `arg` and
6720
+ // a plain closure call `arg` on one line; only the right kind folds.
6721
+ !!r.isMethod === !!call.receiverCallIsMethod);
6722
+ if (prods.length === 0) return null;
6723
+ const results = prods.map(r =>
6724
+ _typeOfCallResultFold(index, fileEntry, filePath, r, ctx, call.receiverCallAwaited));
6725
+ if (!results.every(Boolean)) return null;
6726
+ if (results.every(r => r.externalVia)) return { externalVia: results[0].externalVia };
6727
+ if (results.some(r => r.externalVia)) return null;
6728
+ if (new Set(results.map(r => r.type)).size !== 1) return null;
6729
+ const fromFiles = new Set(results.map(r => r.fromFile));
6730
+ return { type: results[0].type, ...(fromFiles.size === 1 && results[0].fromFile && { fromFile: results[0].fromFile }) };
6731
+ }
6732
+
5629
6733
  /**
5630
6734
  * Is this field symbol callable by its own name (obj.f(...) reaches the
5631
6735
  * field's function value)? Arrow-function class fields are callable by
@@ -5647,6 +6751,9 @@ function _callableFieldDef(index, d) {
5647
6751
 
5648
6752
  function _buildTypedLocalTypeMap(index, def, calls) {
5649
6753
  const localTypes = new Map();
6754
+ // Vars whose entry is a New*-prefix name GUESS (fix #266) — consumers may
6755
+ // confirm through them but never exclude (convention, not compiler truth).
6756
+ const guessedVars = new Set();
5650
6757
  let _cachedLines = null;
5651
6758
 
5652
6759
  for (const call of calls) {
@@ -5655,6 +6762,8 @@ function _buildTypedLocalTypeMap(index, def, calls) {
5655
6762
  // Collect receiverType from method calls (inferred by parser from params/receivers)
5656
6763
  if (call.isMethod && call.receiver && call.receiverType) {
5657
6764
  localTypes.set(call.receiver, call.receiverType);
6765
+ if (call.receiverTypeGuessed) guessedVars.add(call.receiver);
6766
+ else guessedVars.delete(call.receiver);
5658
6767
  }
5659
6768
 
5660
6769
  // Collect types from constructor calls: x := NewFoo() → x maps to Foo
@@ -5678,12 +6787,15 @@ function _buildTypedLocalTypeMap(index, def, calls) {
5678
6787
  const typeName = assignMatch[2].slice(3);
5679
6788
  if (typeName && /^[A-Z]/.test(typeName)) {
5680
6789
  localTypes.set(assignMatch[1], typeName);
6790
+ guessedVars.add(assignMatch[1]); // name convention, not compiler truth (fix #266)
5681
6791
  }
5682
6792
  }
5683
6793
  }
5684
6794
  }
5685
6795
 
5686
- return localTypes.size > 0 ? localTypes : null;
6796
+ if (localTypes.size === 0) return null;
6797
+ localTypes.guessedVars = guessedVars;
6798
+ return localTypes;
5687
6799
  }
5688
6800
 
5689
6801
  /**