ucn 4.1.1 → 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/README.md +10 -8
- package/core/build-worker.js +21 -7
- package/core/cache.js +16 -1
- package/core/callers.js +1210 -98
- package/core/deadcode.js +59 -8
- package/core/entrypoints.js +13 -2
- package/core/imports.js +98 -6
- package/core/project.js +45 -13
- package/languages/go.js +56 -4
- package/languages/java.js +5 -1
- package/languages/javascript.js +95 -9
- package/languages/python.js +23 -1
- package/languages/rust.js +42 -8
- package/package.json +1 -1
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 &&
|
|
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
|
|
473
|
+
if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
|
|
474
|
+
!call.receiverType && call.receiverCall &&
|
|
445
475
|
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1885
|
+
if (collectAccount && inferredGuessed) receiverTypeUnresolved = true;
|
|
1886
|
+
else inferredMismatch = true;
|
|
1683
1887
|
} else {
|
|
1684
1888
|
receiverTypeUnresolved = true;
|
|
1685
1889
|
}
|
|
1686
1890
|
} else {
|
|
1687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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
|
-
|
|
3151
|
+
const onClass = (cls) => (symbols || []).filter(s =>
|
|
2784
3152
|
isCallable(s) && (
|
|
2785
|
-
s.className ===
|
|
2786
|
-
(s.receiver && s.receiver.replace(/^\*/, '') ===
|
|
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
|
-
|
|
2793
|
-
|
|
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
|
-
|
|
3228
|
+
const onClassRT = (cls) => (symbols || []).filter(s =>
|
|
2837
3229
|
isCallableRT(s) && (
|
|
2838
|
-
(s.receiver && s.receiver.replace(/^\*/, '') ===
|
|
2839
|
-
s.className ===
|
|
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
|
-
|
|
2846
|
-
|
|
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 (
|
|
2880
|
-
|
|
2881
|
-
//
|
|
2882
|
-
//
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
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 visible — never 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
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
for (
|
|
2898
|
-
|
|
2899
|
-
if (
|
|
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
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3037
|
-
if (fp
|
|
3038
|
-
|
|
3039
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4597
|
-
|
|
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
|
|
4600
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
6796
|
+
if (localTypes.size === 0) return null;
|
|
6797
|
+
localTypes.guessedVars = guessedVars;
|
|
6798
|
+
return localTypes;
|
|
5687
6799
|
}
|
|
5688
6800
|
|
|
5689
6801
|
/**
|