ucn 4.1.2 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +80 -353
- package/.claude/skills/ucn/references/commands.md +93 -0
- package/.claude/skills/ucn/references/trust-contract.md +63 -0
- package/README.md +67 -44
- package/cli/index.js +24 -19
- package/core/account.js +18 -0
- package/core/analysis.js +27 -7
- package/core/bridge.js +0 -1
- package/core/brief.js +2 -3
- package/core/build-worker.js +5 -0
- package/core/cache.js +55 -6
- package/core/callers.js +1709 -162
- package/core/check.js +107 -19
- package/core/confidence.js +29 -16
- package/core/deadcode.js +84 -24
- package/core/entrypoints.js +32 -27
- package/core/execute.js +19 -3
- package/core/graph-build.js +33 -0
- package/core/output/analysis.js +53 -14
- package/core/output/check.js +11 -0
- package/core/output/doctor.js +40 -19
- package/core/output/endpoints.js +0 -2
- package/core/output/search.js +26 -1
- package/core/parser.js +1 -1
- package/core/project.js +25 -4
- package/core/registry.js +2 -2
- package/core/reporting.js +166 -108
- package/core/search.js +271 -49
- package/core/tracing.js +2 -2
- package/core/trust-matrix.js +158 -0
- package/core/verify.js +0 -1
- package/eslint.config.js +2 -2
- package/languages/go.js +143 -13
- package/languages/html.js +5 -0
- package/languages/index.js +5 -0
- package/languages/java.js +79 -12
- package/languages/javascript.js +233 -42
- package/languages/python.js +54 -15
- package/languages/rust.js +180 -22
- package/languages/utils.js +0 -1
- package/mcp/server.js +118 -38
- package/package.json +11 -4
package/core/callers.js
CHANGED
|
@@ -122,7 +122,14 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
122
122
|
}
|
|
123
123
|
return calls;
|
|
124
124
|
} catch (e) {
|
|
125
|
-
|
|
125
|
+
// A missing/unreadable file, corrupt lazy cache, or parser exception
|
|
126
|
+
// is not equivalent to "this file has no calls". Returning null here
|
|
127
|
+
// let every consumer quietly continue with a plausible partial graph.
|
|
128
|
+
// Unsupported languages still return null through the explicit paths
|
|
129
|
+
// above; operational/engine failures abort with file context.
|
|
130
|
+
const rel = path.relative(index.root, filePath);
|
|
131
|
+
e.message = `getCachedCalls failed for ${rel}: ${e.message}`;
|
|
132
|
+
throw e;
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -172,15 +179,13 @@ function findCallers(index, name, options = {}) {
|
|
|
172
179
|
definitionLines.add(`${def.file}:${def.startLine}`);
|
|
173
180
|
}
|
|
174
181
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// (useStore's only caller was excluded other-definition under the
|
|
180
|
-
// implementation pin, a false zero-caller answer).
|
|
182
|
+
// Callable identity closure: overload signatures + implementation are one
|
|
183
|
+
// function; a TS class declaration paired with an ES5 constructor body
|
|
184
|
+
// (`function X(this: X, ...)`) is likewise one constructable runtime slot.
|
|
185
|
+
// Pinning any member must accept bindings to the compiler-equivalent one.
|
|
181
186
|
if (options.targetDefinitions && options.targetDefinitions.length > 0 &&
|
|
182
187
|
definitions.length > options.targetDefinitions.length) {
|
|
183
|
-
const closed =
|
|
188
|
+
const closed = _closeCallableIdentityGroup(options.targetDefinitions, definitions);
|
|
184
189
|
if (closed !== options.targetDefinitions) {
|
|
185
190
|
options = { ...options, targetDefinitions: closed };
|
|
186
191
|
}
|
|
@@ -426,6 +431,7 @@ function findCallers(index, name, options = {}) {
|
|
|
426
431
|
// persisted with this file's calls.
|
|
427
432
|
if (call.isMethod && call.receiver &&
|
|
428
433
|
(!call.receiverType || call.receiverTypeGuessed) &&
|
|
434
|
+
!call.receiverPatternShadow && !call.receiverFlowInvalidated &&
|
|
429
435
|
!call.receiverIsChainRoot &&
|
|
430
436
|
(langTraits(fileEntry.language)?.typeSystem === 'structural' ||
|
|
431
437
|
(collectAccount && !call.isPotentialCallback && !call.isPathCall &&
|
|
@@ -459,6 +465,27 @@ function findCallers(index, name, options = {}) {
|
|
|
459
465
|
}
|
|
460
466
|
}
|
|
461
467
|
|
|
468
|
+
// Python module-qualified constructor provenance (fix #274):
|
|
469
|
+
// `thread = threading.Thread(); thread.join()` is not evidence
|
|
470
|
+
// for a project `URL.join` merely because the parser retained
|
|
471
|
+
// only the terminal type name. Resolve the qualifier into the
|
|
472
|
+
// project when possible; external or unresolved owners remain
|
|
473
|
+
// visible as possible dispatch and can never enter tier 1.
|
|
474
|
+
if (call.isMethod && call.receiverType && call.receiverTypeQualifier &&
|
|
475
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
476
|
+
const origin = _structuralQualifiedReceiverOrigin(
|
|
477
|
+
index, fileEntry, call.receiverTypeQualifier, call.receiverType);
|
|
478
|
+
if (origin?.kind === 'project') {
|
|
479
|
+
call = { ...call, receiverTypeFlowFile: origin.fromFile };
|
|
480
|
+
} else if (origin?.kind === 'external') {
|
|
481
|
+
call = { ...call, receiverType: undefined,
|
|
482
|
+
receiverExternalFlow: origin.via };
|
|
483
|
+
} else if (origin) {
|
|
484
|
+
call = { ...call, receiverType: undefined,
|
|
485
|
+
receiverQualifiedFlow: origin.via };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
462
489
|
// Chained-receiver typing (fix #219): the receiver IS a call —
|
|
463
490
|
// `me._def.args.parseAsync(args, params).catch(...)` — so the
|
|
464
491
|
// producer's DECLARED return annotation types it (Promise →
|
|
@@ -490,12 +517,14 @@ function findCallers(index, name, options = {}) {
|
|
|
490
517
|
}
|
|
491
518
|
const folded = _foldChainedReceiverType(index, fileEntry, filePath, call, foldCtx);
|
|
492
519
|
if (folded && folded.type) {
|
|
493
|
-
call = { ...call, receiverType: folded.type
|
|
520
|
+
call = { ...call, receiverType: folded.type,
|
|
521
|
+
...(folded.fromFile && { receiverTypeFlowFile: folded.fromFile }) };
|
|
494
522
|
} else if (folded && folded.externalVia) {
|
|
495
523
|
call = { ...call, receiverExternalFlow: folded.externalVia };
|
|
496
|
-
} else {
|
|
524
|
+
} else if (!folded?.suppressFallback) {
|
|
497
525
|
const chainedType = _chainedReceiverType(index, call, fileEntry.language);
|
|
498
|
-
if (chainedType) call = { ...call, receiverType: chainedType
|
|
526
|
+
if (chainedType) call = { ...call, receiverType: chainedType.type,
|
|
527
|
+
...(chainedType.fromFile && { receiverTypeFlowFile: chainedType.fromFile }) };
|
|
499
528
|
}
|
|
500
529
|
} else if (call.isMethod && (!call.receiver || call.receiverIsChainRoot) &&
|
|
501
530
|
!call.receiverType && call.receiverCall &&
|
|
@@ -808,7 +837,10 @@ function findCallers(index, name, options = {}) {
|
|
|
808
837
|
// (super(config) bound to the subclass's OWN constructor).
|
|
809
838
|
// Super records resolve only through the parent-chain walk.
|
|
810
839
|
const selfReceivers = new Set(['self', 'cls', 'this']);
|
|
811
|
-
const
|
|
840
|
+
const indirectStructuralReceiver = call.isMethod && !call.receiver &&
|
|
841
|
+
!!(call.receiverRoot || call.receiverField || call.receiverCall);
|
|
842
|
+
const skipLocalBinding = (call.receiver && !selfReceivers.has(call.receiver)) ||
|
|
843
|
+
indirectStructuralReceiver;
|
|
812
844
|
if (!bindingId && !skipLocalBinding) {
|
|
813
845
|
// A bare call cannot bind to a METHOD def where bare names
|
|
814
846
|
// never reach methods (fix #220, cobra-measured): Go's
|
|
@@ -867,10 +899,23 @@ function findCallers(index, name, options = {}) {
|
|
|
867
899
|
// (JS/TS `new`, Java `new`, Go/Rust composite/struct literals).
|
|
868
900
|
bindingId = bindings.find(b => b.type === 'class' || b.type === 'function').id;
|
|
869
901
|
} else if (bindings.length > 1 && !call.isMethod) {
|
|
870
|
-
//
|
|
871
|
-
//
|
|
902
|
+
// Function-local classes/functions with the same name
|
|
903
|
+
// are distinct lexical bindings. Prefer the one whose
|
|
904
|
+
// enclosing function is the caller's enclosing
|
|
905
|
+
// function before module/same-class heuristics (Click
|
|
906
|
+
// defines TestContext/CustomContext independently in
|
|
907
|
+
// several tests in one file).
|
|
872
908
|
const callerSym = index.findEnclosingFunction(filePath, call.line, true);
|
|
873
|
-
|
|
909
|
+
const sameLexicalOwner = callerSym ? bindings.filter(b => {
|
|
910
|
+
const owner = index.findEnclosingFunction(filePath, b.startLine, true);
|
|
911
|
+
return owner && owner.file === callerSym.file &&
|
|
912
|
+
owner.startLine === callerSym.startLine;
|
|
913
|
+
}) : [];
|
|
914
|
+
if (sameLexicalOwner.length === 1) {
|
|
915
|
+
bindingId = sameLexicalOwner[0].id;
|
|
916
|
+
// For implicit same-class calls (Java: execute() means
|
|
917
|
+
// this.execute()), try the caller class next.
|
|
918
|
+
} else if (callerSym?.className) {
|
|
874
919
|
const callSymbols = index.symbols.get(call.name);
|
|
875
920
|
const sameClassSym = callSymbols?.find(s => s.className === callerSym.className);
|
|
876
921
|
if (sameClassSym) {
|
|
@@ -931,10 +976,39 @@ function findCallers(index, name, options = {}) {
|
|
|
931
976
|
}
|
|
932
977
|
}
|
|
933
978
|
|
|
979
|
+
// Java's bare `method()` is an implicit `this.method()`.
|
|
980
|
+
// If lexical binding lands on a base/interface declaration
|
|
981
|
+
// while the pinned target is a descendant override, runtime
|
|
982
|
+
// dispatch can reach that override. Keep the site visible as
|
|
983
|
+
// possible-dispatch instead of excluding it as another def.
|
|
984
|
+
if (collectAccount && bindingId && !call.isMethod && !call.receiver &&
|
|
985
|
+
langTraits(fileEntry.language)?.bareCallReachesMethods) {
|
|
986
|
+
const bound = (index.symbols.get(call.name) || [])
|
|
987
|
+
.find(s => s.bindingId === bindingId);
|
|
988
|
+
const tDefs = options.targetDefinitions || definitions;
|
|
989
|
+
const boundOwner = bound && (bound.className ||
|
|
990
|
+
(bound.receiver && bound.receiver.replace(/^\*/, '')));
|
|
991
|
+
const targetOwners = new Set(tDefs.map(d => d.className ||
|
|
992
|
+
(d.receiver && d.receiver.replace(/^\*/, ''))).filter(Boolean));
|
|
993
|
+
if (boundOwner && !targetOwners.has(boundOwner) &&
|
|
994
|
+
(_isAncestorOfTargetClass(index, boundOwner, tDefs) ||
|
|
995
|
+
_isDispatchAncestor(index, boundOwner, tDefs))) {
|
|
996
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
997
|
+
dispatchVia: boundOwner,
|
|
998
|
+
});
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
934
1003
|
// Smart method call handling — do this BEFORE uncertain check so
|
|
935
1004
|
// self/this.method() calls can be resolved by same-class matching
|
|
936
1005
|
// even when binding is ambiguous (e.g. method exists in multiple classes)
|
|
937
1006
|
let resolvedBySameClass = false;
|
|
1007
|
+
// Python self.attr.method() resolved from __init__/field type
|
|
1008
|
+
// inference. It shares the routing precedence of same-class
|
|
1009
|
+
// resolution, but its evidence grade is receiver-hint, not
|
|
1010
|
+
// same-class (the receiver is the field's class).
|
|
1011
|
+
let resolvedByTypedAttribute = false;
|
|
938
1012
|
// Receiver/path type known to mismatch the target: such an edge can
|
|
939
1013
|
// never tier as confirmed even when legacy includeUncertain keeps it
|
|
940
1014
|
// visible (scoreEdge checks hasReceiverType before isUncertain, so
|
|
@@ -955,6 +1029,37 @@ function findCallers(index, name, options = {}) {
|
|
|
955
1029
|
// not confirmation evidence, not exclusion evidence. Routed
|
|
956
1030
|
// method-ambiguous under the account contract.
|
|
957
1031
|
let receiverTypeUnresolved = false;
|
|
1032
|
+
if (collectAccount && call.isMethod && !call.receiverType &&
|
|
1033
|
+
fileEntry.language === 'java' && /^[A-Z]/.test(call.receiver || '')) {
|
|
1034
|
+
// Java permits inherited static methods to be invoked
|
|
1035
|
+
// through a subclass qualifier. Resolve the complete
|
|
1036
|
+
// receiver-owned overload family (subclass then parents)
|
|
1037
|
+
// before receiver-name mismatch logic. This distinguishes
|
|
1038
|
+
// TypeVariableName.get(Object.class) → inherited
|
|
1039
|
+
// TypeName.get(Type) from the subclass's inapplicable get
|
|
1040
|
+
// overloads.
|
|
1041
|
+
const callerSymbol = index.findEnclosingFunction(filePath, call.line, true) ||
|
|
1042
|
+
{ file: filePath };
|
|
1043
|
+
const qualified = _calleeTypeQualifiedReceiver(
|
|
1044
|
+
index, callerSymbol, fileEntry, call, fileEntry.language);
|
|
1045
|
+
const qualifiedTargetDefs = options.targetDefinitions || definitions;
|
|
1046
|
+
if (qualified?.match) {
|
|
1047
|
+
const targetKeys = new Set(qualifiedTargetDefs.map(d => d.bindingId ||
|
|
1048
|
+
`${d.file}:${d.startLine}`));
|
|
1049
|
+
const matchKey = qualified.match.bindingId ||
|
|
1050
|
+
`${qualified.match.file}:${qualified.match.startLine}`;
|
|
1051
|
+
if (targetKeys.has(matchKey)) {
|
|
1052
|
+
bindingId = qualified.match.bindingId;
|
|
1053
|
+
resolvedBySameClass = true;
|
|
1054
|
+
} else {
|
|
1055
|
+
recordExcluded(filePath, call.line, 'other-definition');
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
} else if (qualified?.unverified && !qualified.wrongArityOwner) {
|
|
1059
|
+
routeUnverified(filePath, fileEntry, call, qualified.unverified, calledAs);
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
958
1063
|
if (call.isMethod) {
|
|
959
1064
|
if (call.selfAttribute && fileEntry.language === 'python') {
|
|
960
1065
|
// self.attr.method() — resolve via attribute type inference
|
|
@@ -991,6 +1096,7 @@ function findCallers(index, name, options = {}) {
|
|
|
991
1096
|
continue;
|
|
992
1097
|
}
|
|
993
1098
|
resolvedBySameClass = true;
|
|
1099
|
+
resolvedByTypedAttribute = true;
|
|
994
1100
|
} else if (options.collectAccount || !options.includeMethods) {
|
|
995
1101
|
routeUnverified(filePath, fileEntry, call, 'method-no-evidence', calledAs);
|
|
996
1102
|
continue;
|
|
@@ -1179,6 +1285,21 @@ function findCallers(index, name, options = {}) {
|
|
|
1179
1285
|
fieldDispatchType = _declaredFieldInterfaceType(index, fieldHopRootType, call.receiverField, fileEntry.language);
|
|
1180
1286
|
}
|
|
1181
1287
|
|
|
1288
|
+
// Go package-owned value receiver (`io.Discard.Write`). The
|
|
1289
|
+
// imported package owns `Discard`; file/package method-name
|
|
1290
|
+
// bindings cannot identify its concrete type. Keep the edge
|
|
1291
|
+
// visible and defeat every global same-name confirmation.
|
|
1292
|
+
if (collectAccount && fileEntry.language === 'go' &&
|
|
1293
|
+
call.receiverRootIsModule) {
|
|
1294
|
+
const qualified = _goQualifiedReceiverType(
|
|
1295
|
+
index, fileEntry, call.receiverRoot, call.receiverField);
|
|
1296
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
1297
|
+
dispatchVia: qualified?.via || `${call.receiverRoot}.${call.receiverField}`,
|
|
1298
|
+
...(qualified?.kind !== 'project' && { externalContract: true }),
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1182
1303
|
// Skip uncertain calls unless resolved by same-class matching or
|
|
1183
1304
|
// explicitly requested. A declared-field hop type (fix #219) or
|
|
1184
1305
|
// interface-field dispatch type IS receiver evidence — those
|
|
@@ -1237,10 +1358,22 @@ function findCallers(index, name, options = {}) {
|
|
|
1237
1358
|
targetDefs.some(d => d.className === boundDef.className &&
|
|
1238
1359
|
d.file === boundDef.file));
|
|
1239
1360
|
}
|
|
1240
|
-
|
|
1361
|
+
// A validated self.attr receiver is the Python analogue
|
|
1362
|
+
// of a declared-field hop: the binding table only knows
|
|
1363
|
+
// the terminal name and may bind `self._live.update()` to
|
|
1364
|
+
// the enclosing class's `update`. Receiver type evidence
|
|
1365
|
+
// is stronger and must outrank that receiver-blind name
|
|
1366
|
+
// binding (rich Status._live -> Live, measured).
|
|
1367
|
+
if (!fieldHopMatchesTarget && !resolvedBySameClass && !overloadGroupBinding) {
|
|
1241
1368
|
recordExcluded(filePath, call.line, 'other-definition');
|
|
1242
1369
|
continue;
|
|
1243
1370
|
}
|
|
1371
|
+
// The mismatched binding was explicitly overruled by
|
|
1372
|
+
// stronger receiver/overload evidence. Do not carry it
|
|
1373
|
+
// forward as an "exact-binding" score: it identifies a
|
|
1374
|
+
// different definition. The surviving edge must be scored
|
|
1375
|
+
// from the evidence that actually justified it.
|
|
1376
|
+
bindingId = null;
|
|
1244
1377
|
}
|
|
1245
1378
|
|
|
1246
1379
|
// Name-level import shadowing (fix #209, httpx-measured): an
|
|
@@ -1370,7 +1503,7 @@ function findCallers(index, name, options = {}) {
|
|
|
1370
1503
|
// file imports from the target definition's file. Skips false positives like
|
|
1371
1504
|
// user_b importing from b.js being reported as a caller of a.js:process.
|
|
1372
1505
|
// Go/Java/Rust are excluded — they use package/module scoping, not file imports.
|
|
1373
|
-
if (!bindingId && options.targetDefinitions && definitions.length > 1 &&
|
|
1506
|
+
if (!bindingId && !call.isMethod && options.targetDefinitions && definitions.length > 1 &&
|
|
1374
1507
|
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
1375
1508
|
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
1376
1509
|
if (targetFiles.size > 0 && !targetFiles.has(filePath)) {
|
|
@@ -1687,6 +1820,28 @@ function findCallers(index, name, options = {}) {
|
|
|
1687
1820
|
let knownType = call.receiverType || fieldHopType;
|
|
1688
1821
|
if (knownType && _isGenericParamReceiverType(index, filePath, call.line, knownType)) knownType = null;
|
|
1689
1822
|
if (knownType) {
|
|
1823
|
+
// Go package-qualified annotation identity (fix
|
|
1824
|
+
// #273): `next http.Handler` is not a project-local
|
|
1825
|
+
// `Handler`. External/opaque packages may expose an
|
|
1826
|
+
// interface (implicit satisfaction), and a project
|
|
1827
|
+
// interface is explicitly a dispatch contract.
|
|
1828
|
+
// Both stay visible; neither can confirm a pinned
|
|
1829
|
+
// concrete implementation.
|
|
1830
|
+
if (collectAccount && fileEntry.language === 'go' &&
|
|
1831
|
+
call.receiverTypeQualifier) {
|
|
1832
|
+
const qualified = _goQualifiedReceiverType(
|
|
1833
|
+
index, fileEntry, call.receiverTypeQualifier, knownType);
|
|
1834
|
+
const isContract = qualified?.kind === 'project' &&
|
|
1835
|
+
qualified.defs.some(d => d.type === 'interface' || d.type === 'trait');
|
|
1836
|
+
if (qualified && (qualified.kind !== 'project' || isContract)) {
|
|
1837
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
1838
|
+
dispatchVia: qualified.via,
|
|
1839
|
+
dispatchCandidates: countDispatchCandidates(knownType),
|
|
1840
|
+
...(qualified.kind !== 'project' && { externalContract: true }),
|
|
1841
|
+
});
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1690
1845
|
const viaFieldHop = !call.receiverType; // declared-field hop (fix #202)
|
|
1691
1846
|
// Exclusion requires an UNRELATED type. A receiver typed
|
|
1692
1847
|
// as an ANCESTOR of the target's class may dynamically
|
|
@@ -1697,7 +1852,8 @@ function findCallers(index, name, options = {}) {
|
|
|
1697
1852
|
const structural = langTraits(fileEntry.language)?.typeSystem === 'structural';
|
|
1698
1853
|
if (targetTypes.has(knownType)) {
|
|
1699
1854
|
receiverTypeValidated = true;
|
|
1700
|
-
// Identity discipline (
|
|
1855
|
+
// Identity discipline (fix #206, extended to
|
|
1856
|
+
// structural flow in #272): a
|
|
1701
1857
|
// NAME match is only identity when the
|
|
1702
1858
|
// unqualified type name resolves (same file →
|
|
1703
1859
|
// same package directory → import edge) to the
|
|
@@ -1722,6 +1878,26 @@ function findCallers(index, name, options = {}) {
|
|
|
1722
1878
|
receiverTypeValidated = false;
|
|
1723
1879
|
receiverTypeUnresolved = true;
|
|
1724
1880
|
}
|
|
1881
|
+
} else if (structural) {
|
|
1882
|
+
// Multiple project types may share the
|
|
1883
|
+
// same class name across packages/versions
|
|
1884
|
+
// (zod v3 + v4 both define ZodArray). A
|
|
1885
|
+
// folded/module return type carries the
|
|
1886
|
+
// annotation's origin file; use it before
|
|
1887
|
+
// accepting the name as target identity.
|
|
1888
|
+
const sameNameTypeDefs = (index.symbols.get(knownType) || [])
|
|
1889
|
+
.filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
1890
|
+
if (sameNameTypeDefs.length > 1 ||
|
|
1891
|
+
(call.receiverTypeQualifier && call.receiverTypeFlowFile)) {
|
|
1892
|
+
const identity = _resolveStructuralFlowTypeIdentity(
|
|
1893
|
+
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs);
|
|
1894
|
+
if (identity === 'other') {
|
|
1895
|
+
receiverTypeValidated = false;
|
|
1896
|
+
} else if (identity === 'unknown') {
|
|
1897
|
+
receiverTypeValidated = false;
|
|
1898
|
+
receiverTypeUnresolved = true;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1725
1901
|
}
|
|
1726
1902
|
}
|
|
1727
1903
|
const matchesTarget = receiverTypeValidated ||
|
|
@@ -1916,7 +2092,9 @@ function findCallers(index, name, options = {}) {
|
|
|
1916
2092
|
// call.receiver guard: a generic-param knownType
|
|
1917
2093
|
// (fix #220) reaches here receiver-less — there is
|
|
1918
2094
|
// no receiver NAME to match against.
|
|
1919
|
-
if (call.receiver && !inferredMatch && !inferredMismatch &&
|
|
2095
|
+
if (call.receiver && !inferredMatch && !inferredMismatch &&
|
|
2096
|
+
definitions.length > 1 && !fieldDispatchType &&
|
|
2097
|
+
!call.receiverExternalFlow) {
|
|
1920
2098
|
const receiverLower = call.receiver.toLowerCase();
|
|
1921
2099
|
const matchesTarget = [...targetTypes].some(cn => cn.toLowerCase() === receiverLower);
|
|
1922
2100
|
// Type-qualified identity discipline (fix #220,
|
|
@@ -2559,6 +2737,12 @@ function findCallers(index, name, options = {}) {
|
|
|
2559
2737
|
});
|
|
2560
2738
|
continue;
|
|
2561
2739
|
}
|
|
2740
|
+
if (!typeQualifiedReceiver && call.receiverQualifiedFlow) {
|
|
2741
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
2742
|
+
dispatchVia: call.receiverQualifiedFlow,
|
|
2743
|
+
});
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2562
2746
|
// Builtin-global receiver (fix #232, campaign-measured:
|
|
2563
2747
|
// console.log() confirmed scope-match against a private
|
|
2564
2748
|
// Logger.log — its single project-wide owner). console/
|
|
@@ -2709,13 +2893,13 @@ function findCallers(index, name, options = {}) {
|
|
|
2709
2893
|
calledAs,
|
|
2710
2894
|
_evidence: {
|
|
2711
2895
|
hasBindingId: !!bindingId,
|
|
2712
|
-
resolvedBySameClass: !!resolvedBySameClass,
|
|
2896
|
+
resolvedBySameClass: !!resolvedBySameClass && !resolvedByTypedAttribute,
|
|
2713
2897
|
hasSamePackageEvidence,
|
|
2714
2898
|
// Method calls where binding resolution was skipped (non-self receiver)
|
|
2715
2899
|
// and the receiver has no binding evidence → uncertain (JS/TS/Python only)
|
|
2716
2900
|
isUncertain: !!isUncertain || uncertainMethodReceiver,
|
|
2717
2901
|
hasReceiverType: langTraits(fileEntry.language)?.typeSystem === 'structural'
|
|
2718
|
-
? receiverTypeValidated
|
|
2902
|
+
? (receiverTypeValidated || resolvedByTypedAttribute)
|
|
2719
2903
|
: !!call.receiverType,
|
|
2720
2904
|
hasReceiverEvidence: !!(call.receiver &&
|
|
2721
2905
|
(fileEntry.bindings || []).some(b => b.name === call.receiver)),
|
|
@@ -2726,8 +2910,12 @@ function findCallers(index, name, options = {}) {
|
|
|
2726
2910
|
pendingCount++;
|
|
2727
2911
|
}
|
|
2728
2912
|
} catch (e) {
|
|
2729
|
-
//
|
|
2730
|
-
//
|
|
2913
|
+
// Parsing/read failures are contained by getCachedCalls(). Any
|
|
2914
|
+
// exception that reaches this semantic loop is an engine defect;
|
|
2915
|
+
// returning a partial caller set would be a high-trust lie.
|
|
2916
|
+
const rel = path.relative(index.root, filePath);
|
|
2917
|
+
e.message = `findCallers failed while analyzing ${rel}: ${e.message}`;
|
|
2918
|
+
throw e;
|
|
2731
2919
|
}
|
|
2732
2920
|
}
|
|
2733
2921
|
|
|
@@ -2751,7 +2939,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2751
2939
|
let unverifiedEnriched = 0;
|
|
2752
2940
|
|
|
2753
2941
|
// Phase 2: Read content only for files with matching calls (eliminates ~98% of file reads)
|
|
2754
|
-
|
|
2942
|
+
for (const [filePath, pending] of pendingByFile) {
|
|
2755
2943
|
let content = null;
|
|
2756
2944
|
for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType, calledAs, _evidence, _tier, _reason, _meta } of pending) {
|
|
2757
2945
|
const scored = scoreEdge(_evidence || {});
|
|
@@ -2769,6 +2957,8 @@ function findCallers(index, name, options = {}) {
|
|
|
2769
2957
|
relativePath: fileEntry.relativePath,
|
|
2770
2958
|
line: call.line,
|
|
2771
2959
|
confidence: scored.confidence,
|
|
2960
|
+
evidenceScore: scored.evidenceScore,
|
|
2961
|
+
scoreKind: scored.scoreKind,
|
|
2772
2962
|
resolution: scored.resolution,
|
|
2773
2963
|
tier: _tier,
|
|
2774
2964
|
reason: _reason,
|
|
@@ -2782,7 +2972,10 @@ function findCallers(index, name, options = {}) {
|
|
|
2782
2972
|
if (unverifiedEnriched < unverifiedEnrichLimit) {
|
|
2783
2973
|
if (content === null) {
|
|
2784
2974
|
try { content = fs.readFileSync(filePath, 'utf-8'); }
|
|
2785
|
-
catch (e) {
|
|
2975
|
+
catch (e) {
|
|
2976
|
+
e.message = `caller enrichment read failed: ${e.message}`;
|
|
2977
|
+
throw e;
|
|
2978
|
+
}
|
|
2786
2979
|
}
|
|
2787
2980
|
const enclosing = index.findEnclosingFunction(filePath, call.line, true);
|
|
2788
2981
|
unverifiedEntries.push({
|
|
@@ -2812,6 +3005,8 @@ function findCallers(index, name, options = {}) {
|
|
|
2812
3005
|
relativePath: fileEntry.relativePath,
|
|
2813
3006
|
line: call.line,
|
|
2814
3007
|
confidence: scored.confidence,
|
|
3008
|
+
evidenceScore: scored.evidenceScore,
|
|
3009
|
+
scoreKind: scored.scoreKind,
|
|
2815
3010
|
resolution: scored.resolution,
|
|
2816
3011
|
...(tier && { tier }),
|
|
2817
3012
|
isMethod: call.isMethod || false,
|
|
@@ -2825,7 +3020,10 @@ function findCallers(index, name, options = {}) {
|
|
|
2825
3020
|
// First time we hit this file's enrichment loop — read the file once.
|
|
2826
3021
|
if (content === null) {
|
|
2827
3022
|
try { content = fs.readFileSync(filePath, 'utf-8'); }
|
|
2828
|
-
catch (e) {
|
|
3023
|
+
catch (e) {
|
|
3024
|
+
e.message = `caller enrichment read failed: ${e.message}`;
|
|
3025
|
+
throw e;
|
|
3026
|
+
}
|
|
2829
3027
|
}
|
|
2830
3028
|
callers.push({
|
|
2831
3029
|
file: filePath,
|
|
@@ -2842,6 +3040,8 @@ function findCallers(index, name, options = {}) {
|
|
|
2842
3040
|
...(receiverType && { receiverType }),
|
|
2843
3041
|
...(edgeCalledAs && { calledAs: edgeCalledAs }),
|
|
2844
3042
|
confidence: scored.confidence,
|
|
3043
|
+
evidenceScore: scored.evidenceScore,
|
|
3044
|
+
scoreKind: scored.scoreKind,
|
|
2845
3045
|
resolution: scored.resolution,
|
|
2846
3046
|
...(tier && { tier }),
|
|
2847
3047
|
});
|
|
@@ -2905,20 +3105,40 @@ function findCallers(index, name, options = {}) {
|
|
|
2905
3105
|
* - Python: receiverType from __init__ attribute type inference (getInstanceAttributeTypes)
|
|
2906
3106
|
*
|
|
2907
3107
|
* @param {object} index - ProjectIndex instance
|
|
2908
|
-
* @param {object}
|
|
3108
|
+
* @param {object|string} definition - Symbol definition or resolvable symbol name
|
|
2909
3109
|
* @param {object} [options] - Options
|
|
2910
3110
|
* @param {boolean} [options.includeMethods] - Include method calls (default: false)
|
|
2911
3111
|
*/
|
|
2912
|
-
function findCallees(index,
|
|
3112
|
+
function findCallees(index, definition, options = {}) {
|
|
2913
3113
|
index._beginOp();
|
|
2914
3114
|
try {
|
|
3115
|
+
const def = typeof definition === 'string'
|
|
3116
|
+
? index.resolveSymbol(definition, options).def
|
|
3117
|
+
: definition;
|
|
3118
|
+
if (!def) return [];
|
|
3119
|
+
if (!def.file) throw new TypeError('findCallees requires a definition with a file');
|
|
2915
3120
|
// Lazy-load callsCache from disk if not already populated
|
|
2916
3121
|
if (index.loadCallsCache) index.loadCallsCache();
|
|
2917
3122
|
|
|
2918
3123
|
try {
|
|
2919
3124
|
// Get all calls from the file's cache (now includes enclosingFunction)
|
|
2920
|
-
const
|
|
2921
|
-
if (!
|
|
3125
|
+
const allCalls = getCachedCalls(index, def.file);
|
|
3126
|
+
if (!allCalls) return [];
|
|
3127
|
+
|
|
3128
|
+
// A file can contain hundreds of symbols and thousands of calls.
|
|
3129
|
+
// Reachability invokes findCallees once per reachable symbol; scanning
|
|
3130
|
+
// the full file for every symbol made that walk O(symbols*calls)
|
|
3131
|
+
// (14-16s on clap). Restrict the resolution loop and local-type scan
|
|
3132
|
+
// to this definition's source range. Nested closures deliberately
|
|
3133
|
+
// remain in the slice and retain the existing inner-symbol rules.
|
|
3134
|
+
const calls = _callsInDefinitionRange(index, def.file, allCalls,
|
|
3135
|
+
def.startLine, def.endLine);
|
|
3136
|
+
// The reachability walk uses the legacy (non-accounting) path and most
|
|
3137
|
+
// entry/test symbols contain no calls. Avoid constructing receiver,
|
|
3138
|
+
// overload, and flow machinery for an empty source range. Contract
|
|
3139
|
+
// callers still run below so they receive the explicit zero-site
|
|
3140
|
+
// callee account object.
|
|
3141
|
+
if (calls.length === 0 && !options.collectAccount) return [];
|
|
2922
3142
|
|
|
2923
3143
|
// Get file language for smart method call handling
|
|
2924
3144
|
const fileEntry = index.files.get(def.file);
|
|
@@ -2928,12 +3148,9 @@ function findCallees(index, def, options = {}) {
|
|
|
2928
3148
|
// Only class methods are excluded — they are independently addressable symbols.
|
|
2929
3149
|
// Calls within closures (named functions without className) ARE included as
|
|
2930
3150
|
// callees of the parent function, since closures are part of the parent's behavior.
|
|
2931
|
-
const innerSymbolRanges = fileEntry
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
s.startLine > def.startLine && s.endLine <= def.endLine &&
|
|
2935
|
-
s.startLine !== def.startLine)
|
|
2936
|
-
.map(s => [s.startLine, s.endLine]) : [];
|
|
3151
|
+
const innerSymbolRanges = fileEntry
|
|
3152
|
+
? _innerClassMethodRanges(index, def, fileEntry)
|
|
3153
|
+
: [];
|
|
2937
3154
|
|
|
2938
3155
|
const callees = new Map(); // key -> { name, bindingId, count }
|
|
2939
3156
|
let selfAttrCalls = null; // collected for Python self.attr.method() resolution
|
|
@@ -2984,20 +3201,71 @@ function findCallees(index, def, options = {}) {
|
|
|
2984
3201
|
};
|
|
2985
3202
|
// Retain an uncertain/unresolved call as a visible unverified callee
|
|
2986
3203
|
// entry (aggregated by name+reason) and claim its site.
|
|
2987
|
-
const noteUnverified = (siteId, call, reason) => {
|
|
3204
|
+
const noteUnverified = (siteId, call, reason, meta = {}) => {
|
|
2988
3205
|
if (!collectAccount || claimedSiteIds.has(siteId)) return;
|
|
2989
3206
|
noteSite(siteId, 'unverified', reason, call);
|
|
2990
|
-
const key = `${call.name}|${reason}`;
|
|
3207
|
+
const key = `${call.name}|${reason}|${meta.dispatchVia || ''}`;
|
|
2991
3208
|
let entry = unverifiedCallees.get(key);
|
|
2992
3209
|
if (!entry) {
|
|
2993
3210
|
const defs = index.symbols.get(call.name) || [];
|
|
2994
3211
|
const owners = defs.filter(s => !NON_CALLABLE_TYPES.has(s.type)).length;
|
|
2995
|
-
entry = { name: call.name, reason, callCount: 0, sites: [], ownerCount: owners };
|
|
3212
|
+
entry = { name: call.name, reason, callCount: 0, sites: [], ownerCount: owners, ...meta };
|
|
2996
3213
|
unverifiedCallees.set(key, entry);
|
|
2997
3214
|
}
|
|
2998
3215
|
entry.callCount++;
|
|
2999
3216
|
entry.sites.push(call.line);
|
|
3000
3217
|
};
|
|
3218
|
+
// A statically selected base implementation is not the only runtime
|
|
3219
|
+
// target in languages with virtual/structural dispatch. If a project
|
|
3220
|
+
// descendant overrides the method, the call site must stay visible as
|
|
3221
|
+
// possible-dispatch instead of being presented as an exact callee.
|
|
3222
|
+
// This is contract-only so legacy traversal remains byte-compatible.
|
|
3223
|
+
const routeVirtualOverride = (siteId, call, receiverType, match) => {
|
|
3224
|
+
if (!collectAccount || !match) return false;
|
|
3225
|
+
const traits = langTraits(language);
|
|
3226
|
+
const matchedOwner = match.className || (match.receiver && match.receiver.replace(/^\*/, ''));
|
|
3227
|
+
if (!matchedOwner) return false;
|
|
3228
|
+
|
|
3229
|
+
let dispatchBase = receiverType || matchedOwner;
|
|
3230
|
+
const explicitContract = _isDispatchContractType(index, dispatchBase);
|
|
3231
|
+
// Go is nominal in UCN's receiver-resolution taxonomy, but its
|
|
3232
|
+
// interfaces dispatch structurally and are satisfied implicitly.
|
|
3233
|
+
// A project interface is therefore a virtual boundary even though
|
|
3234
|
+
// there is no implements edge in the graph.
|
|
3235
|
+
if (traits?.typeSystem !== 'structural' && !traits?.allMethodsVirtual &&
|
|
3236
|
+
!explicitContract) return false;
|
|
3237
|
+
if (explicitContract) {
|
|
3238
|
+
const candidates = _countDispatchCandidates(
|
|
3239
|
+
index, dispatchBase, index.symbols.get(call.name) || []);
|
|
3240
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3241
|
+
dispatchVia: dispatchBase,
|
|
3242
|
+
dispatchCandidates: Math.max(1, candidates),
|
|
3243
|
+
});
|
|
3244
|
+
return true;
|
|
3245
|
+
}
|
|
3246
|
+
let owners = _descendantOverrideOwners(index, dispatchBase, call.name);
|
|
3247
|
+
// Structural aliases/generic heads (for example ZodTypeAny) may
|
|
3248
|
+
// have no inheritance node of their own. When the statically
|
|
3249
|
+
// selected declaration lives on an ancestor, use that declaration
|
|
3250
|
+
// owner as the conservative virtual-dispatch boundary. Do not do
|
|
3251
|
+
// this for a concrete class: an exact Child receiver cannot become
|
|
3252
|
+
// an unrelated sibling of Child.
|
|
3253
|
+
if (owners.size === 0 && dispatchBase !== matchedOwner) {
|
|
3254
|
+
const typeDefs = index.symbols.get(dispatchBase) || [];
|
|
3255
|
+
const isConcreteClass = typeDefs.some(d =>
|
|
3256
|
+
d.type === 'class' || d.type === 'struct' || d.type === 'impl');
|
|
3257
|
+
if (!isConcreteClass) {
|
|
3258
|
+
dispatchBase = matchedOwner;
|
|
3259
|
+
owners = _descendantOverrideOwners(index, dispatchBase, call.name);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
if (owners.size === 0) return false;
|
|
3263
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3264
|
+
dispatchVia: dispatchBase,
|
|
3265
|
+
dispatchCandidates: owners.size + 1,
|
|
3266
|
+
});
|
|
3267
|
+
return true;
|
|
3268
|
+
};
|
|
3001
3269
|
|
|
3002
3270
|
// Build local variable type map for receiver resolution
|
|
3003
3271
|
// Scans for patterns like: bt = Backtester(...) → bt maps to Backtester
|
|
@@ -3012,7 +3280,7 @@ function findCallees(index, def, options = {}) {
|
|
|
3012
3280
|
// resolution needs the external-producer/typed-receiver defeater).
|
|
3013
3281
|
let _flowMap;
|
|
3014
3282
|
const flowMap = () => {
|
|
3015
|
-
if (_flowMap === undefined) _flowMap = _buildReturnTypeFlowMap(index, def.file,
|
|
3283
|
+
if (_flowMap === undefined) _flowMap = _buildReturnTypeFlowMap(index, def.file, allCalls);
|
|
3016
3284
|
return _flowMap;
|
|
3017
3285
|
};
|
|
3018
3286
|
// Chained-receiver fold context (fix #268 — the #258 rails, callee
|
|
@@ -3044,6 +3312,16 @@ function findCallees(index, def, options = {}) {
|
|
|
3044
3312
|
if (!isDirectMatch && !isNestedCallback) continue;
|
|
3045
3313
|
if (calleeAccount) calleeAccount.totalSites++;
|
|
3046
3314
|
|
|
3315
|
+
// Query-time return flow for an ordinary receiver assignment:
|
|
3316
|
+
// `v := New(); v.ReadConfig()`. The compiler-declared return type
|
|
3317
|
+
// is stronger than constructor-name guesses and must participate
|
|
3318
|
+
// in callee identity just as it already does for callers.
|
|
3319
|
+
const directReceiverFlow = call.isMethod && call.receiver && !call.receiverType &&
|
|
3320
|
+
!call.receiverPatternShadow && !call.receiverFlowInvalidated &&
|
|
3321
|
+
!['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver)
|
|
3322
|
+
? _lookupReturnTypeFlow(flowMap(), call)
|
|
3323
|
+
: undefined;
|
|
3324
|
+
|
|
3047
3325
|
// Declared-field receiver hop (fix #231 — callee-side parity
|
|
3048
3326
|
// with the caller side's #202/#219): `tm.service.Save()` /
|
|
3049
3327
|
// `this._map.has()` records carry receiverRoot/receiverField —
|
|
@@ -3056,8 +3334,19 @@ function findCallees(index, def, options = {}) {
|
|
|
3056
3334
|
// trait-typed and generic-param fields return null.
|
|
3057
3335
|
let fieldHopType = null;
|
|
3058
3336
|
let fieldHopInfo = null;
|
|
3337
|
+
let fieldDispatchType = null;
|
|
3059
3338
|
if (call.isMethod && !call.receiverType && call.receiverField) {
|
|
3060
3339
|
let hopRoot = call.receiverRootType;
|
|
3340
|
+
if (!hopRoot && call.receiverRoot && localTypes?.has(call.receiverRoot)) {
|
|
3341
|
+
hopRoot = localTypes.get(call.receiverRoot);
|
|
3342
|
+
}
|
|
3343
|
+
if (!hopRoot && call.receiverRoot) {
|
|
3344
|
+
const inferredRoot = _lookupReturnTypeFlow(flowMap(), {
|
|
3345
|
+
...call,
|
|
3346
|
+
receiver: call.receiverRoot,
|
|
3347
|
+
});
|
|
3348
|
+
if (inferredRoot?.type) hopRoot = inferredRoot.type;
|
|
3349
|
+
}
|
|
3061
3350
|
if (!hopRoot && call.receiverRoot === 'this' &&
|
|
3062
3351
|
langTraits(language)?.typeSystem === 'structural') {
|
|
3063
3352
|
hopRoot = index.findEnclosingFunction(def.file, call.line, true)?.className;
|
|
@@ -3065,9 +3354,78 @@ function findCallees(index, def, options = {}) {
|
|
|
3065
3354
|
if (hopRoot) {
|
|
3066
3355
|
fieldHopInfo = {};
|
|
3067
3356
|
fieldHopType = _declaredFieldType(index, hopRoot, call.receiverField, language, fieldHopInfo);
|
|
3357
|
+
if (!fieldHopType) {
|
|
3358
|
+
fieldDispatchType = _declaredFieldInterfaceType(
|
|
3359
|
+
index, hopRoot, call.receiverField, language);
|
|
3360
|
+
}
|
|
3068
3361
|
}
|
|
3069
3362
|
}
|
|
3070
3363
|
|
|
3364
|
+
if (fieldDispatchType) {
|
|
3365
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3366
|
+
dispatchVia: fieldDispatchType,
|
|
3367
|
+
dispatchCandidates: _countDispatchCandidates(
|
|
3368
|
+
index, fieldDispatchType, index.symbols.get(call.name) || []),
|
|
3369
|
+
});
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
if (collectAccount && language === 'go' && call.receiverRootIsModule) {
|
|
3374
|
+
const qualified = _goQualifiedReceiverType(
|
|
3375
|
+
index, fileEntry, call.receiverRoot, call.receiverField);
|
|
3376
|
+
if (_calleeZeroCandidateName(index, call)) {
|
|
3377
|
+
noteSite(siteId, 'external', null, call);
|
|
3378
|
+
} else {
|
|
3379
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3380
|
+
dispatchVia: qualified?.via || `${call.receiverRoot}.${call.receiverField}`,
|
|
3381
|
+
...(qualified?.kind !== 'project' && { externalContract: true }),
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
continue;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// Macro invocations occupy their own callable namespace/shape.
|
|
3388
|
+
// A same-named method or function is never the macro callee
|
|
3389
|
+
// (`arg!(...)` beside `.arg(...)` was resolving to ArgGroup.arg
|
|
3390
|
+
// in clap). Resolve a unique project macro exactly; ambiguity is
|
|
3391
|
+
// visible under the account contract, and absent macros are
|
|
3392
|
+
// external. Never fall through to generic name ranking.
|
|
3393
|
+
if (call.isMacro) {
|
|
3394
|
+
const macroDefs = (index.symbols.get(call.name) || []).filter(s =>
|
|
3395
|
+
s.type === 'macro' && _calleeLanguageCompatible(index, s, language));
|
|
3396
|
+
const uniqueMacro = macroDefs.length === 1 ? macroDefs[0] : null;
|
|
3397
|
+
const macroInScope = !!uniqueMacro && (
|
|
3398
|
+
(uniqueMacro.file === def.file && uniqueMacro.startLine <= call.line) ||
|
|
3399
|
+
!!call.receiver ||
|
|
3400
|
+
(fileEntry?.importBindings || []).some(b => b.name === call.name));
|
|
3401
|
+
if (uniqueMacro && macroInScope) {
|
|
3402
|
+
const match = uniqueMacro;
|
|
3403
|
+
const key = match.bindingId || `${match.file}:${match.startLine}:${call.name}`;
|
|
3404
|
+
const existing = callees.get(key);
|
|
3405
|
+
if (existing) {
|
|
3406
|
+
existing.count += 1;
|
|
3407
|
+
if (collectAccount) {
|
|
3408
|
+
existing.sites.push(call.line);
|
|
3409
|
+
existing.siteIds.push(siteId);
|
|
3410
|
+
}
|
|
3411
|
+
} else {
|
|
3412
|
+
callees.set(key, {
|
|
3413
|
+
name: call.name,
|
|
3414
|
+
bindingId: match.bindingId,
|
|
3415
|
+
count: 1,
|
|
3416
|
+
...(collectAccount && { sites: [call.line], siteIds: [siteId] }),
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
} else if (macroDefs.length > 1) {
|
|
3420
|
+
noteUnverified(siteId, call, 'macro-ambiguous');
|
|
3421
|
+
} else if (uniqueMacro) {
|
|
3422
|
+
noteUnverified(siteId, call, 'macro-scope-unknown');
|
|
3423
|
+
} else {
|
|
3424
|
+
noteSite(siteId, 'external', null, call);
|
|
3425
|
+
}
|
|
3426
|
+
continue;
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3071
3429
|
// Go package-qualified receiver: resolve the import module up
|
|
3072
3430
|
// front so the dispatch chain can tell package calls apart from
|
|
3073
3431
|
// type-qualified method expressions (fix #236 — a receiver that
|
|
@@ -3075,18 +3433,25 @@ function findCallees(index, def, options = {}) {
|
|
|
3075
3433
|
let goImportModule = null;
|
|
3076
3434
|
if (call.receiver && langTraits(language)?.hasReceiverPackageCalls) {
|
|
3077
3435
|
const goImports = fileEntry?.imports || [];
|
|
3436
|
+
const goImportNames = fileEntry?.importNames || [];
|
|
3078
3437
|
// Handle Go version suffixes: k8s.io/klog/v2 → klog, not v2
|
|
3079
3438
|
// fix #268 (chi-measured): computed for NON-method records
|
|
3080
3439
|
// too — the parser marks `context.WithValue(...)` isMethod:
|
|
3081
3440
|
// false, which used to skip the whole package-ownership
|
|
3082
3441
|
// dispatch and confirm the bare name against the project's
|
|
3083
3442
|
// only WithValue def (middleware/value.go).
|
|
3084
|
-
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3443
|
+
if (goImportNames.length === goImports.length) {
|
|
3444
|
+
const importIndex = goImportNames.indexOf(call.receiver);
|
|
3445
|
+
if (importIndex >= 0) goImportModule = goImports[importIndex];
|
|
3446
|
+
}
|
|
3447
|
+
if (!goImportModule) {
|
|
3448
|
+
goImportModule = goImports.find(mod => {
|
|
3449
|
+
const parts = mod.split('/');
|
|
3450
|
+
const last = parts[parts.length - 1];
|
|
3451
|
+
const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
3452
|
+
return pkgName === call.receiver;
|
|
3453
|
+
}) || null;
|
|
3454
|
+
}
|
|
3090
3455
|
}
|
|
3091
3456
|
|
|
3092
3457
|
// Type-qualified receiver resolution (fix #236): the receiver
|
|
@@ -3140,16 +3505,44 @@ function findCallees(index, def, options = {}) {
|
|
|
3140
3505
|
// Rust Self::method() resolves same-impl the same way (fix #236, the #232 callee analog)
|
|
3141
3506
|
} else if (call.receiver === 'super') {
|
|
3142
3507
|
// super().method() — resolve to parent class method below
|
|
3143
|
-
} else if (
|
|
3508
|
+
} else if (directReceiverFlow?.externalVia) {
|
|
3509
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3510
|
+
dispatchVia: directReceiverFlow.externalVia,
|
|
3511
|
+
externalContract: true,
|
|
3512
|
+
});
|
|
3513
|
+
continue;
|
|
3514
|
+
} else if (localTypes && localTypes.has(call.receiver) && !directReceiverFlow?.type) {
|
|
3144
3515
|
// Resolve method calls on locally-constructed objects:
|
|
3145
3516
|
// bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
|
|
3146
3517
|
// Go: f.Run() where f is *Framework → Framework.Run (receiver match)
|
|
3147
3518
|
const typeName = localTypes.get(call.receiver);
|
|
3148
3519
|
const symbols = index.symbols.get(call.name);
|
|
3520
|
+
const qualifiedType = language === 'go'
|
|
3521
|
+
? _goQualifiedReceiverType(index, fileEntry,
|
|
3522
|
+
call.receiverTypeQualifier, typeName)
|
|
3523
|
+
: null;
|
|
3524
|
+
const qualifiedContract = qualifiedType?.kind === 'project' &&
|
|
3525
|
+
qualifiedType.defs.some(d => d.type === 'interface' || d.type === 'trait');
|
|
3526
|
+
if (qualifiedType && (qualifiedType.kind !== 'project' || qualifiedContract)) {
|
|
3527
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3528
|
+
dispatchVia: qualifiedType.via,
|
|
3529
|
+
dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
|
|
3530
|
+
...(qualifiedType.kind !== 'project' && { externalContract: true }),
|
|
3531
|
+
});
|
|
3532
|
+
continue;
|
|
3533
|
+
}
|
|
3534
|
+
if (!qualifiedType && _isDispatchContractType(index, typeName)) {
|
|
3535
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3536
|
+
dispatchVia: typeName,
|
|
3537
|
+
dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
|
|
3538
|
+
});
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3149
3541
|
const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
3150
3542
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
3151
3543
|
const onClass = (cls) => (symbols || []).filter(s =>
|
|
3152
|
-
isCallable(s) &&
|
|
3544
|
+
isCallable(s) &&
|
|
3545
|
+
(!qualifiedType?.dir || (s.file && path.dirname(s.file) === qualifiedType.dir)) && (
|
|
3153
3546
|
s.className === cls ||
|
|
3154
3547
|
(s.receiver && s.receiver.replace(/^\*/, '') === cls)));
|
|
3155
3548
|
// Same-class overloads select by static call shape —
|
|
@@ -3172,6 +3565,7 @@ function findCallees(index, def, options = {}) {
|
|
|
3172
3565
|
}
|
|
3173
3566
|
const match = sel.match;
|
|
3174
3567
|
if (match) {
|
|
3568
|
+
if (routeVirtualOverride(siteId, call, typeName, match)) continue;
|
|
3175
3569
|
const key = match.bindingId || `${typeName}.${call.name}`;
|
|
3176
3570
|
const existing = callees.get(key);
|
|
3177
3571
|
if (existing) {
|
|
@@ -3199,7 +3593,8 @@ function findCallees(index, def, options = {}) {
|
|
|
3199
3593
|
noteUnverified(siteId, call, 'uncertain-receiver');
|
|
3200
3594
|
}
|
|
3201
3595
|
continue;
|
|
3202
|
-
} else if (call.receiverType ||
|
|
3596
|
+
} else if (call.receiverType || directReceiverFlow?.type ||
|
|
3597
|
+
fieldHopType || fieldHopInfo?.externalVia) {
|
|
3203
3598
|
// Use parser-inferred receiverType for method resolution
|
|
3204
3599
|
// Go/Java/Rust: from param/receiver type declarations
|
|
3205
3600
|
// JS/TS: from `new Foo()` assignments or TypeScript type annotations
|
|
@@ -3221,12 +3616,36 @@ function findCallees(index, def, options = {}) {
|
|
|
3221
3616
|
}
|
|
3222
3617
|
continue;
|
|
3223
3618
|
}
|
|
3224
|
-
const typeName = call.receiverType || fieldHopType;
|
|
3619
|
+
const typeName = call.receiverType || directReceiverFlow?.type || fieldHopType;
|
|
3225
3620
|
const symbols = index.symbols.get(call.name);
|
|
3621
|
+
const qualifiedType = language === 'go' && call.receiverType
|
|
3622
|
+
? _goQualifiedReceiverType(index, fileEntry,
|
|
3623
|
+
call.receiverTypeQualifier, typeName)
|
|
3624
|
+
: null;
|
|
3625
|
+
const qualifiedContract = qualifiedType?.kind === 'project' &&
|
|
3626
|
+
qualifiedType.defs.some(d => d.type === 'interface' || d.type === 'trait');
|
|
3627
|
+
if (qualifiedType && (qualifiedType.kind !== 'project' || qualifiedContract)) {
|
|
3628
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3629
|
+
dispatchVia: qualifiedType.via,
|
|
3630
|
+
dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
|
|
3631
|
+
...(qualifiedType.kind !== 'project' && { externalContract: true }),
|
|
3632
|
+
});
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
if (!qualifiedType && _isDispatchContractType(index, typeName)) {
|
|
3636
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3637
|
+
dispatchVia: typeName,
|
|
3638
|
+
dispatchCandidates: _countDispatchCandidates(index, typeName, symbols || []),
|
|
3639
|
+
});
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3226
3642
|
const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
3227
3643
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
3228
3644
|
const onClassRT = (cls) => (symbols || []).filter(s =>
|
|
3229
|
-
isCallableRT(s) &&
|
|
3645
|
+
isCallableRT(s) &&
|
|
3646
|
+
(!qualifiedType?.dir || (s.file && path.dirname(s.file) === qualifiedType.dir)) &&
|
|
3647
|
+
(!directReceiverFlow?.fromFile || !s.file ||
|
|
3648
|
+
path.dirname(s.file) === path.dirname(directReceiverFlow.fromFile)) && (
|
|
3230
3649
|
(s.receiver && s.receiver.replace(/^\*/, '') === cls) ||
|
|
3231
3650
|
s.className === cls));
|
|
3232
3651
|
// Same-class overload selection by static call shape (fix #268)
|
|
@@ -3247,6 +3666,7 @@ function findCallees(index, def, options = {}) {
|
|
|
3247
3666
|
}
|
|
3248
3667
|
const match = sel.match;
|
|
3249
3668
|
if (match) {
|
|
3669
|
+
if (routeVirtualOverride(siteId, call, typeName, match)) continue;
|
|
3250
3670
|
const key = match.bindingId || `${typeName}.${call.name}`;
|
|
3251
3671
|
const existing = callees.get(key);
|
|
3252
3672
|
if (existing) {
|
|
@@ -3285,8 +3705,31 @@ function findCallees(index, def, options = {}) {
|
|
|
3285
3705
|
}
|
|
3286
3706
|
continue;
|
|
3287
3707
|
}
|
|
3708
|
+
if (qualifiedType?.kind === 'project') {
|
|
3709
|
+
// The import pins this receiver to a concrete project
|
|
3710
|
+
// package, but no exact member body is indexed there.
|
|
3711
|
+
// Promotion/generation may explain the gap; never fall
|
|
3712
|
+
// through to an unrelated global same-name method.
|
|
3713
|
+
noteUnverified(siteId, call, 'uncertain-receiver', {
|
|
3714
|
+
dispatchVia: qualifiedType.via,
|
|
3715
|
+
});
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
// An unindexed Go receiver type may be an interface from
|
|
3719
|
+
// another package. Go satisfaction is implicit, so a
|
|
3720
|
+
// same-name project method is a possible runtime target,
|
|
3721
|
+
// never a sound bare-name confirmation or exclusion.
|
|
3722
|
+
if (collectAccount && language === 'go' &&
|
|
3723
|
+
_externalGoDispatchType(index, typeName)) {
|
|
3724
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3725
|
+
dispatchVia: typeName,
|
|
3726
|
+
dispatchCandidates: _countDispatchCandidates(
|
|
3727
|
+
index, typeName, symbols || []),
|
|
3728
|
+
});
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3288
3731
|
// No match found with inferred type — fall through to include as unresolved
|
|
3289
|
-
} else if (call.receiverCall && !call.receiver && !call.isConstructor &&
|
|
3732
|
+
} else if (call.receiverCall && (!call.receiver || call.receiverIsChainRoot) && !call.isConstructor &&
|
|
3290
3733
|
langTraits(language)?.typeSystem === 'nominal') {
|
|
3291
3734
|
// Chained receiver, nominal callee direction (fix #268,
|
|
3292
3735
|
// chi-measured): `m.NotFound().ServeHTTP(w, r)` /
|
|
@@ -3320,6 +3763,7 @@ function findCallees(index, def, options = {}) {
|
|
|
3320
3763
|
}
|
|
3321
3764
|
if (sel.match) {
|
|
3322
3765
|
const match = sel.match;
|
|
3766
|
+
if (routeVirtualOverride(siteId, call, chained.type, match)) continue;
|
|
3323
3767
|
const key = match.bindingId || `${chained.type}.${call.name}`;
|
|
3324
3768
|
const existing = callees.get(key);
|
|
3325
3769
|
if (existing) {
|
|
@@ -3394,6 +3838,33 @@ function findCallees(index, def, options = {}) {
|
|
|
3394
3838
|
noteSite(siteId, 'filtered', 'method-calls-excluded', call);
|
|
3395
3839
|
continue;
|
|
3396
3840
|
}
|
|
3841
|
+
|
|
3842
|
+
// Contract surface: an untyped nominal receiver with several
|
|
3843
|
+
// project owner types has no exact callee identity. File or
|
|
3844
|
+
// package bindings name method declarations, not the runtime
|
|
3845
|
+
// type of `h` in `h.ServeHTTP(...)`; letting the generic
|
|
3846
|
+
// lexical pass choose one fabricates a concrete edge.
|
|
3847
|
+
if (collectAccount && call.receiver && call.receiverExternalFlow) {
|
|
3848
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
3849
|
+
dispatchVia: call.receiverExternalFlow,
|
|
3850
|
+
externalContract: true,
|
|
3851
|
+
});
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
if (collectAccount && call.receiver && !call.receiverType &&
|
|
3855
|
+
!call.receiverCall && !call.isPotentialCallback &&
|
|
3856
|
+
!['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver)) {
|
|
3857
|
+
const owners = new Set((index.symbols.get(call.name) || [])
|
|
3858
|
+
.filter(s => !NON_CALLABLE_TYPES.has(s.type))
|
|
3859
|
+
.map(s => s.className || (s.receiver && s.receiver.replace(/^\*/, '')))
|
|
3860
|
+
.filter(Boolean));
|
|
3861
|
+
if (owners.size > 1) {
|
|
3862
|
+
noteUnverified(siteId, call, 'method-ambiguous', {
|
|
3863
|
+
dispatchCandidates: owners.size,
|
|
3864
|
+
});
|
|
3865
|
+
continue;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3397
3868
|
}
|
|
3398
3869
|
|
|
3399
3870
|
// Skip keywords and built-ins — EXCEPT self/super-received method
|
|
@@ -3404,7 +3875,14 @@ function findCallees(index, def, options = {}) {
|
|
|
3404
3875
|
const selfShaped = call.isMethod &&
|
|
3405
3876
|
(['self', 'cls', 'this', 'super'].includes(call.receiver) ||
|
|
3406
3877
|
(call.receiver === 'Self' && language === 'rust'));
|
|
3407
|
-
|
|
3878
|
+
// Builtin/global names are shadowable. `Request` is a web global,
|
|
3879
|
+
// but `const Request = require('./request')` owns `new Request()`
|
|
3880
|
+
// in that module. Resolve explicit lexical/import bindings before
|
|
3881
|
+
// classifying a same-named builtin as external.
|
|
3882
|
+
const shadowsBuiltin = !call.receiver && (
|
|
3883
|
+
fileEntry?.importBindings?.some(b => b.name === call.name) ||
|
|
3884
|
+
fileEntry?.bindings?.some(b => b.name === call.name));
|
|
3885
|
+
if (!selfShaped && !shadowsBuiltin && index.isKeyword(call.name, language)) {
|
|
3408
3886
|
noteSite(siteId, 'external', null, call);
|
|
3409
3887
|
continue;
|
|
3410
3888
|
}
|
|
@@ -3463,20 +3941,93 @@ function findCallees(index, def, options = {}) {
|
|
|
3463
3941
|
continue;
|
|
3464
3942
|
}
|
|
3465
3943
|
|
|
3944
|
+
// Structural module ownership in the callee direction (the
|
|
3945
|
+
// caller-side #209/#217 rule). `@click.group()` resolves through
|
|
3946
|
+
// the imported `click` module; a same-named local decorated
|
|
3947
|
+
// function named `group` is receiver-blind and must never steal
|
|
3948
|
+
// the callee. Follow the module's name-level re-export chain and
|
|
3949
|
+
// add only definitions it actually exposes. Unknown CJS/dynamic
|
|
3950
|
+
// surfaces stay visible; external modules are external.
|
|
3951
|
+
if (call.isMethod && call.receiverIsModule &&
|
|
3952
|
+
langTraits(language)?.typeSystem === 'structural') {
|
|
3953
|
+
const moduleRoute = _calleeStructuralModuleRoute(index, fileEntry, call, language);
|
|
3954
|
+
if (moduleRoute.matches?.length) {
|
|
3955
|
+
for (const match of moduleRoute.matches) {
|
|
3956
|
+
const key = match.bindingId || `${match.file}:${match.startLine}:${call.name}`;
|
|
3957
|
+
const existing = callees.get(key);
|
|
3958
|
+
if (existing) {
|
|
3959
|
+
existing.count += 1;
|
|
3960
|
+
if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
|
|
3961
|
+
} else {
|
|
3962
|
+
callees.set(key, { name: effectiveName, bindingId: match.bindingId, count: 1,
|
|
3963
|
+
...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
continue;
|
|
3967
|
+
}
|
|
3968
|
+
if (moduleRoute.external) {
|
|
3969
|
+
noteSite(siteId, 'external', null, call);
|
|
3970
|
+
} else {
|
|
3971
|
+
noteUnverified(siteId, call, 'no-import-link');
|
|
3972
|
+
}
|
|
3973
|
+
continue;
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
// Bare imported-name ownership: `import { process } from './x';
|
|
3977
|
+
// process()` (or Python from-import) denotes the imported export,
|
|
3978
|
+
// not a same-named class method in the caller file. Resolve the
|
|
3979
|
+
// name through the same #217 export-chain discipline used for
|
|
3980
|
+
// module receivers. Only intercept when an explicit binding of
|
|
3981
|
+
// this name exists; ordinary locals continue to lexical binding.
|
|
3982
|
+
if (!call.isMethod && !call.receiver &&
|
|
3983
|
+
langTraits(language)?.typeSystem === 'structural') {
|
|
3984
|
+
const importRoute = _calleeStructuralImportedNameRoute(index, fileEntry, call, language);
|
|
3985
|
+
if (importRoute) {
|
|
3986
|
+
if (importRoute.matches?.length) {
|
|
3987
|
+
for (const match of importRoute.matches) {
|
|
3988
|
+
const key = match.bindingId || `${match.file}:${match.startLine}:${call.name}`;
|
|
3989
|
+
const existing = callees.get(key);
|
|
3990
|
+
if (existing) {
|
|
3991
|
+
existing.count += 1;
|
|
3992
|
+
if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
|
|
3993
|
+
} else {
|
|
3994
|
+
callees.set(key, { name: effectiveName, bindingId: match.bindingId, count: 1,
|
|
3995
|
+
...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
} else if (importRoute.external) {
|
|
3999
|
+
noteSite(siteId, 'external', null, call);
|
|
4000
|
+
} else {
|
|
4001
|
+
noteUnverified(siteId, call, 'ambiguous-binding');
|
|
4002
|
+
}
|
|
4003
|
+
continue;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
|
|
3466
4007
|
// Resolve binding within this file (without mutating cached call objects)
|
|
3467
4008
|
let calleeKey = call.bindingId || effectiveName;
|
|
3468
4009
|
let bindingResolved = call.bindingId;
|
|
3469
4010
|
let isUncertain = call.uncertain;
|
|
3470
4011
|
let uncertainReason = null; // account-mode reason for the unverified bucket
|
|
3471
4012
|
if (!call.bindingId && fileEntry?.bindings) {
|
|
3472
|
-
|
|
4013
|
+
// A method terminal resolves through its receiver in every
|
|
4014
|
+
// supported language, not through a bare file/package binding.
|
|
4015
|
+
// This includes deep/chained structural receivers and nominal
|
|
4016
|
+
// calls such as Go `v.ReadConfig()` beside a package function
|
|
4017
|
+
// also named ReadConfig. Otherwise the bare wrapper steals the
|
|
4018
|
+
// exact callee before receiver evidence is considered.
|
|
4019
|
+
const receiverBlindMethodBinding = call.isMethod &&
|
|
4020
|
+
!['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver);
|
|
4021
|
+
let bindings = receiverBlindMethodBinding ? [] :
|
|
4022
|
+
fileEntry.bindings.filter(b => b.name === call.name);
|
|
3473
4023
|
// For Go, also check sibling files in same directory (same
|
|
3474
4024
|
// package scope). dirToFiles is rebuilt on every build and
|
|
3475
4025
|
// cache load in canonical path order — the same iteration
|
|
3476
4026
|
// order as the full index.files scan it replaces (this loop
|
|
3477
4027
|
// runs per zero-binding call record, so the full scan was
|
|
3478
4028
|
// quadratic on large repos — 1037-file grpc-go measured).
|
|
3479
|
-
if (bindings.length === 0 &&
|
|
4029
|
+
if (bindings.length === 0 && !receiverBlindMethodBinding &&
|
|
4030
|
+
langTraits(language)?.packageScope === 'directory') {
|
|
3480
4031
|
const dir = path.dirname(def.file);
|
|
3481
4032
|
for (const fp of index.dirToFiles?.get(dir) || []) {
|
|
3482
4033
|
if (fp === def.file) continue;
|
|
@@ -3501,6 +4052,8 @@ function findCallees(index, def, options = {}) {
|
|
|
3501
4052
|
noteSite(siteId, 'external', null, call);
|
|
3502
4053
|
continue;
|
|
3503
4054
|
} else if (route?.resolve) {
|
|
4055
|
+
const rawType = call.receiverType || localTypes?.get(call.receiver);
|
|
4056
|
+
if (routeVirtualOverride(siteId, call, rawType, route.resolve)) continue;
|
|
3504
4057
|
bindingResolved = route.resolve.bindingId;
|
|
3505
4058
|
calleeKey = bindingResolved ||
|
|
3506
4059
|
`${route.resolve.className}.${effectiveName}`;
|
|
@@ -3508,20 +4061,25 @@ function findCallees(index, def, options = {}) {
|
|
|
3508
4061
|
isUncertain = true;
|
|
3509
4062
|
uncertainReason = 'uncertain-receiver';
|
|
3510
4063
|
} else {
|
|
3511
|
-
// JS/TS/Python:
|
|
3512
|
-
//
|
|
3513
|
-
//
|
|
3514
|
-
//
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
4064
|
+
// JS/TS/Python: lexical existence of `receiver` proves
|
|
4065
|
+
// only that the variable is in scope, not which class
|
|
4066
|
+
// owns its terminal method. Typed/local-constructor
|
|
4067
|
+
// evidence was already handled by the route above;
|
|
4068
|
+
// module ownership was handled before binding lookup.
|
|
4069
|
+
// Everything left is uncertain. The single-owner rule
|
|
4070
|
+
// below may still promote names with exactly one
|
|
4071
|
+
// project owner, but a multi-owner call such as
|
|
4072
|
+
// `catchall._parse()` must never bind defs[0].
|
|
4073
|
+
const defs = index.symbols.get(call.name) || [];
|
|
4074
|
+
const owners = new Set(defs.filter(d => !NON_CALLABLE_TYPES.has(d.type))
|
|
4075
|
+
.map(d => d.className || (d.receiver && d.receiver.replace(/^\*/, '')))
|
|
4076
|
+
.filter(Boolean));
|
|
4077
|
+
if (owners.size > 1) {
|
|
3518
4078
|
isUncertain = true;
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
isUncertain = true;
|
|
3522
|
-
} else if (receiverBinding.type === 'function') {
|
|
3523
|
-
// Functions don't have user-defined methods (return value is unknown)
|
|
4079
|
+
uncertainReason = 'method-ambiguous';
|
|
4080
|
+
} else {
|
|
3524
4081
|
isUncertain = true;
|
|
4082
|
+
uncertainReason = 'uncertain-receiver';
|
|
3525
4083
|
}
|
|
3526
4084
|
}
|
|
3527
4085
|
} else {
|
|
@@ -3553,6 +4111,58 @@ function findCallees(index, def, options = {}) {
|
|
|
3553
4111
|
}
|
|
3554
4112
|
}
|
|
3555
4113
|
}
|
|
4114
|
+
if (bindings.length === 0 && !call.isMethod && !call.isConstructor && def.className &&
|
|
4115
|
+
langTraits(language)?.bareCallReachesMethods) {
|
|
4116
|
+
// A Java bare call in an instance method is implicit-this
|
|
4117
|
+
// even when the defining member is inherited and therefore
|
|
4118
|
+
// absent from this file's lexical bindings. Resolve class
|
|
4119
|
+
// then ancestors; if overrides exist, retain the dispatch
|
|
4120
|
+
// set visibly instead of falling through to defs[0].
|
|
4121
|
+
const callSymbols = index.symbols.get(call.name) || [];
|
|
4122
|
+
const onClass = (cls) => callSymbols.filter(s =>
|
|
4123
|
+
s.className === cls && !NON_CALLABLE_TYPES.has(s.type));
|
|
4124
|
+
let selected = _calleeOverloadSelect(
|
|
4125
|
+
index, call, onClass(def.className), language);
|
|
4126
|
+
if (!selected.match && !selected.ambiguous) {
|
|
4127
|
+
const parents = index._getInheritanceParents?.(def.className, def.file);
|
|
4128
|
+
if (parents) {
|
|
4129
|
+
for (const parentName of parents) {
|
|
4130
|
+
selected = _calleeOverloadSelect(
|
|
4131
|
+
index, call, onClass(parentName), language);
|
|
4132
|
+
if (selected.match || selected.ambiguous) break;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
if (selected.ambiguous) {
|
|
4137
|
+
noteUnverified(siteId, call, 'overload-ambiguous');
|
|
4138
|
+
continue;
|
|
4139
|
+
}
|
|
4140
|
+
if (selected.match) {
|
|
4141
|
+
if (routeVirtualOverride(siteId, call, def.className, selected.match)) continue;
|
|
4142
|
+
const key = selected.match.bindingId ||
|
|
4143
|
+
`${selected.match.className}.${call.name}`;
|
|
4144
|
+
const existing = callees.get(key);
|
|
4145
|
+
if (existing) {
|
|
4146
|
+
existing.count += 1;
|
|
4147
|
+
if (collectAccount) {
|
|
4148
|
+
existing.sites.push(call.line);
|
|
4149
|
+
existing.siteIds.push(siteId);
|
|
4150
|
+
}
|
|
4151
|
+
} else {
|
|
4152
|
+
callees.set(key, {
|
|
4153
|
+
name: call.name,
|
|
4154
|
+
bindingId: selected.match.bindingId,
|
|
4155
|
+
count: 1,
|
|
4156
|
+
...(collectAccount && { sites: [call.line], siteIds: [siteId] }),
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
continue;
|
|
4160
|
+
}
|
|
4161
|
+
if (collectAccount) {
|
|
4162
|
+
noteUnverified(siteId, call, 'binding-ambiguous');
|
|
4163
|
+
continue;
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
3556
4166
|
if (bindings.length === 1) {
|
|
3557
4167
|
// For method calls with a receiver, verify the receiver plausibly
|
|
3558
4168
|
// matches the binding's class. Prevents plt.close() → ReportGenerator.close()
|
|
@@ -3573,7 +4183,56 @@ function findCallees(index, def, options = {}) {
|
|
|
3573
4183
|
bindingResolved = bindings[0].id;
|
|
3574
4184
|
calleeKey = bindingResolved;
|
|
3575
4185
|
} else if (bindings.length > 1) {
|
|
3576
|
-
if (call.name === def.name
|
|
4186
|
+
if (call.name === def.name && !call.isMethod &&
|
|
4187
|
+
langTraits(language)?.typeSystem === 'structural') {
|
|
4188
|
+
// Lexical recursion in JS/TS/Python binds the
|
|
4189
|
+
// enclosing definition, even when a sibling callback
|
|
4190
|
+
// or test case declares another same-named function.
|
|
4191
|
+
// Selecting "all other bindings" inverted the edge:
|
|
4192
|
+
// Fastify's first nested createNestedRoutes called the
|
|
4193
|
+
// later test's copy instead of itself.
|
|
4194
|
+
const selfBinding = bindings.find(b =>
|
|
4195
|
+
b.id === def.bindingId || b.startLine === def.startLine);
|
|
4196
|
+
if (selfBinding) {
|
|
4197
|
+
bindingResolved = selfBinding.id || def.bindingId;
|
|
4198
|
+
calleeKey = bindingResolved || `${def.file}:${def.startLine}:${def.name}`;
|
|
4199
|
+
} else {
|
|
4200
|
+
isUncertain = true;
|
|
4201
|
+
uncertainReason = 'binding-ambiguous';
|
|
4202
|
+
}
|
|
4203
|
+
} else if (call.name === def.name && !call.isMethod &&
|
|
4204
|
+
def.className && langTraits(language)?.bareCallReachesMethods) {
|
|
4205
|
+
// Java bare calls are implicit-this calls. Even when
|
|
4206
|
+
// the terminal name equals the enclosing overload,
|
|
4207
|
+
// argument shape selects one member of the same-class
|
|
4208
|
+
// family; spraying every overload invents edges and
|
|
4209
|
+
// hides the exact dependency an agent needs.
|
|
4210
|
+
const sameClassSyms = (index.symbols.get(call.name) || [])
|
|
4211
|
+
.filter(s => s.className === def.className &&
|
|
4212
|
+
!NON_CALLABLE_TYPES.has(s.type));
|
|
4213
|
+
const selected = _calleeOverloadSelect(
|
|
4214
|
+
index, call, sameClassSyms, language);
|
|
4215
|
+
if (selected.ambiguous) {
|
|
4216
|
+
isUncertain = true;
|
|
4217
|
+
uncertainReason = 'overload-ambiguous';
|
|
4218
|
+
} else if (selected.match) {
|
|
4219
|
+
if (routeVirtualOverride(siteId, call, def.className, selected.match)) continue;
|
|
4220
|
+
if (selected.match.bindingId === def.bindingId ||
|
|
4221
|
+
selected.match.startLine === def.startLine) {
|
|
4222
|
+
noteSite(siteId, 'excluded', 'self-recursion', call);
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
const matchingBinding = bindings.find(b =>
|
|
4226
|
+
b.id === selected.match.bindingId ||
|
|
4227
|
+
b.startLine === selected.match.startLine);
|
|
4228
|
+
bindingResolved = matchingBinding?.id || selected.match.bindingId;
|
|
4229
|
+
calleeKey = bindingResolved ||
|
|
4230
|
+
`${def.className}.${call.name}:${selected.match.startLine}`;
|
|
4231
|
+
} else {
|
|
4232
|
+
isUncertain = true;
|
|
4233
|
+
uncertainReason = 'binding-ambiguous';
|
|
4234
|
+
}
|
|
4235
|
+
} else if (call.name === def.name) {
|
|
3577
4236
|
// Calling same-name function (e.g., Java overloads)
|
|
3578
4237
|
// Add ALL other overloads as potential callees.
|
|
3579
4238
|
// A RECEIVER-QUALIFIED same-name call names its type
|
|
@@ -3642,8 +4301,31 @@ function findCallees(index, def, options = {}) {
|
|
|
3642
4301
|
// Try to resolve to a binding in the same class via symbol lookup
|
|
3643
4302
|
const callSymbols = index.symbols.get(call.name);
|
|
3644
4303
|
if (callSymbols) {
|
|
3645
|
-
const
|
|
3646
|
-
|
|
4304
|
+
const onClass = (cls) => callSymbols.filter(s => s.className === cls);
|
|
4305
|
+
let selected = _calleeOverloadSelect(
|
|
4306
|
+
index, call, onClass(def.className), language);
|
|
4307
|
+
// Java implicit-this lookup includes inherited
|
|
4308
|
+
// members. Resolve the statically selected base
|
|
4309
|
+
// declaration, then let routeVirtualOverride keep
|
|
4310
|
+
// descendant implementations visible rather than
|
|
4311
|
+
// choosing an arbitrary sibling override.
|
|
4312
|
+
if (!selected.match && !selected.ambiguous &&
|
|
4313
|
+
langTraits(language)?.bareCallReachesMethods) {
|
|
4314
|
+
const parents = index._getInheritanceParents?.(def.className, def.file);
|
|
4315
|
+
if (parents) {
|
|
4316
|
+
for (const parentName of parents) {
|
|
4317
|
+
selected = _calleeOverloadSelect(
|
|
4318
|
+
index, call, onClass(parentName), language);
|
|
4319
|
+
if (selected.match || selected.ambiguous) break;
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
if (selected.ambiguous) {
|
|
4324
|
+
isUncertain = true;
|
|
4325
|
+
uncertainReason = 'overload-ambiguous';
|
|
4326
|
+
} else if (selected.match) {
|
|
4327
|
+
const sameClassSym = selected.match;
|
|
4328
|
+
if (routeVirtualOverride(siteId, call, def.className, sameClassSym)) continue;
|
|
3647
4329
|
// Find the binding that matches this symbol's line
|
|
3648
4330
|
const matchingBinding = bindings.find(b => b.startLine === sameClassSym.startLine);
|
|
3649
4331
|
if (matchingBinding) {
|
|
@@ -3678,6 +4360,16 @@ function findCallees(index, def, options = {}) {
|
|
|
3678
4360
|
}
|
|
3679
4361
|
}
|
|
3680
4362
|
|
|
4363
|
+
// Bare nominal calls inside a class are implicit-this calls. A
|
|
4364
|
+
// lexical binding to a base/interface declaration is not an exact
|
|
4365
|
+
// runtime target when overrides/implementations exist.
|
|
4366
|
+
if (bindingResolved && !call.isMethod && !call.receiver &&
|
|
4367
|
+
langTraits(language)?.bareCallReachesMethods) {
|
|
4368
|
+
const bound = (index.symbols.get(effectiveName) || [])
|
|
4369
|
+
.find(s => s.bindingId === bindingResolved);
|
|
4370
|
+
if (bound && routeVirtualOverride(siteId, call, def.className, bound)) continue;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
3681
4373
|
// Single project-wide owner (fix #236 — the caller side's
|
|
3682
4374
|
// #204/#209 rule on the callee side): an untyped-receiver method
|
|
3683
4375
|
// call whose name has exactly ONE owner type resolves to that
|
|
@@ -3844,6 +4536,24 @@ function findCallees(index, def, options = {}) {
|
|
|
3844
4536
|
|
|
3845
4537
|
if (!match) { noteUnverified(siteId, call, 'inherited-unresolved'); continue; }
|
|
3846
4538
|
|
|
4539
|
+
// Virtual self/this dispatch (callee-side parity with the
|
|
4540
|
+
// caller side's strict-ancestor rule): a call lexically in a
|
|
4541
|
+
// base class can execute a descendant override. Confirming
|
|
4542
|
+
// only the base declaration is therefore false precision.
|
|
4543
|
+
// `super.method()` is statically pinned to the parent and is
|
|
4544
|
+
// exempt. In contract mode retain the site as possible
|
|
4545
|
+
// dispatch; legacy command behavior remains unchanged.
|
|
4546
|
+
const overrideOwners = call.receiver !== 'super'
|
|
4547
|
+
? _descendantOverrideOwners(index, def.className, call.name)
|
|
4548
|
+
: new Set();
|
|
4549
|
+
if (collectAccount && overrideOwners.size > 0) {
|
|
4550
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
4551
|
+
dispatchVia: def.className,
|
|
4552
|
+
dispatchCandidates: overrideOwners.size + 1,
|
|
4553
|
+
});
|
|
4554
|
+
continue;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
3847
4557
|
const key = match.bindingId || `${match.className}.${call.name}`;
|
|
3848
4558
|
const existing = callees.get(key);
|
|
3849
4559
|
if (existing) {
|
|
@@ -3988,8 +4698,10 @@ function findCallees(index, def, options = {}) {
|
|
|
3988
4698
|
if (!bindingId && NON_CALLABLE_TYPES.has(callee.type)) {
|
|
3989
4699
|
const isFuncField = callee.type === 'field' && callee.fieldType &&
|
|
3990
4700
|
/^func\b/.test(callee.fieldType);
|
|
4701
|
+
const isCallableClass = callee.type === 'class' &&
|
|
4702
|
+
langTraits(language)?.classesCallableWithoutNew;
|
|
3991
4703
|
// Constructor calls (new Foo()) are always callable regardless of type
|
|
3992
|
-
if (!isFuncField && !isConstructor) {
|
|
4704
|
+
if (!isFuncField && !isConstructor && !isCallableClass) {
|
|
3993
4705
|
claimSites('excluded', 'non-callable-shadow');
|
|
3994
4706
|
continue;
|
|
3995
4707
|
}
|
|
@@ -4017,6 +4729,8 @@ function findCallees(index, def, options = {}) {
|
|
|
4017
4729
|
callCount: count,
|
|
4018
4730
|
weight: index.calculateWeight(count),
|
|
4019
4731
|
confidence: calleeScored.confidence,
|
|
4732
|
+
evidenceScore: calleeScored.evidenceScore,
|
|
4733
|
+
scoreKind: calleeScored.scoreKind,
|
|
4020
4734
|
resolution: calleeScored.resolution,
|
|
4021
4735
|
...(collectAccount && {
|
|
4022
4736
|
tier: TIER.CONFIRMED,
|
|
@@ -4050,9 +4764,12 @@ function findCallees(index, def, options = {}) {
|
|
|
4050
4764
|
|
|
4051
4765
|
return result;
|
|
4052
4766
|
} catch (e) {
|
|
4053
|
-
//
|
|
4054
|
-
//
|
|
4055
|
-
|
|
4767
|
+
// Empty callees is a semantic assertion, not an error fallback. Any
|
|
4768
|
+
// read/cache/parser/engine failure here would otherwise turn a broken
|
|
4769
|
+
// trace-down query into a believable false zero.
|
|
4770
|
+
const rel = def?.file ? path.relative(index.root, def.file) : '<unknown>';
|
|
4771
|
+
e.message = `findCallees failed while analyzing ${rel}: ${e.message}`;
|
|
4772
|
+
throw e;
|
|
4056
4773
|
}
|
|
4057
4774
|
} finally { index._endOp(); }
|
|
4058
4775
|
}
|
|
@@ -4221,14 +4938,81 @@ function _typeNameFromReturnAnnotation(text) {
|
|
|
4221
4938
|
* consuming call happens to be.
|
|
4222
4939
|
*/
|
|
4223
4940
|
function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
4941
|
+
// File-scoped derivation: the result is independent of the queried
|
|
4942
|
+
// definition. A command operation can therefore share it across every
|
|
4943
|
+
// findCallers/findCallees request in that file. Cache null explicitly via
|
|
4944
|
+
// Map.has() so files without resolvable flow are cheap too.
|
|
4945
|
+
const opCache = index._opReturnTypeFlowCache;
|
|
4946
|
+
if (opCache?.has(filePath)) return opCache.get(filePath);
|
|
4224
4947
|
const fileEntry = index.files.get(filePath);
|
|
4225
4948
|
const language = fileEntry?.language;
|
|
4226
4949
|
const nominal = langTraits(language)?.typeSystem === 'nominal';
|
|
4227
4950
|
let map = null;
|
|
4951
|
+
const assignedFoldCtx = {
|
|
4952
|
+
memo: new Map(),
|
|
4953
|
+
visiting: new Set(),
|
|
4954
|
+
records: calls,
|
|
4955
|
+
getFlowMap: () => map,
|
|
4956
|
+
};
|
|
4957
|
+
// A flow map is nearest-assignment semantics, not "last assignment whose
|
|
4958
|
+
// type UCN happened to understand". Record an explicit tombstone when a
|
|
4959
|
+
// call rebinds a variable but its result type cannot be resolved. Without
|
|
4960
|
+
// this, `let m = make_result(); let m = m.unwrap(); m.method()` retained
|
|
4961
|
+
// the stale Result-like type and excluded compiler-true calls on the
|
|
4962
|
+
// unwrapped value (clap-measured).
|
|
4963
|
+
const invalidateAssignment = (call) => {
|
|
4964
|
+
const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
4965
|
+
const names = [call.assignedTo, ...(call.assignedTupleRest || [])].filter(Boolean);
|
|
4966
|
+
if (names.length === 0) return;
|
|
4967
|
+
if (!map) map = new Map();
|
|
4968
|
+
for (const name of names) {
|
|
4969
|
+
const key = `${scope}:${name}`;
|
|
4970
|
+
if (!map.has(key)) map.set(key, []);
|
|
4971
|
+
map.get(key).push({ line: call.line, invalidated: true });
|
|
4972
|
+
}
|
|
4973
|
+
};
|
|
4974
|
+
const routeUnknownAssignment = (call, externalVia) => {
|
|
4975
|
+
const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
4976
|
+
const names = [call.assignedTo, ...(call.assignedTupleRest || [])].filter(Boolean);
|
|
4977
|
+
if (names.length === 0) return;
|
|
4978
|
+
if (!map) map = new Map();
|
|
4979
|
+
for (const name of names) {
|
|
4980
|
+
const key = `${scope}:${name}`;
|
|
4981
|
+
if (!map.has(key)) map.set(key, []);
|
|
4982
|
+
map.get(key).push({ line: call.line, externalVia });
|
|
4983
|
+
}
|
|
4984
|
+
};
|
|
4228
4985
|
for (const call of calls) {
|
|
4229
4986
|
if (!call.assignedTo) continue;
|
|
4230
|
-
|
|
4231
|
-
|
|
4987
|
+
|
|
4988
|
+
// Assigned builder chain (chi-measured):
|
|
4989
|
+
// `hr := RouteHeaders().Route(...)` stores the OUTERMOST method call
|
|
4990
|
+
// as the assignment producer. Fold the chain to its declared result
|
|
4991
|
+
// before composing later `hr.Handler(...)` flow.
|
|
4992
|
+
if (nominal && call.receiverCall) {
|
|
4993
|
+
const folded = _typeOfCallResultFold(
|
|
4994
|
+
index, fileEntry, filePath, call, assignedFoldCtx);
|
|
4995
|
+
if (folded?.type || folded?.externalVia) {
|
|
4996
|
+
const scope = call.enclosingFunction
|
|
4997
|
+
? `${call.enclosingFunction.startLine}` : '';
|
|
4998
|
+
const key = `${scope}:${call.assignedTo}`;
|
|
4999
|
+
if (!map) map = new Map();
|
|
5000
|
+
if (!map.has(key)) map.set(key, []);
|
|
5001
|
+
map.get(key).push({ line: call.line,
|
|
5002
|
+
...(folded.type && { type: folded.type }),
|
|
5003
|
+
...(folded.fromFile && { fromFile: folded.fromFile }),
|
|
5004
|
+
...(folded.externalVia && { externalVia: folded.externalVia }) });
|
|
5005
|
+
continue;
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
let returnType, fromFile, selfClass, returnedFunctionResult;
|
|
5009
|
+
const callableFlow = !call.isMethod && !call.receiver
|
|
5010
|
+
? _lookupReturnTypeFlow(map, { ...call, receiver: call.name })
|
|
5011
|
+
: undefined;
|
|
5012
|
+
if (callableFlow?.returnedFunctionResult) {
|
|
5013
|
+
returnType = callableFlow.returnedFunctionResult;
|
|
5014
|
+
fromFile = callableFlow.fromFile;
|
|
5015
|
+
} else if (call.isMethod && call.receiverType) {
|
|
4232
5016
|
const defs = index.symbols.get(call.name) || [];
|
|
4233
5017
|
if (nominal) {
|
|
4234
5018
|
const matches = defs.filter(d => d.className === call.receiverType && d.returnType);
|
|
@@ -4241,6 +5025,37 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
4241
5025
|
const def = defs.find(d => d.className === call.receiverType && d.returnType);
|
|
4242
5026
|
returnType = def && def.returnType;
|
|
4243
5027
|
}
|
|
5028
|
+
} else if (call.isMethod && call.receiver &&
|
|
5029
|
+
!['self', 'this', 'cls'].includes(call.receiver) &&
|
|
5030
|
+
_lookupReturnTypeFlow(map, call)) {
|
|
5031
|
+
// One-hop local flow composition (fix #273, chi-measured):
|
|
5032
|
+
// `hr := RouteHeaders(); handler := hr.Handler(...)` first types
|
|
5033
|
+
// hr from RouteHeaders' annotation, then resolves Handler on that
|
|
5034
|
+
// exact owner to type handler. This stays order/scoped and uses
|
|
5035
|
+
// only compiler-declared returns; disagreement remains untyped.
|
|
5036
|
+
const receiverFlow = _lookupReturnTypeFlow(map, call);
|
|
5037
|
+
if (receiverFlow?.externalVia) {
|
|
5038
|
+
const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
5039
|
+
const key = `${scope}:${call.assignedTo}`;
|
|
5040
|
+
if (!map) map = new Map();
|
|
5041
|
+
if (!map.has(key)) map.set(key, []);
|
|
5042
|
+
map.get(key).push({
|
|
5043
|
+
line: call.line,
|
|
5044
|
+
externalVia: `${receiverFlow.externalVia}.${call.name}`,
|
|
5045
|
+
});
|
|
5046
|
+
continue;
|
|
5047
|
+
}
|
|
5048
|
+
if (receiverFlow?.type) {
|
|
5049
|
+
const matches = (index.symbols.get(call.name) || []).filter(d =>
|
|
5050
|
+
(d.className === receiverFlow.type ||
|
|
5051
|
+
(d.receiver && d.receiver.replace(/^\*/, '') === receiverFlow.type)) &&
|
|
5052
|
+
d.returnType);
|
|
5053
|
+
if (matches.length > 0 && new Set(matches.map(d => d.returnType)).size === 1) {
|
|
5054
|
+
returnType = matches[0].returnType;
|
|
5055
|
+
fromFile = matches[0].file;
|
|
5056
|
+
selfClass = receiverFlow.type;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
4244
5059
|
} else if (call.isMethod && ['self', 'this', 'cls'].includes(call.receiver)) {
|
|
4245
5060
|
const enclosing = index.findEnclosingFunction(filePath, call.line, true);
|
|
4246
5061
|
let cls = enclosing && enclosing.className;
|
|
@@ -4350,6 +5165,7 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
4350
5165
|
}
|
|
4351
5166
|
if (matches.length > 0 && new Set(matches.map(d => d.returnType)).size === 1) {
|
|
4352
5167
|
returnType = matches[0].returnType;
|
|
5168
|
+
if (new Set(matches.map(d => d.file)).size === 1) fromFile = matches[0].file;
|
|
4353
5169
|
}
|
|
4354
5170
|
}
|
|
4355
5171
|
} else if (!call.isMethod && !call.receiver) {
|
|
@@ -4368,23 +5184,91 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
4368
5184
|
const sameFile = defs.filter(d => d.file === filePath);
|
|
4369
5185
|
if (sameFile.length === 1) chosen = sameFile[0];
|
|
4370
5186
|
}
|
|
4371
|
-
if (chosen) {
|
|
5187
|
+
if (chosen) {
|
|
5188
|
+
returnType = chosen.returnType;
|
|
5189
|
+
returnedFunctionResult = chosen.returnedFunctionResult;
|
|
5190
|
+
fromFile = chosen.file;
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
if (!returnType) {
|
|
5194
|
+
invalidateAssignment(call);
|
|
5195
|
+
continue;
|
|
5196
|
+
}
|
|
5197
|
+
if (language === 'go' && returnedFunctionResult) {
|
|
5198
|
+
const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
5199
|
+
const key = `${scope}:${call.assignedTo}`;
|
|
5200
|
+
if (!map) map = new Map();
|
|
5201
|
+
if (!map.has(key)) map.set(key, []);
|
|
5202
|
+
map.get(key).push({
|
|
5203
|
+
line: call.line,
|
|
5204
|
+
returnedFunctionResult,
|
|
5205
|
+
...(fromFile && { fromFile }),
|
|
5206
|
+
});
|
|
5207
|
+
continue;
|
|
4372
5208
|
}
|
|
4373
|
-
if (!returnType) continue;
|
|
4374
5209
|
let typeName, entryFromFile;
|
|
4375
5210
|
if (nominal) {
|
|
4376
5211
|
const parsed = _returnTypeNameNominal(returnType, language, {
|
|
4377
5212
|
unwrapped: call.assignedUnwrap, tuple: call.assignedTuple, selfClass,
|
|
4378
5213
|
});
|
|
4379
|
-
if (!parsed)
|
|
5214
|
+
if (!parsed) {
|
|
5215
|
+
const uncertainVia = _rejectedNominalFlowVia(returnType, language, {
|
|
5216
|
+
tuple: call.assignedTuple,
|
|
5217
|
+
});
|
|
5218
|
+
if (uncertainVia) routeUnknownAssignment(call, uncertainVia);
|
|
5219
|
+
else invalidateAssignment(call);
|
|
5220
|
+
continue;
|
|
5221
|
+
}
|
|
5222
|
+
// Qualifier provenance is meaningful even when no same-named
|
|
5223
|
+
// project TYPE symbol exists. `http.Handler` is externally owned
|
|
5224
|
+
// regardless of whether the project happens to define a struct,
|
|
5225
|
+
// interface, or named function type called Handler.
|
|
5226
|
+
if (language === 'go' && parsed.qualifier) {
|
|
5227
|
+
const producerEntry = index.files.get(fromFile || filePath) || fileEntry;
|
|
5228
|
+
const qualified = _goQualifiedReceiverType(
|
|
5229
|
+
index, producerEntry, parsed.qualifier, parsed.name);
|
|
5230
|
+
if (qualified && qualified.kind !== 'project') {
|
|
5231
|
+
const scope = call.enclosingFunction
|
|
5232
|
+
? `${call.enclosingFunction.startLine}` : '';
|
|
5233
|
+
const key = `${scope}:${call.assignedTo}`;
|
|
5234
|
+
if (!map) map = new Map();
|
|
5235
|
+
if (!map.has(key)) map.set(key, []);
|
|
5236
|
+
map.get(key).push({ line: call.line, externalVia: qualified.via });
|
|
5237
|
+
continue;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
4380
5240
|
const origin = _resolveFlowTypeOrigin(index, fromFile || filePath, parsed.name, parsed.qualifier);
|
|
4381
|
-
if (!origin)
|
|
5241
|
+
if (!origin) {
|
|
5242
|
+
// A project producer can return a package-qualified external
|
|
5243
|
+
// Go type (`DefaultLogger(...) http.Handler`). That still
|
|
5244
|
+
// gives compiler-grade provenance: the assigned receiver is
|
|
5245
|
+
// externally decided and must defeat project-local bare-name
|
|
5246
|
+
// confirmation. Preserve it on the same visible uncertainty
|
|
5247
|
+
// rail as an external producer call.
|
|
5248
|
+
invalidateAssignment(call);
|
|
5249
|
+
continue; // identity unpinnable — don't type at all
|
|
5250
|
+
}
|
|
4382
5251
|
typeName = parsed.name;
|
|
4383
5252
|
entryFromFile = origin.fromFile;
|
|
4384
5253
|
} else {
|
|
4385
5254
|
typeName = _typeNameFromReturnAnnotation(returnType);
|
|
5255
|
+
if (typeName) {
|
|
5256
|
+
const typeDefs = (index.symbols.get(typeName) || [])
|
|
5257
|
+
.filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
5258
|
+
if (typeDefs.length > 0) {
|
|
5259
|
+
const origin = _resolveFlowTypeOrigin(index, fromFile || filePath, typeName);
|
|
5260
|
+
if (!origin) {
|
|
5261
|
+
invalidateAssignment(call);
|
|
5262
|
+
continue;
|
|
5263
|
+
}
|
|
5264
|
+
entryFromFile = origin.fromFile;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
if (!typeName) {
|
|
5269
|
+
invalidateAssignment(call);
|
|
5270
|
+
continue;
|
|
4386
5271
|
}
|
|
4387
|
-
if (!typeName) continue;
|
|
4388
5272
|
const scope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
4389
5273
|
const key = `${scope}:${call.assignedTo}`;
|
|
4390
5274
|
if (!map) map = new Map();
|
|
@@ -4392,11 +5276,80 @@ function _buildReturnTypeFlowMap(index, filePath, calls) {
|
|
|
4392
5276
|
map.get(key).push({ line: call.line, type: typeName,
|
|
4393
5277
|
...(entryFromFile && { fromFile: entryFromFile }) });
|
|
4394
5278
|
}
|
|
5279
|
+
if (opCache) opCache.set(filePath, map);
|
|
4395
5280
|
return map;
|
|
4396
5281
|
}
|
|
4397
5282
|
|
|
5283
|
+
/**
|
|
5284
|
+
* Return call records whose source line falls inside one definition.
|
|
5285
|
+
*
|
|
5286
|
+
* Parser output is normally source-ordered. We verify/order it once per file
|
|
5287
|
+
* per command and then use two binary searches, avoiding a full-file scan for
|
|
5288
|
+
* every symbol during reachability traversal.
|
|
5289
|
+
*/
|
|
5290
|
+
function _callsInDefinitionRange(index, filePath, calls, startLine, endLine) {
|
|
5291
|
+
let ordered = index._opCallsByLineCache?.get(filePath);
|
|
5292
|
+
if (!ordered) {
|
|
5293
|
+
let sorted = true;
|
|
5294
|
+
for (let i = 1; i < calls.length; i++) {
|
|
5295
|
+
if ((calls[i - 1].line || 0) > (calls[i].line || 0)) {
|
|
5296
|
+
sorted = false;
|
|
5297
|
+
break;
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
ordered = sorted ? calls : [...calls].sort((a, b) => (a.line || 0) - (b.line || 0));
|
|
5301
|
+
index._opCallsByLineCache?.set(filePath, ordered);
|
|
5302
|
+
}
|
|
5303
|
+
const lowerBound = (needle) => {
|
|
5304
|
+
let lo = 0, hi = ordered.length;
|
|
5305
|
+
while (lo < hi) {
|
|
5306
|
+
const mid = (lo + hi) >>> 1;
|
|
5307
|
+
if ((ordered[mid].line || 0) < needle) lo = mid + 1;
|
|
5308
|
+
else hi = mid;
|
|
5309
|
+
}
|
|
5310
|
+
return lo;
|
|
5311
|
+
};
|
|
5312
|
+
return ordered.slice(lowerBound(startLine), lowerBound(endLine + 1));
|
|
5313
|
+
}
|
|
5314
|
+
|
|
5315
|
+
/**
|
|
5316
|
+
* Return independently-addressable class method ranges nested inside one
|
|
5317
|
+
* definition. Reachability calls findCallees for thousands of symbols; a
|
|
5318
|
+
* full fileEntry.symbols scan per symbol made this step quadratic in files
|
|
5319
|
+
* with many methods. Cache one sorted range list per file, then binary-slice
|
|
5320
|
+
* the relevant start-line window for each definition.
|
|
5321
|
+
*/
|
|
5322
|
+
function _innerClassMethodRanges(index, def, fileEntry) {
|
|
5323
|
+
let ordered = index._opInnerSymbolRangesCache?.get(def.file);
|
|
5324
|
+
if (!ordered) {
|
|
5325
|
+
ordered = fileEntry.symbols
|
|
5326
|
+
.filter(symbol => !NON_CALLABLE_TYPES.has(symbol.type) && symbol.className)
|
|
5327
|
+
.map(symbol => [symbol.startLine, symbol.endLine])
|
|
5328
|
+
.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
5329
|
+
index._opInnerSymbolRangesCache?.set(def.file, ordered);
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
const lowerBound = (needle) => {
|
|
5333
|
+
let lo = 0, hi = ordered.length;
|
|
5334
|
+
while (lo < hi) {
|
|
5335
|
+
const mid = (lo + hi) >>> 1;
|
|
5336
|
+
if (ordered[mid][0] < needle) lo = mid + 1;
|
|
5337
|
+
else hi = mid;
|
|
5338
|
+
}
|
|
5339
|
+
return lo;
|
|
5340
|
+
};
|
|
5341
|
+
const from = lowerBound(def.startLine + 1);
|
|
5342
|
+
const to = lowerBound(def.endLine + 1);
|
|
5343
|
+
const ranges = [];
|
|
5344
|
+
for (let i = from; i < to; i++) {
|
|
5345
|
+
if (ordered[i][1] <= def.endLine) ranges.push(ordered[i]);
|
|
5346
|
+
}
|
|
5347
|
+
return ranges;
|
|
5348
|
+
}
|
|
5349
|
+
|
|
4398
5350
|
/** Nearest preceding flow assignment for this call's receiver (fn scope, then module). */
|
|
4399
5351
|
function _lookupReturnTypeFlow(map, call) {
|
|
5352
|
+
if (!map) return undefined;
|
|
4400
5353
|
const fnScope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
4401
5354
|
for (const scope of fnScope === '' ? [''] : [fnScope, '']) {
|
|
4402
5355
|
const entries = map.get(`${scope}:${call.receiver}`);
|
|
@@ -4405,7 +5358,7 @@ function _lookupReturnTypeFlow(map, call) {
|
|
|
4405
5358
|
for (const e of entries) {
|
|
4406
5359
|
if (e.line < call.line && (!best || e.line > best.line)) best = e;
|
|
4407
5360
|
}
|
|
4408
|
-
if (best) return best;
|
|
5361
|
+
if (best) return best.invalidated ? undefined : best;
|
|
4409
5362
|
}
|
|
4410
5363
|
return undefined;
|
|
4411
5364
|
}
|
|
@@ -4503,6 +5456,36 @@ function _returnTypeNameNominal(text, language, opts = {}) {
|
|
|
4503
5456
|
return { name: norm, qualifier };
|
|
4504
5457
|
}
|
|
4505
5458
|
|
|
5459
|
+
/**
|
|
5460
|
+
* Some declared nominal return types are deliberately rejected as concrete
|
|
5461
|
+
* receiver identity because they are dispatch contracts or type parameters.
|
|
5462
|
+
* They are still strong evidence AGAINST the single-owner shortcut: `error`
|
|
5463
|
+
* can call any implementation's Error(), Java Object can reach overrides,
|
|
5464
|
+
* and Rust T may be instantiated by any implementing type. Preserve that as
|
|
5465
|
+
* visible possible-dispatch provenance instead of collapsing to "untyped".
|
|
5466
|
+
*/
|
|
5467
|
+
function _rejectedNominalFlowVia(text, language, opts = {}) {
|
|
5468
|
+
if (!text || typeof text !== 'string') return null;
|
|
5469
|
+
let raw = text.trim();
|
|
5470
|
+
if (language === 'go' && raw.startsWith('(')) {
|
|
5471
|
+
if (!opts.tuple) return null;
|
|
5472
|
+
const first = raw.slice(1, -1).split(',')[0]?.trim();
|
|
5473
|
+
raw = first ? first.split(/\s+/).pop() : '';
|
|
5474
|
+
}
|
|
5475
|
+
const norm = _normalizeFieldTypeName(raw, language);
|
|
5476
|
+
if (!norm) return null;
|
|
5477
|
+
if (language === 'go' && _GO_FLOW_REJECT.has(norm)) {
|
|
5478
|
+
return `${norm} — builtin contract`;
|
|
5479
|
+
}
|
|
5480
|
+
if (language === 'java' && _JAVA_FLOW_REJECT.has(norm)) {
|
|
5481
|
+
return `${norm} — builtin contract`;
|
|
5482
|
+
}
|
|
5483
|
+
if (language === 'rust' && /^[A-Z][A-Z0-9]?$/.test(norm)) {
|
|
5484
|
+
return `${norm} — generic type`;
|
|
5485
|
+
}
|
|
5486
|
+
return null;
|
|
5487
|
+
}
|
|
5488
|
+
|
|
4506
5489
|
/**
|
|
4507
5490
|
* Pin a flow type name to its defining file from the PRODUCER's scope
|
|
4508
5491
|
* (fix #207 — the #206 identity lesson applied to annotations: `Builder` in
|
|
@@ -4518,28 +5501,35 @@ function _returnTypeNameNominal(text, language, opts = {}) {
|
|
|
4518
5501
|
* - no project type def at all: external name — safe, can't conflate
|
|
4519
5502
|
*/
|
|
4520
5503
|
function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
|
|
5504
|
+
const opCache = index._opFlowTypeOriginCache;
|
|
5505
|
+
const cacheKey = `${producerFile}\x00${typeName}\x00${qualifier || ''}`;
|
|
5506
|
+
if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
|
|
5507
|
+
const finish = (result) => {
|
|
5508
|
+
if (opCache) opCache.set(cacheKey, result);
|
|
5509
|
+
return result;
|
|
5510
|
+
};
|
|
4521
5511
|
const typeDefs = (index.symbols.get(typeName) || [])
|
|
4522
5512
|
.filter(d => IDENTITY_TYPE_KINDS.has(d.type) && d.file);
|
|
4523
|
-
if (typeDefs.length === 0) return { fromFile: producerFile };
|
|
4524
|
-
if (qualifier === '<unresolvable>') return null;
|
|
5513
|
+
if (typeDefs.length === 0) return finish({ fromFile: producerFile });
|
|
5514
|
+
if (qualifier === '<unresolvable>') return finish(null);
|
|
4525
5515
|
if (qualifier) {
|
|
4526
5516
|
const fe = index.files.get(producerFile);
|
|
4527
5517
|
const inPkg = fe && _qualifiedProducerDefs(index, fe, qualifier, typeDefs);
|
|
4528
5518
|
if (inPkg && inPkg.length > 0 && new Set(inPkg.map(d => d.file)).size === 1) {
|
|
4529
|
-
return { fromFile: inPkg[0].file };
|
|
5519
|
+
return finish({ fromFile: inPkg[0].file });
|
|
4530
5520
|
}
|
|
4531
|
-
return null;
|
|
5521
|
+
return finish(null);
|
|
4532
5522
|
}
|
|
4533
5523
|
const sameFile = typeDefs.find(d => d.file === producerFile);
|
|
4534
|
-
if (sameFile) return { fromFile: sameFile.file };
|
|
5524
|
+
if (sameFile) return finish({ fromFile: sameFile.file });
|
|
4535
5525
|
const dir = path.dirname(producerFile);
|
|
4536
5526
|
const sameDir = typeDefs.filter(d => path.dirname(d.file) === dir);
|
|
4537
|
-
if (sameDir.length === 1) return { fromFile: sameDir[0].file };
|
|
4538
|
-
if (sameDir.length > 1) return null;
|
|
5527
|
+
if (sameDir.length === 1) return finish({ fromFile: sameDir[0].file });
|
|
5528
|
+
if (sameDir.length > 1) return finish(null);
|
|
4539
5529
|
const imports = index.importGraph.get(producerFile);
|
|
4540
5530
|
if (imports) {
|
|
4541
5531
|
const imported = typeDefs.filter(d => imports.has(d.file));
|
|
4542
|
-
if (imported.length === 1) return { fromFile: imported[0].file };
|
|
5532
|
+
if (imported.length === 1) return finish({ fromFile: imported[0].file });
|
|
4543
5533
|
}
|
|
4544
5534
|
// Re-export chains (fix #258, the #209 lesson brought to identity
|
|
4545
5535
|
// resolution): `use clap::Command` lands the import edge on the crate's
|
|
@@ -4547,17 +5537,18 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
|
|
|
4547
5537
|
// (depth 4) and pin only when exactly ONE same-name type is reachable.
|
|
4548
5538
|
const reachable = typeDefs.filter(d =>
|
|
4549
5539
|
_importReaches(index, producerFile, new Set([d.file])));
|
|
4550
|
-
if (reachable.length === 1) return { fromFile: reachable[0].file };
|
|
4551
|
-
return null;
|
|
5540
|
+
if (reachable.length === 1) return finish({ fromFile: reachable[0].file });
|
|
5541
|
+
return finish(null);
|
|
4552
5542
|
}
|
|
4553
5543
|
|
|
4554
5544
|
/**
|
|
4555
5545
|
* Defs that live in the package an import-qualified receiver names, resolved
|
|
4556
5546
|
* through the producer file's imports (alias-aware — importNames pairs 1:1
|
|
4557
5547
|
* with imports for Go). STRICT counterpart of _receiverPackageResolution:
|
|
4558
|
-
* used for POSITIVE typing (fix #207)
|
|
4559
|
-
*
|
|
4560
|
-
*
|
|
5548
|
+
* used for POSITIVE typing (fix #207). Prefer the import resolver's concrete
|
|
5549
|
+
* project package directory; this is what makes a subpackage importing its
|
|
5550
|
+
* own module root (`chi.RouteContext`) compiler-grade evidence for root
|
|
5551
|
+
* definitions. Returns null when the receiver names no import.
|
|
4561
5552
|
*/
|
|
4562
5553
|
function _qualifiedProducerDefs(index, fileEntry, receiver, defs) {
|
|
4563
5554
|
const modules = fileEntry.imports || [];
|
|
@@ -4576,6 +5567,23 @@ function _qualifiedProducerDefs(index, fileEntry, receiver, defs) {
|
|
|
4576
5567
|
}) || null;
|
|
4577
5568
|
}
|
|
4578
5569
|
if (!importModule || !importModule.includes('/')) return null;
|
|
5570
|
+
|
|
5571
|
+
// graph-build resolves a Go import to one representative source file in
|
|
5572
|
+
// the imported package. Package identity is the containing directory, so
|
|
5573
|
+
// every definition in that directory is eligible. This exact resolver
|
|
5574
|
+
// evidence must precede path heuristics: module-root packages have a
|
|
5575
|
+
// project-relative directory of `.` and were previously (and wrongly)
|
|
5576
|
+
// classified as external producers.
|
|
5577
|
+
const resolvedRel = fileEntry.moduleResolved && fileEntry.moduleResolved[importModule];
|
|
5578
|
+
if (resolvedRel) {
|
|
5579
|
+
const resolvedFile = path.isAbsolute(resolvedRel)
|
|
5580
|
+
? resolvedRel
|
|
5581
|
+
: path.join(index.root, resolvedRel);
|
|
5582
|
+
const resolvedDir = path.dirname(resolvedFile);
|
|
5583
|
+
const resolved = defs.filter(d => d.file && path.dirname(d.file) === resolvedDir);
|
|
5584
|
+
if (resolved.length > 0) return resolved;
|
|
5585
|
+
}
|
|
5586
|
+
|
|
4579
5587
|
const parts = importModule.split('/');
|
|
4580
5588
|
const last = parts[parts.length - 1];
|
|
4581
5589
|
const pkgSeg = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
@@ -4656,6 +5664,37 @@ function _receiverTypeTrustedForExclusion(index, typeName) {
|
|
|
4656
5664
|
return !!defs && defs.some(d => d.type === 'class' || d.type === 'struct');
|
|
4657
5665
|
}
|
|
4658
5666
|
|
|
5667
|
+
/**
|
|
5668
|
+
* Resolve the module that supplied a structural constructor type hint.
|
|
5669
|
+
* Exact project resolution pins later type-identity checks to that module;
|
|
5670
|
+
* an external module or a project-looking resolver gap is explicit routing
|
|
5671
|
+
* evidence, never confirmation evidence.
|
|
5672
|
+
*/
|
|
5673
|
+
function _structuralQualifiedReceiverOrigin(index, fileEntry, qualifier, typeName) {
|
|
5674
|
+
const bindings = (fileEntry.importBindings || []).filter(b => b.name === qualifier);
|
|
5675
|
+
if (bindings.length === 0) return { kind: 'unknown', via: `${qualifier}.${typeName}` };
|
|
5676
|
+
let projectish = false;
|
|
5677
|
+
for (const binding of bindings) {
|
|
5678
|
+
const moduleName = String(binding.module || '');
|
|
5679
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[moduleName];
|
|
5680
|
+
if (rel) {
|
|
5681
|
+
return {
|
|
5682
|
+
kind: 'project',
|
|
5683
|
+
via: `${qualifier}.${typeName}`,
|
|
5684
|
+
fromFile: path.join(index.root, rel),
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
const first = moduleName.split(/[./]/).filter(Boolean)[0];
|
|
5688
|
+
if (moduleName.startsWith('.') ||
|
|
5689
|
+
(first && _projectTopLevelNames(index).has(first))) {
|
|
5690
|
+
projectish = true;
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
return projectish
|
|
5694
|
+
? { kind: 'unknown', via: `${qualifier}.${typeName} — unresolved module` }
|
|
5695
|
+
: { kind: 'external', via: `${qualifier}.${typeName}` };
|
|
5696
|
+
}
|
|
5697
|
+
|
|
4659
5698
|
/**
|
|
4660
5699
|
* Resolve which same-name TYPE an unqualified receiver-type name denotes from
|
|
4661
5700
|
* a caller file's scope, and compare it against the pinned target's package
|
|
@@ -4866,6 +5905,19 @@ function _submoduleReceiverModule(index, fileEntry, receiverName) {
|
|
|
4866
5905
|
*/
|
|
4867
5906
|
function _importReaches(index, fromAbs, targetFiles, maxDepth = 4) {
|
|
4868
5907
|
if (targetFiles.has(fromAbs)) return true;
|
|
5908
|
+
// Import ownership is queried repeatedly while a reachability walk
|
|
5909
|
+
// resolves return types and namespace calls. The graph is immutable for
|
|
5910
|
+
// the duration of an operation, so memoize the bounded BFS by source,
|
|
5911
|
+
// target set, and depth. This removed hundreds of milliseconds of
|
|
5912
|
+
// identical barrel-chain walks on large workspaces.
|
|
5913
|
+
const opCache = index._opImportReachCache;
|
|
5914
|
+
const targetKey = [...targetFiles].sort(codeUnitCompare).join('\x00');
|
|
5915
|
+
const cacheKey = `${maxDepth}\x00${fromAbs}\x00${targetKey}`;
|
|
5916
|
+
if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
|
|
5917
|
+
const finish = (value) => {
|
|
5918
|
+
if (opCache) opCache.set(cacheKey, value);
|
|
5919
|
+
return value;
|
|
5920
|
+
};
|
|
4869
5921
|
const visited = new Set([fromAbs]);
|
|
4870
5922
|
let frontier = [fromAbs];
|
|
4871
5923
|
for (let d = 0; d < maxDepth; d++) {
|
|
@@ -4875,7 +5927,7 @@ function _importReaches(index, fromAbs, targetFiles, maxDepth = 4) {
|
|
|
4875
5927
|
if (!edges) continue;
|
|
4876
5928
|
for (const e of edges) {
|
|
4877
5929
|
if (visited.has(e)) continue;
|
|
4878
|
-
if (targetFiles.has(e)) return true;
|
|
5930
|
+
if (targetFiles.has(e)) return finish(true);
|
|
4879
5931
|
visited.add(e);
|
|
4880
5932
|
next.push(e);
|
|
4881
5933
|
}
|
|
@@ -4883,7 +5935,7 @@ function _importReaches(index, fromAbs, targetFiles, maxDepth = 4) {
|
|
|
4883
5935
|
if (next.length === 0) break;
|
|
4884
5936
|
frontier = next;
|
|
4885
5937
|
}
|
|
4886
|
-
return false;
|
|
5938
|
+
return finish(false);
|
|
4887
5939
|
}
|
|
4888
5940
|
|
|
4889
5941
|
/**
|
|
@@ -5000,8 +6052,8 @@ function _sameNominalPackageDir(dirA, dirB, language) {
|
|
|
5000
6052
|
if (dirA === dirB) return true;
|
|
5001
6053
|
if (language !== 'java') return false;
|
|
5002
6054
|
const norm = (d) => {
|
|
5003
|
-
const m = d.match(/^(.*?)[
|
|
5004
|
-
return m ? `${m[1]}
|
|
6055
|
+
const m = d.match(/^(.*?)[/\\]src[/\\][^/\\]+[/\\]java(?:[/\\](.*))?$/);
|
|
6056
|
+
return m ? `${m[1]}\0${m[2] || ''}` : null;
|
|
5005
6057
|
};
|
|
5006
6058
|
const a = norm(dirA);
|
|
5007
6059
|
if (a === null) return false;
|
|
@@ -5011,12 +6063,15 @@ function _sameNominalPackageDir(dirA, dirB, language) {
|
|
|
5011
6063
|
function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs) {
|
|
5012
6064
|
const typeDefs = (index.symbols.get(knownType) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
5013
6065
|
if (typeDefs.length <= 1) return 'target';
|
|
6066
|
+
const language = index.files.get(filePath)?.language;
|
|
5014
6067
|
const targetDirs = new Set(targetDefs.map(d => d.file && path.dirname(d.file)).filter(Boolean));
|
|
5015
|
-
const inTargetPkg = (d) => d.file && targetDirs.
|
|
6068
|
+
const inTargetPkg = (d) => d.file && [...targetDirs].some(dir =>
|
|
6069
|
+
_sameNominalPackageDir(path.dirname(d.file), dir, language));
|
|
5016
6070
|
const sameFile = typeDefs.filter(d => d.file === filePath);
|
|
5017
6071
|
if (sameFile.length > 0) return sameFile.some(inTargetPkg) ? 'target' : 'other';
|
|
5018
6072
|
const callerDir = path.dirname(filePath);
|
|
5019
|
-
const sameDir = typeDefs.filter(d => d.file &&
|
|
6073
|
+
const sameDir = typeDefs.filter(d => d.file &&
|
|
6074
|
+
_sameNominalPackageDir(path.dirname(d.file), callerDir, language));
|
|
5020
6075
|
if (sameDir.length > 0) return sameDir.some(inTargetPkg) ? 'target' : 'other';
|
|
5021
6076
|
const imports = index.importGraph.get(filePath);
|
|
5022
6077
|
if (imports) {
|
|
@@ -5026,6 +6081,71 @@ function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs) {
|
|
|
5026
6081
|
return 'unknown';
|
|
5027
6082
|
}
|
|
5028
6083
|
|
|
6084
|
+
/**
|
|
6085
|
+
* Resolve a structural flow type by declaration origin and walk that exact
|
|
6086
|
+
* declaration's inheritance chain toward the pinned target owner. This is
|
|
6087
|
+
* stricter than name-only targetTypes and more accurate than comparing the
|
|
6088
|
+
* receiver type's directory directly with the target method's directory:
|
|
6089
|
+
* legitimate subclasses routinely live elsewhere (Click test-local
|
|
6090
|
+
* CustomCommand extends click.Command), while parallel package versions may
|
|
6091
|
+
* reuse every class name (zod v3/v4 ZodArray -> ZodType).
|
|
6092
|
+
*/
|
|
6093
|
+
function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, targetDefs) {
|
|
6094
|
+
const origin = _resolveFlowTypeOrigin(index, originFile, knownType);
|
|
6095
|
+
if (!origin?.fromFile) return 'unknown';
|
|
6096
|
+
const targetOwners = new Set(targetDefs
|
|
6097
|
+
.map(d => d.className || (d.receiver && d.receiver.replace(/^\*/, '')))
|
|
6098
|
+
.filter(Boolean));
|
|
6099
|
+
if (targetOwners.size === 0) return 'unknown';
|
|
6100
|
+
const targetFiles = new Map();
|
|
6101
|
+
for (const owner of targetOwners) {
|
|
6102
|
+
const methodFiles = targetDefs.filter(d =>
|
|
6103
|
+
(d.className || (d.receiver && d.receiver.replace(/^\*/, ''))) === owner)
|
|
6104
|
+
.map(d => d.file).filter(Boolean);
|
|
6105
|
+
const typeDefs = (index.symbols.get(owner) || [])
|
|
6106
|
+
.filter(d => IDENTITY_TYPE_KINDS.has(d.type) && d.file);
|
|
6107
|
+
const files = new Set();
|
|
6108
|
+
for (const mf of methodFiles) {
|
|
6109
|
+
for (const td of typeDefs) {
|
|
6110
|
+
if (td.file === mf || path.dirname(td.file) === path.dirname(mf)) files.add(td.file);
|
|
6111
|
+
}
|
|
6112
|
+
if (files.size === 0) files.add(mf);
|
|
6113
|
+
}
|
|
6114
|
+
targetFiles.set(owner, files);
|
|
6115
|
+
}
|
|
6116
|
+
const ownerIdentity = (name, file) => {
|
|
6117
|
+
if (!targetOwners.has(name)) return null;
|
|
6118
|
+
if (!file) return 'unknown';
|
|
6119
|
+
const files = targetFiles.get(name) || new Set();
|
|
6120
|
+
if (files.has(file)) return 'target';
|
|
6121
|
+
const sameDir = [...files].some(f => path.dirname(f) === path.dirname(file));
|
|
6122
|
+
return sameDir ? 'target' : 'other';
|
|
6123
|
+
};
|
|
6124
|
+
const direct = ownerIdentity(knownType, origin.fromFile);
|
|
6125
|
+
if (direct) return direct;
|
|
6126
|
+
|
|
6127
|
+
const queue = [{ name: knownType, file: origin.fromFile }];
|
|
6128
|
+
const visited = new Set();
|
|
6129
|
+
let sawUnresolved = false;
|
|
6130
|
+
while (queue.length > 0 && visited.size < 128) {
|
|
6131
|
+
const cur = queue.shift();
|
|
6132
|
+
const key = `${cur.file || ''}\0${cur.name}`;
|
|
6133
|
+
if (visited.has(key)) continue;
|
|
6134
|
+
visited.add(key);
|
|
6135
|
+
const parents = index._getInheritanceParents(cur.name, cur.file) || [];
|
|
6136
|
+
for (const parent of parents) {
|
|
6137
|
+
const pFile = index._resolveClassFile
|
|
6138
|
+
? index._resolveClassFile(parent, cur.file) : undefined;
|
|
6139
|
+
const verdict = ownerIdentity(parent, pFile);
|
|
6140
|
+
if (verdict === 'target') return 'target';
|
|
6141
|
+
if (verdict === 'unknown') sawUnresolved = true;
|
|
6142
|
+
if (pFile) queue.push({ name: parent, file: pFile });
|
|
6143
|
+
else sawUnresolved = true;
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
return sawUnresolved ? 'unknown' : 'other';
|
|
6147
|
+
}
|
|
6148
|
+
|
|
5029
6149
|
/**
|
|
5030
6150
|
* Is typeName an ancestor (transitively) of any target definition's class?
|
|
5031
6151
|
* Used by receiver-class disambiguation: a receiver typed as a SUPERTYPE of
|
|
@@ -5079,6 +6199,18 @@ function _collectDescendants(index, className, cap = 256) {
|
|
|
5079
6199
|
return out;
|
|
5080
6200
|
}
|
|
5081
6201
|
|
|
6202
|
+
function _descendantOverrideOwners(index, className, methodName) {
|
|
6203
|
+
const descendants = _collectDescendants(index, className);
|
|
6204
|
+
descendants.delete(className);
|
|
6205
|
+
const owners = new Set();
|
|
6206
|
+
for (const d of (index.symbols.get(methodName) || [])) {
|
|
6207
|
+
if (NON_CALLABLE_TYPES.has(d.type)) continue;
|
|
6208
|
+
const owner = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
|
|
6209
|
+
if (owner && descendants.has(owner)) owners.add(owner);
|
|
6210
|
+
}
|
|
6211
|
+
return owners;
|
|
6212
|
+
}
|
|
6213
|
+
|
|
5082
6214
|
function _shareProjectDescendant(index, className, targetClasses) {
|
|
5083
6215
|
if (!targetClasses || targetClasses.size === 0) return false;
|
|
5084
6216
|
const mine = _collectDescendants(index, className);
|
|
@@ -5117,7 +6249,7 @@ function _shareProjectDescendant(index, className, targetClasses) {
|
|
|
5117
6249
|
* signature member and never closes. `definitions` is the same-name def list,
|
|
5118
6250
|
* so the group key needs no name component.
|
|
5119
6251
|
*/
|
|
5120
|
-
function
|
|
6252
|
+
function _closeCallableIdentityGroup(targetDefs, definitions) {
|
|
5121
6253
|
const keyOf = (d) => `${d.file}\0${d.className || ''}\0${d.isNested ? 1 : 0}`;
|
|
5122
6254
|
const groups = new Map();
|
|
5123
6255
|
for (const d of definitions) {
|
|
@@ -5136,6 +6268,30 @@ function _closeOverloadGroup(targetDefs, definitions) {
|
|
|
5136
6268
|
expanded.push(d);
|
|
5137
6269
|
}
|
|
5138
6270
|
}
|
|
6271
|
+
|
|
6272
|
+
// TypeScript declaration/runtime pairing (preact-signals measured):
|
|
6273
|
+
// `class Signal { constructor(...) }` supplies the public type surface,
|
|
6274
|
+
// while `function Signal(this: Signal, ...)` is the ES5-size-optimized
|
|
6275
|
+
// runtime constructor. `new Signal()` resolves to both in the compiler
|
|
6276
|
+
// oracle. The explicit this:Self parameter is the proof; ordinary same-
|
|
6277
|
+
// name class/function declarations never close.
|
|
6278
|
+
const explicitThisSelf = (d) => {
|
|
6279
|
+
if (d.type !== 'function' || !Array.isArray(d.paramsStructured)) return false;
|
|
6280
|
+
const first = d.paramsStructured[0];
|
|
6281
|
+
if (!first || first.name !== 'this' || !first.type) return false;
|
|
6282
|
+
const head = String(first.type).replace(/<.*$/, '').split('.').pop();
|
|
6283
|
+
return head === d.name;
|
|
6284
|
+
};
|
|
6285
|
+
for (const t of targetDefs) {
|
|
6286
|
+
const pair = definitions.filter(d => d.file === t.file && d.name === t.name &&
|
|
6287
|
+
((t.type === 'class' && explicitThisSelf(d)) ||
|
|
6288
|
+
(explicitThisSelf(t) && d.type === 'class')));
|
|
6289
|
+
for (const d of pair) {
|
|
6290
|
+
if (targetDefs.includes(d) || (expanded && expanded.includes(d))) continue;
|
|
6291
|
+
if (!expanded) expanded = [...targetDefs];
|
|
6292
|
+
expanded.push(d);
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
5139
6295
|
return expanded || targetDefs;
|
|
5140
6296
|
}
|
|
5141
6297
|
|
|
@@ -5196,9 +6352,68 @@ function _goQualifierNamesImport(index, fieldFile, qualifier) {
|
|
|
5196
6352
|
// wins so import "k8s.io/client-go/kubernetes/scheme" prefers a def in
|
|
5197
6353
|
// .../kubernetes/scheme/ over .../kubeadm/scheme/). Extracted for reuse:
|
|
5198
6354
|
// the parser marks some package calls isMethod:false (fix #268).
|
|
6355
|
+
function _calleeStructuralModuleRoute(index, fileEntry, call, language) {
|
|
6356
|
+
const bindings = (fileEntry?.importBindings || []).filter(b => b.name === call.receiver);
|
|
6357
|
+
if (bindings.length === 0) return { unknown: true };
|
|
6358
|
+
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, call.name);
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
function _calleeStructuralImportedNameRoute(index, fileEntry, call, language) {
|
|
6362
|
+
const lookupName = call.resolvedName || call.name;
|
|
6363
|
+
let bindings = (fileEntry?.importBindings || []).filter(b => b.name === lookupName);
|
|
6364
|
+
if (call.resolvedName && bindings.some(b => b.alias)) {
|
|
6365
|
+
const paired = bindings.filter(b => b.alias === call.name);
|
|
6366
|
+
if (paired.length > 0) bindings = paired;
|
|
6367
|
+
}
|
|
6368
|
+
if (bindings.length === 0) return null;
|
|
6369
|
+
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, lookupName);
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
function _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, exportName) {
|
|
6373
|
+
const candidates = (index.symbols.get(exportName) || []).filter(d =>
|
|
6374
|
+
(!NON_CALLABLE_TYPES.has(d.type) ||
|
|
6375
|
+
(call.isConstructor && d.type === 'class') ||
|
|
6376
|
+
(langTraits(language)?.classesCallableWithoutNew && d.type === 'class')) &&
|
|
6377
|
+
_calleeLanguageCompatible(index, d, language));
|
|
6378
|
+
const matches = new Map();
|
|
6379
|
+
let sawProjectish = false;
|
|
6380
|
+
let sawUnknown = false;
|
|
6381
|
+
for (const binding of bindings) {
|
|
6382
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
|
|
6383
|
+
if (!rel) {
|
|
6384
|
+
const mod = String(binding.module || '');
|
|
6385
|
+
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
6386
|
+
if (mod.startsWith('.') ||
|
|
6387
|
+
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
6388
|
+
sawProjectish = true;
|
|
6389
|
+
sawUnknown = true;
|
|
6390
|
+
}
|
|
6391
|
+
continue;
|
|
6392
|
+
}
|
|
6393
|
+
sawProjectish = true;
|
|
6394
|
+
const moduleFile = path.join(index.root, rel);
|
|
6395
|
+
for (const d of candidates) {
|
|
6396
|
+
const verdict = _nameBindingReaches(index, moduleFile, exportName, new Set([d.file]));
|
|
6397
|
+
if (verdict === 'yes') matches.set(`${d.file}:${d.startLine}`, d);
|
|
6398
|
+
else if (verdict === 'unknown') sawUnknown = true;
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
if (matches.size > 0) return { matches: [...matches.values()] };
|
|
6402
|
+
if (!sawProjectish && !sawUnknown) return { external: true };
|
|
6403
|
+
return { unknown: true };
|
|
6404
|
+
}
|
|
6405
|
+
|
|
5199
6406
|
function _calleeGoPackageMatch(index, call, importModule) {
|
|
5200
|
-
const
|
|
5201
|
-
if (!
|
|
6407
|
+
const allSymbols = index.symbols.get(call.name);
|
|
6408
|
+
if (!allSymbols) return null;
|
|
6409
|
+
// Package-qualified composite literals (`clients.Locality{}`) name a
|
|
6410
|
+
// TYPE, never a same-named struct field. Ordinary package calls name
|
|
6411
|
+
// callable symbols. Shape-filter before package ranking so the first
|
|
6412
|
+
// declaration in the right directory cannot steal another namespace.
|
|
6413
|
+
const symbols = allSymbols.filter(s => call.isConstructor
|
|
6414
|
+
? ['struct', 'class', 'type'].includes(s.type)
|
|
6415
|
+
: !NON_CALLABLE_TYPES.has(s.type));
|
|
6416
|
+
if (symbols.length === 0) return null;
|
|
5202
6417
|
// Self-module imports (fix #268, cobra-measured — the #220(8) go.mod
|
|
5203
6418
|
// identity): `import "github.com/spf13/cobra"` from doc/ names the
|
|
5204
6419
|
// module's ROOT package, whose relative dir is '.' and can never
|
|
@@ -5236,12 +6451,28 @@ function _calleeGoPackageMatch(index, call, importModule) {
|
|
|
5236
6451
|
}
|
|
5237
6452
|
|
|
5238
6453
|
function _calleeOverloadSelect(index, call, matches, language) {
|
|
5239
|
-
if (matches.length
|
|
5240
|
-
if (call.argCount == null || call.argSpread)
|
|
6454
|
+
if (matches.length === 0) return { match: null };
|
|
6455
|
+
if (call.argCount == null || call.argSpread) {
|
|
6456
|
+
return matches.length === 1 ? { match: matches[0] } : { ambiguous: true };
|
|
6457
|
+
}
|
|
5241
6458
|
const fits = matches.filter(d => _callArityCompatible(call, [d], language));
|
|
5242
|
-
if (fits.length === 1) return { match: fits[0] };
|
|
5243
6459
|
if (fits.length === 0) return { match: null }; // fits nothing we model — no claim
|
|
5244
|
-
|
|
6460
|
+
let applicable = language === 'java'
|
|
6461
|
+
? fits.filter(d => _overloadApplicable(index, call, d))
|
|
6462
|
+
: fits;
|
|
6463
|
+
// Java overload resolution considers fixed-arity applicability before
|
|
6464
|
+
// variable-arity invocation. An inherited fixed method therefore wins
|
|
6465
|
+
// over a subclass varargs overload when the call omits the vararg array.
|
|
6466
|
+
if (language === 'java') {
|
|
6467
|
+
const fixed = applicable.filter(d =>
|
|
6468
|
+
!Array.isArray(d.paramsStructured) ||
|
|
6469
|
+
!d.paramsStructured.some(p => p?.rest));
|
|
6470
|
+
if (fixed.length > 0) applicable = fixed;
|
|
6471
|
+
if (applicable.length > 1) {
|
|
6472
|
+
const mostSpecific = _javaMostSpecificOverload(index, applicable, call.argCount);
|
|
6473
|
+
if (mostSpecific) return { match: mostSpecific };
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
5245
6476
|
if (applicable.length === 1) return { match: applicable[0] };
|
|
5246
6477
|
return { ambiguous: true };
|
|
5247
6478
|
}
|
|
@@ -5440,6 +6671,18 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
5440
6671
|
|
|
5441
6672
|
const typeKindsOf = (name) => (index.symbols.get(name) || [])
|
|
5442
6673
|
.filter(d => IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf));
|
|
6674
|
+
const structuralTypeInScope = (name, defs) => {
|
|
6675
|
+
if (defs.some(d => d.file === def.file)) return true;
|
|
6676
|
+
if ((fileEntry?.bindings || []).some(b => b.name === name)) return true;
|
|
6677
|
+
for (const im of (fileEntry?.importBindings || [])) {
|
|
6678
|
+
if (im.name !== name) continue;
|
|
6679
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[String(im.module || '')];
|
|
6680
|
+
if (!rel) continue;
|
|
6681
|
+
const abs = path.join(index.root, rel);
|
|
6682
|
+
if (defs.some(d => d.file === abs)) return true;
|
|
6683
|
+
}
|
|
6684
|
+
return false;
|
|
6685
|
+
};
|
|
5443
6686
|
|
|
5444
6687
|
// Multi-segment path receivers (std::sync::Arc::new): the LAST segment
|
|
5445
6688
|
// is the type; the qualifier owns it (#206). A crate-internal qualifier
|
|
@@ -5461,8 +6704,13 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
5461
6704
|
return { unverified: 'method-ambiguous' };
|
|
5462
6705
|
}
|
|
5463
6706
|
}
|
|
5464
|
-
if (!/^[A-Z]/.test(receiver)) return null;
|
|
5465
6707
|
let typeDefs = typeKindsOf(receiver);
|
|
6708
|
+
// Python permits lowercase class names. Capitalization is only a naming
|
|
6709
|
+
// convention there; an imported/same-scope class binding is stronger AST
|
|
6710
|
+
// evidence that `codes.is_informational()` is type-qualified dispatch.
|
|
6711
|
+
if (!/^[A-Z]/.test(receiver) &&
|
|
6712
|
+
!(traits?.typeSystem === 'structural' && typeDefs.length > 0 &&
|
|
6713
|
+
structuralTypeInScope(receiver, typeDefs))) return null;
|
|
5466
6714
|
if (typeDefs.length === 0) {
|
|
5467
6715
|
for (const im of (fileEntry?.importBindings || [])) {
|
|
5468
6716
|
if (im.name !== receiver) continue;
|
|
@@ -5497,9 +6745,7 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
5497
6745
|
return null;
|
|
5498
6746
|
}
|
|
5499
6747
|
if (traits?.typeSystem === 'structural') {
|
|
5500
|
-
|
|
5501
|
-
const bound = (fileEntry?.bindings || []).some(b => b.name === receiver);
|
|
5502
|
-
if (!inFile && !bound) return null;
|
|
6748
|
+
if (!structuralTypeInScope(receiver, typeDefs)) return null;
|
|
5503
6749
|
}
|
|
5504
6750
|
// Alias closure (#208): a pure alias set agreeing on one base is the
|
|
5505
6751
|
// SAME type — the method may live on the base's inherent impl.
|
|
@@ -5511,12 +6757,48 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
5511
6757
|
const symbols = index.symbols.get(call.name) || [];
|
|
5512
6758
|
const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
5513
6759
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
5514
|
-
|
|
5515
|
-
|
|
6760
|
+
if (language === 'java') {
|
|
6761
|
+
// A class-qualified Java call sees static overloads declared on the
|
|
6762
|
+
// qualifier and inherited from its ancestors. Resolve the complete
|
|
6763
|
+
// family in one pass so a fixed inherited method beats an otherwise
|
|
6764
|
+
// applicable subclass varargs overload (JLS phase ordering).
|
|
6765
|
+
const family = [];
|
|
6766
|
+
const seenTypes = new Set();
|
|
6767
|
+
const queue = [...candidateTypes];
|
|
6768
|
+
while (queue.length > 0 && seenTypes.size < 64) {
|
|
6769
|
+
const typeName = queue.shift();
|
|
6770
|
+
if (!typeName || seenTypes.has(typeName)) continue;
|
|
6771
|
+
seenTypes.add(typeName);
|
|
6772
|
+
family.push(...symbols.filter(s => isCallable(s) && s.type === 'static' &&
|
|
6773
|
+
s.className === typeName));
|
|
6774
|
+
const parents = index._getInheritanceParents?.(typeName, def.file);
|
|
6775
|
+
if (parents) queue.push(...parents);
|
|
6776
|
+
}
|
|
6777
|
+
const selected = _calleeOverloadSelect(index, call, family, language);
|
|
6778
|
+
if (selected.ambiguous) return { unverified: 'overload-ambiguous' };
|
|
6779
|
+
if (selected.match) {
|
|
6780
|
+
return { match: selected.match, typeName: selected.match.className || receiver };
|
|
6781
|
+
}
|
|
6782
|
+
// The qualifier still proves ownership when no overload accepts the
|
|
6783
|
+
// argument count. Caller-side verify must see this broken call site as
|
|
6784
|
+
// a mismatch; callee dependency analysis keeps it unverified.
|
|
6785
|
+
if (family.length > 0 && call.argCount != null && !call.argSpread &&
|
|
6786
|
+
family.every(d => !_callArityCompatible(call, [d], language))) {
|
|
6787
|
+
return {
|
|
6788
|
+
unverified: 'arity-mismatch',
|
|
6789
|
+
wrongArityOwner: true,
|
|
6790
|
+
typeName: receiver,
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
const selectOn = (tn) => _calleeOverloadSelect(index, call, symbols.filter(s => isCallable(s) &&
|
|
6795
|
+
(s.className === tn || (s.receiver && s.receiver.replace(/^\*/, '') === tn))), language);
|
|
5516
6796
|
let match = null;
|
|
5517
6797
|
let matchedType = null;
|
|
5518
6798
|
for (const tn of candidateTypes) {
|
|
5519
|
-
|
|
6799
|
+
const selected = selectOn(tn);
|
|
6800
|
+
if (selected.ambiguous) return { unverified: 'overload-ambiguous' };
|
|
6801
|
+
match = selected.match;
|
|
5520
6802
|
if (match) { matchedType = tn; break; }
|
|
5521
6803
|
}
|
|
5522
6804
|
if (!match && traits?.typeSystem === 'nominal') {
|
|
@@ -5524,7 +6806,9 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
5524
6806
|
const parentNames = index._getInheritanceParents?.(tn, def.file);
|
|
5525
6807
|
if (!parentNames) continue;
|
|
5526
6808
|
for (const pName of parentNames) {
|
|
5527
|
-
|
|
6809
|
+
const selected = selectOn(pName);
|
|
6810
|
+
if (selected.ambiguous) return { unverified: 'overload-ambiguous' };
|
|
6811
|
+
match = selected.match;
|
|
5528
6812
|
if (match) { matchedType = pName; break; }
|
|
5529
6813
|
}
|
|
5530
6814
|
if (match) break;
|
|
@@ -5602,8 +6886,8 @@ function _receiverTypeAncestors(index, typeName, maxHops = 6) {
|
|
|
5602
6886
|
function _calleeReceiverTypeRoute(index, call, localTypes, language) {
|
|
5603
6887
|
const raw = call.receiverType || localTypes?.get(call.receiver);
|
|
5604
6888
|
if (!raw || typeof raw !== 'string') return null;
|
|
5605
|
-
const head = _structuralTypeHead(raw) || raw;
|
|
5606
|
-
const norm =
|
|
6889
|
+
const head = _structuralTypeHead(raw, { index, language }) || raw;
|
|
6890
|
+
const norm = head;
|
|
5607
6891
|
const defs = (index.symbols.get(call.name) || []).filter(d =>
|
|
5608
6892
|
!NON_CALLABLE_TYPES.has(d.type) && d.className &&
|
|
5609
6893
|
_calleeLanguageCompatible(index, d, language));
|
|
@@ -5649,6 +6933,7 @@ function _calleeZeroCandidateName(index, call) {
|
|
|
5649
6933
|
function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, flowEntry) {
|
|
5650
6934
|
if (JS_GLOBAL_RECEIVERS.has(call.receiver)) return null;
|
|
5651
6935
|
if (call.receiverIsModule) return null;
|
|
6936
|
+
if (_universalMethodName(language, name)) return null;
|
|
5652
6937
|
const traits = langTraits(language);
|
|
5653
6938
|
if (traits?.typeSystem === 'structural' &&
|
|
5654
6939
|
_submoduleReceiverModule(index, fileEntry, call.receiver)) return null;
|
|
@@ -5681,8 +6966,8 @@ function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, fl
|
|
|
5681
6966
|
// a `new Map()` local can never single-owner-confirm a project method).
|
|
5682
6967
|
if (call.receiverType && typeof call.receiverType === 'string') {
|
|
5683
6968
|
const owner = ownerKeys.values().next().value;
|
|
5684
|
-
const head = _structuralTypeHead(call.receiverType) || call.receiverType;
|
|
5685
|
-
const norm =
|
|
6969
|
+
const head = _structuralTypeHead(call.receiverType, { index, language }) || call.receiverType;
|
|
6970
|
+
const norm = head;
|
|
5686
6971
|
if (head !== owner && norm !== owner &&
|
|
5687
6972
|
!_receiverTypeAncestors(index, head).has(owner)) return null;
|
|
5688
6973
|
}
|
|
@@ -5744,6 +7029,10 @@ function _callArityCompatible(call, targetDefs, language) {
|
|
|
5744
7029
|
}
|
|
5745
7030
|
|
|
5746
7031
|
const JAVA_PRIMITIVES = new Set(['int', 'long', 'short', 'byte', 'char', 'float', 'double', 'boolean']);
|
|
7032
|
+
const JAVA_FINAL_REFERENCE_TYPES = new Set([
|
|
7033
|
+
'String', 'Class', 'Boolean', 'Byte', 'Character', 'Short',
|
|
7034
|
+
'Integer', 'Long', 'Float', 'Double', 'Void',
|
|
7035
|
+
]);
|
|
5747
7036
|
|
|
5748
7037
|
// Which parameter types a call-site literal kind can bind (Java overload
|
|
5749
7038
|
// resolution: identity, widening, boxing — plus the boxed types' interfaces).
|
|
@@ -5772,9 +7061,30 @@ function _javaArgKindMatches(index, kind, paramType) {
|
|
|
5772
7061
|
if (!bare || bare === 'Object') return true;
|
|
5773
7062
|
if (/^[A-Z][0-9]?$/.test(bare)) return true; // generic type variable (T, E, K1...)
|
|
5774
7063
|
if (kind === 'null') return !JAVA_PRIMITIVES.has(bare);
|
|
5775
|
-
if (kind.startsWith('
|
|
7064
|
+
if (kind.startsWith('class:')) {
|
|
7065
|
+
return ['Class', 'Type', 'Serializable', 'GenericDeclaration',
|
|
7066
|
+
'AnnotatedElement'].includes(bare);
|
|
7067
|
+
}
|
|
7068
|
+
if (kind.startsWith('call:')) {
|
|
7069
|
+
const parts = kind.split(':');
|
|
7070
|
+
const owner = parts[1];
|
|
7071
|
+
const method = parts.slice(2).join(':');
|
|
7072
|
+
const returns = new Set((index.symbols.get(method) || [])
|
|
7073
|
+
.filter(d => d.className === owner && d.returnType)
|
|
7074
|
+
.map(d => String(d.returnType).replace(/<.*$/s, '').trim()
|
|
7075
|
+
.replace(/\[\]$/, '').split('.').pop())
|
|
7076
|
+
.filter(Boolean));
|
|
7077
|
+
// Only an owner-scoped, agreeing return annotation is type evidence.
|
|
7078
|
+
if (returns.size !== 1) return true;
|
|
7079
|
+
return _javaArgKindMatches(index, `type:${[...returns][0]}`, paramType);
|
|
7080
|
+
}
|
|
7081
|
+
if (kind.startsWith('new:') || kind.startsWith('cast:') || kind.startsWith('type:')) {
|
|
5776
7082
|
const t = kind.slice(kind.indexOf(':') + 1);
|
|
5777
7083
|
if (t === bare) return true;
|
|
7084
|
+
// These java.lang types are final. A statically known different type
|
|
7085
|
+
// can never bind their overload, even when the argument type's own
|
|
7086
|
+
// ancestry ends in external Object and is therefore incomplete.
|
|
7087
|
+
if (JAVA_FINAL_REFERENCE_TYPES.has(bare)) return false;
|
|
5778
7088
|
const tDefs = (index.symbols.get(t) || [])
|
|
5779
7089
|
.filter(d => d.type === 'class' || d.type === 'interface');
|
|
5780
7090
|
if (tDefs.length === 0) return true; // external arg type — unknowable
|
|
@@ -5807,6 +7117,58 @@ function _overloadApplicable(index, call, def) {
|
|
|
5807
7117
|
return true;
|
|
5808
7118
|
}
|
|
5809
7119
|
|
|
7120
|
+
function _javaBareParamType(param) {
|
|
7121
|
+
if (!param?.type) return null;
|
|
7122
|
+
return String(param.type).replace(/<.*$/s, '').trim()
|
|
7123
|
+
.replace(/\.\.\.$/, '').replace(/\[\]$/, '').split('.').pop() || null;
|
|
7124
|
+
}
|
|
7125
|
+
|
|
7126
|
+
function _javaParamAt(def, position) {
|
|
7127
|
+
const ps = def.paramsStructured;
|
|
7128
|
+
if (!Array.isArray(ps) || ps.length === 0) return null;
|
|
7129
|
+
if (position < ps.length) return ps[position];
|
|
7130
|
+
const last = ps[ps.length - 1];
|
|
7131
|
+
return last?.rest ? last : null;
|
|
7132
|
+
}
|
|
7133
|
+
|
|
7134
|
+
function _javaTypeAtLeastAsSpecific(index, subType, superType, subDef) {
|
|
7135
|
+
if (!subType || !superType) return false;
|
|
7136
|
+
if (subType === superType) return true;
|
|
7137
|
+
if (superType === 'Object' && !JAVA_PRIMITIVES.has(subType)) return true;
|
|
7138
|
+
if (subType === 'String' && ['CharSequence', 'Comparable', 'Serializable'].includes(superType)) {
|
|
7139
|
+
return true;
|
|
7140
|
+
}
|
|
7141
|
+
if (['Byte', 'Short', 'Integer', 'Long', 'Float', 'Double'].includes(subType) &&
|
|
7142
|
+
superType === 'Number') return true;
|
|
7143
|
+
const subDefs = (index.symbols.get(subType) || [])
|
|
7144
|
+
.filter(d => d.type === 'class' || d.type === 'interface');
|
|
7145
|
+
if (subDefs.length === 0) return false;
|
|
7146
|
+
return _isDispatchAncestor(index, superType, [{
|
|
7147
|
+
className: subType,
|
|
7148
|
+
file: subDef?.file || subDefs[0].file,
|
|
7149
|
+
}]);
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7152
|
+
// Conservative subset of Java's "most specific" rule. A candidate wins only
|
|
7153
|
+
// when every argument position is at least as specific as every competing
|
|
7154
|
+
// candidate and at least one position is strictly more specific. Unknown
|
|
7155
|
+
// relationships stay ambiguous.
|
|
7156
|
+
function _javaMostSpecificOverload(index, applicable, argCount) {
|
|
7157
|
+
if (!Number.isInteger(argCount) || applicable.length < 2) return null;
|
|
7158
|
+
const dominates = (a, b) => {
|
|
7159
|
+
let strict = false;
|
|
7160
|
+
for (let i = 0; i < argCount; i++) {
|
|
7161
|
+
const at = _javaBareParamType(_javaParamAt(a, i));
|
|
7162
|
+
const bt = _javaBareParamType(_javaParamAt(b, i));
|
|
7163
|
+
if (!at || !bt || !_javaTypeAtLeastAsSpecific(index, at, bt, a)) return false;
|
|
7164
|
+
if (at !== bt) strict = true;
|
|
7165
|
+
}
|
|
7166
|
+
return strict;
|
|
7167
|
+
};
|
|
7168
|
+
const winners = applicable.filter(a => applicable.every(b => a === b || dominates(a, b)));
|
|
7169
|
+
return winners.length === 1 ? winners[0] : null;
|
|
7170
|
+
}
|
|
7171
|
+
|
|
5810
7172
|
/**
|
|
5811
7173
|
* Overload discipline (Java): when the pinned target has same-class sibling
|
|
5812
7174
|
* overloads, decide what the call site's static argument shape proves.
|
|
@@ -5817,8 +7179,12 @@ function _overloadApplicable(index, call, def) {
|
|
|
5817
7179
|
function _overloadDiscipline(index, call, targetDefs, definitions) {
|
|
5818
7180
|
const targetOwners = new Set(targetDefs.map(d => d.className).filter(Boolean));
|
|
5819
7181
|
if (targetOwners.size === 0) return null;
|
|
7182
|
+
const targetLanguage = targetDefs[0]?.file && index.files.get(targetDefs[0].file)?.language;
|
|
7183
|
+
const sameOwnerIdentity = (d) => targetLanguage !== 'java' || targetDefs.some(t =>
|
|
7184
|
+
t.className === d.className && t.file && d.file &&
|
|
7185
|
+
_sameNominalPackageDir(path.dirname(t.file), path.dirname(d.file), 'java'));
|
|
5820
7186
|
const family = definitions.filter(d => !NON_CALLABLE_TYPES.has(d.type) &&
|
|
5821
|
-
d.className && targetOwners.has(d.className));
|
|
7187
|
+
d.className && targetOwners.has(d.className) && sameOwnerIdentity(d));
|
|
5822
7188
|
// Inherited sibling overloads (fix #268, javapoet-measured): the pin's
|
|
5823
7189
|
// dispatch surface includes ancestor same-name methods — ClassName's
|
|
5824
7190
|
// annotated(List) coexists with TypeName's FINAL annotated(Spec...), and
|
|
@@ -5856,6 +7222,11 @@ function _overloadDiscipline(index, call, targetDefs, definitions) {
|
|
|
5856
7222
|
if (family.every(d => pinnedKeys.has(`${d.file}:${d.startLine}`))) return null;
|
|
5857
7223
|
const applicable = family.filter(d => _overloadApplicable(index, call, d));
|
|
5858
7224
|
if (applicable.length === 0) return null; // shape fits nothing we model — no claim
|
|
7225
|
+
const mostSpecific = _javaMostSpecificOverload(index, applicable, call.argCount);
|
|
7226
|
+
if (mostSpecific) {
|
|
7227
|
+
return pinnedKeys.has(`${mostSpecific.file}:${mostSpecific.startLine}`)
|
|
7228
|
+
? null : 'other-overload';
|
|
7229
|
+
}
|
|
5859
7230
|
const pinnedApplicable = applicable.some(d => pinnedKeys.has(`${d.file}:${d.startLine}`));
|
|
5860
7231
|
if (!pinnedApplicable) return 'other-overload';
|
|
5861
7232
|
if (applicable.length === 1) return null; // uniquely the pinned overload
|
|
@@ -5957,12 +7328,74 @@ function _dispatchCapableSupertype(index, language, typeName, targetDefs, defini
|
|
|
5957
7328
|
if (definitions.some(d => d.className === typeName)) return true;
|
|
5958
7329
|
return _isDispatchAncestor(index, typeName, targetDefs);
|
|
5959
7330
|
}
|
|
7331
|
+
// A Go type declared in an external package may be an interface. Since
|
|
7332
|
+
// interface satisfaction is implicit, UCN has no implements edge that
|
|
7333
|
+
// could disprove dispatch into a project method. Route it visibly; never
|
|
7334
|
+
// use this uncertainty to confirm an edge.
|
|
7335
|
+
if (language === 'go' && _externalGoDispatchType(index, typeName) &&
|
|
7336
|
+
targetDefs.some(d => d.className || d.receiver)) return true;
|
|
5960
7337
|
if (traits.allMethodsVirtual) {
|
|
5961
7338
|
return _isDispatchAncestor(index, typeName, targetDefs);
|
|
5962
7339
|
}
|
|
5963
7340
|
return false;
|
|
5964
7341
|
}
|
|
5965
7342
|
|
|
7343
|
+
function _externalGoDispatchType(index, typeName) {
|
|
7344
|
+
if (!typeName || BUILTIN_RECEIVER_TYPES.has(typeName)) return false;
|
|
7345
|
+
// `error` and `any` are builtin interfaces and therefore dispatch
|
|
7346
|
+
// contracts; other lowercase builtins are concrete and excluded above.
|
|
7347
|
+
if (typeName === 'error' || typeName === 'any') return true;
|
|
7348
|
+
const defs = index.symbols.get(typeName) || [];
|
|
7349
|
+
return !defs.some(d => d.type === 'class' || d.type === 'struct' ||
|
|
7350
|
+
d.type === 'interface' || d.type === 'trait' || d.type === 'type');
|
|
7351
|
+
}
|
|
7352
|
+
|
|
7353
|
+
/**
|
|
7354
|
+
* Resolve the package provenance retained on a Go receiver annotation.
|
|
7355
|
+
* `http.Handler` must never collapse into an unrelated project-local
|
|
7356
|
+
* `Handler`. A resolved project import returns the exact package directory
|
|
7357
|
+
* and its same-named type declarations; an unresolved imported package is
|
|
7358
|
+
* external/opaque and therefore dispatch-capable but never confirmable.
|
|
7359
|
+
*/
|
|
7360
|
+
function _goQualifiedReceiverType(index, fileEntry, qualifier, typeName) {
|
|
7361
|
+
if (!fileEntry || !qualifier || !typeName) return null;
|
|
7362
|
+
const modules = fileEntry.imports || [];
|
|
7363
|
+
const names = fileEntry.importNames || [];
|
|
7364
|
+
let importModule = null;
|
|
7365
|
+
if (names.length === modules.length) {
|
|
7366
|
+
const i = names.indexOf(qualifier);
|
|
7367
|
+
if (i >= 0) importModule = modules[i];
|
|
7368
|
+
}
|
|
7369
|
+
if (!importModule) {
|
|
7370
|
+
importModule = modules.find(mod => {
|
|
7371
|
+
const parts = String(mod).split('/');
|
|
7372
|
+
const last = parts[parts.length - 1];
|
|
7373
|
+
const packageName = /^v\d+$/.test(last) && parts.length > 1
|
|
7374
|
+
? parts[parts.length - 2] : last;
|
|
7375
|
+
return packageName === qualifier;
|
|
7376
|
+
}) || null;
|
|
7377
|
+
}
|
|
7378
|
+
if (!importModule) return null;
|
|
7379
|
+
|
|
7380
|
+
const resolvedRel = fileEntry.moduleResolved && fileEntry.moduleResolved[importModule];
|
|
7381
|
+
if (!resolvedRel) {
|
|
7382
|
+
return { kind: 'opaque', via: `${qualifier}.${typeName}`, importModule };
|
|
7383
|
+
}
|
|
7384
|
+
const resolvedFile = path.isAbsolute(resolvedRel)
|
|
7385
|
+
? resolvedRel : path.join(index.root, resolvedRel);
|
|
7386
|
+
const dir = path.dirname(resolvedFile);
|
|
7387
|
+
const defs = (index.symbols.get(typeName) || []).filter(d =>
|
|
7388
|
+
d.file && path.dirname(d.file) === dir && IDENTITY_TYPE_KINDS.has(d.type));
|
|
7389
|
+
return { kind: 'project', via: `${qualifier}.${typeName}`, importModule, dir, defs };
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
/** A declared interface/trait receiver names a dispatch contract, not one executable body. */
|
|
7393
|
+
function _isDispatchContractType(index, typeName) {
|
|
7394
|
+
if (!typeName) return false;
|
|
7395
|
+
return (index.symbols.get(typeName) || [])
|
|
7396
|
+
.some(d => d.type === 'interface' || d.type === 'trait');
|
|
7397
|
+
}
|
|
7398
|
+
|
|
5966
7399
|
/**
|
|
5967
7400
|
* Like _isAncestorOfTargetClass, but walks `implements` records (Java
|
|
5968
7401
|
* implements clauses, Rust `impl Trait for Type` surfaced as implements)
|
|
@@ -6205,7 +7638,7 @@ function _normalizeFieldTypeName(raw, language) {
|
|
|
6205
7638
|
if (langTraits(language)?.typeSystem === 'structural') {
|
|
6206
7639
|
// JS/TS/Python (fix #219): compiler-true annotation heads, value-
|
|
6207
7640
|
// position semantics — a field declared Promise<X> HOLDS a Promise.
|
|
6208
|
-
return _structuralTypeHead(t);
|
|
7641
|
+
return _structuralTypeHead(t, { language });
|
|
6209
7642
|
}
|
|
6210
7643
|
return null;
|
|
6211
7644
|
}
|
|
@@ -6231,6 +7664,7 @@ const _PY_TYPING_BUILTINS = {
|
|
|
6231
7664
|
function _structuralTypeHead(text, opts = {}) {
|
|
6232
7665
|
if (!text || typeof text !== 'string') return null;
|
|
6233
7666
|
let t = text.trim().replace(/^readonly\s+/, '').replace(/^["']|["']$/g, '').trim();
|
|
7667
|
+
const explicitlyTypingQualified = /^(?:typing\.)/.test(t);
|
|
6234
7668
|
if (t.includes('|')) {
|
|
6235
7669
|
const parts = t.split('|').map(s => s.trim())
|
|
6236
7670
|
.filter(s => s && !['None', 'null', 'undefined'].includes(s));
|
|
@@ -6255,7 +7689,28 @@ function _structuralTypeHead(text, opts = {}) {
|
|
|
6255
7689
|
if (m) t = m[1];
|
|
6256
7690
|
const last = t.split('.').pop();
|
|
6257
7691
|
if (!/^[A-Za-z_$][\w$]*$/.test(last)) return null; // fn types, object literals, tuples
|
|
6258
|
-
|
|
7692
|
+
// Python's typing aliases share ordinary identifier names with perfectly
|
|
7693
|
+
// valid project classes (`class Text` in rich; user-defined List/Set are
|
|
7694
|
+
// also legal). A global Text -> str rewrite is therefore exclusion-grade
|
|
7695
|
+
// misinformation. Explicit `typing.Text` is unambiguous; an unqualified
|
|
7696
|
+
// alias is normalized only when no project type with that name is
|
|
7697
|
+
// reachable from the annotation's defining file. TypeScript must never
|
|
7698
|
+
// receive Python alias normalization at all.
|
|
7699
|
+
const builtinAlias = _PY_TYPING_BUILTINS[last];
|
|
7700
|
+
if (!builtinAlias || (opts.language && opts.language !== 'python')) return last;
|
|
7701
|
+
if (explicitlyTypingQualified) return builtinAlias;
|
|
7702
|
+
if (opts.index) {
|
|
7703
|
+
const projectTypeDefs = (opts.index.symbols.get(last) || []).filter(d =>
|
|
7704
|
+
['class', 'interface', 'type', 'trait', 'struct', 'record'].includes(d.type));
|
|
7705
|
+
if (projectTypeDefs.length > 0) {
|
|
7706
|
+
if (!opts.originFile) {
|
|
7707
|
+
if (projectTypeDefs.length === 1) return last;
|
|
7708
|
+
} else if (_resolveFlowTypeOrigin(opts.index, opts.originFile, last)) {
|
|
7709
|
+
return last;
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
return builtinAlias;
|
|
6259
7714
|
}
|
|
6260
7715
|
|
|
6261
7716
|
// Structural annotation heads that carry no receiver identity: TS escape
|
|
@@ -6293,8 +7748,7 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
|
|
|
6293
7748
|
const language = fileEntry.language;
|
|
6294
7749
|
const defs = (index.symbols.get(call.receiverCall) || [])
|
|
6295
7750
|
.filter(d => !NON_CALLABLE_TYPES.has(d.type));
|
|
6296
|
-
let producer
|
|
6297
|
-
let selfClass;
|
|
7751
|
+
let producer;
|
|
6298
7752
|
if (call.receiverCallReceiver) {
|
|
6299
7753
|
// Package-qualified producer: os.CreateTemp().Name(). A package that
|
|
6300
7754
|
// resolves to no project def decided the type OUTSIDE the project —
|
|
@@ -6308,17 +7762,13 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
|
|
|
6308
7762
|
}
|
|
6309
7763
|
producer = inPkg[0];
|
|
6310
7764
|
} else if (call.receiverCallIsMethod) {
|
|
6311
|
-
//
|
|
6312
|
-
//
|
|
6313
|
-
//
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
producer = methodDefs[0];
|
|
6319
|
-
const classes = new Set(methodDefs.map(d =>
|
|
6320
|
-
d.className || (d.receiver || '').replace(/^\*/, '')));
|
|
6321
|
-
selfClass = classes.size === 1 ? [...classes][0] : undefined;
|
|
7765
|
+
// An untyped method receiver cannot borrow a return annotation by
|
|
7766
|
+
// terminal name alone. External owners are invisible to the project
|
|
7767
|
+
// index (`Map.Entry.getValue()` vs a unique project getValue()), so
|
|
7768
|
+
// project-wide agreement is not a closed world and is unsafe for
|
|
7769
|
+
// exclusion-grade receiver typing. The recursive fold handles method
|
|
7770
|
+
// producers when their own receiver is typed; otherwise stay unknown.
|
|
7771
|
+
return null;
|
|
6322
7772
|
} else {
|
|
6323
7773
|
// Plain producer: Go resolves within the package; others same-file
|
|
6324
7774
|
// narrowing, then global-unique (#199/#207 rules). Where bare calls
|
|
@@ -6354,6 +7804,7 @@ function _nominalChainedReceiverType(index, call, fileEntry, filePath) {
|
|
|
6354
7804
|
if (!chosen || !chosen.returnType) return null;
|
|
6355
7805
|
producer = chosen;
|
|
6356
7806
|
}
|
|
7807
|
+
const selfClass = producer.className || (producer.receiver || '').replace(/^\*/, '') || undefined;
|
|
6357
7808
|
const parsed = _returnTypeNameNominal(producer.returnType, language, { selfClass });
|
|
6358
7809
|
if (!parsed) return null;
|
|
6359
7810
|
const origin = _resolveFlowTypeOrigin(index, producer.file || filePath, parsed.name, parsed.qualifier);
|
|
@@ -6376,16 +7827,27 @@ function _chainedReceiverType(index, call, language) {
|
|
|
6376
7827
|
if (language === 'python' && !call.receiverCallAwaited &&
|
|
6377
7828
|
producers.some(d => d.isAsync)) return null;
|
|
6378
7829
|
const heads = new Set();
|
|
7830
|
+
const origins = new Set();
|
|
6379
7831
|
for (const d of producers) {
|
|
6380
|
-
const h = _structuralTypeHead(d.returnType, {
|
|
7832
|
+
const h = _structuralTypeHead(d.returnType, {
|
|
7833
|
+
unwrapAsync: call.receiverCallAwaited, index, language, originFile: d.file,
|
|
7834
|
+
});
|
|
6381
7835
|
if (!h) return null;
|
|
6382
7836
|
heads.add(h);
|
|
6383
7837
|
if (heads.size > 1) return null;
|
|
7838
|
+
const typeDefs = (index.symbols.get(h) || []).filter(td => IDENTITY_TYPE_KINDS.has(td.type));
|
|
7839
|
+
if (typeDefs.length > 0) {
|
|
7840
|
+
const origin = _resolveFlowTypeOrigin(index, d.file, h);
|
|
7841
|
+
if (!origin) return null;
|
|
7842
|
+
origins.add(origin.fromFile);
|
|
7843
|
+
if (origins.size > 1) return null;
|
|
7844
|
+
}
|
|
6384
7845
|
}
|
|
6385
7846
|
const head = [...heads][0];
|
|
6386
7847
|
if (/^[A-Z][A-Z0-9]?$/.test(head)) return null; // generic type param (T, K, V1)
|
|
6387
7848
|
if (_STRUCTURAL_FLOW_REJECT.has(head)) return null;
|
|
6388
|
-
|
|
7849
|
+
const fromFile = origins.size === 1 ? [...origins][0] : undefined;
|
|
7850
|
+
return { type: head, ...(fromFile && { fromFile }) };
|
|
6389
7851
|
}
|
|
6390
7852
|
|
|
6391
7853
|
// ── Chained-receiver fold (fix #258, clap-measured) ─────────────────────────
|
|
@@ -6469,7 +7931,9 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
|
|
|
6469
7931
|
const heads = new Set();
|
|
6470
7932
|
for (const d of owned) {
|
|
6471
7933
|
if (!d.returnType) return null;
|
|
6472
|
-
let h = _structuralTypeHead(d.returnType, {
|
|
7934
|
+
let h = _structuralTypeHead(d.returnType, {
|
|
7935
|
+
unwrapAsync: opts.consumerAwaited, index, language, originFile: d.file,
|
|
7936
|
+
});
|
|
6473
7937
|
if (h === 'this' || h === 'Self') h = selfType;
|
|
6474
7938
|
if (!h) return null;
|
|
6475
7939
|
heads.add(h);
|
|
@@ -6478,6 +7942,18 @@ function _methodReturnOnType(index, typeName, fromFile, methodName, language, op
|
|
|
6478
7942
|
const head = [...heads][0];
|
|
6479
7943
|
if (/^[A-Z][A-Z0-9]?$/.test(head)) return null; // generic type param
|
|
6480
7944
|
if (_STRUCTURAL_FLOW_REJECT.has(head)) return null;
|
|
7945
|
+
const returnTypeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
7946
|
+
if (returnTypeDefs.length > 0) {
|
|
7947
|
+
const origins = new Set();
|
|
7948
|
+
for (const d of owned) {
|
|
7949
|
+
const origin = _resolveFlowTypeOrigin(index, d.file || opts.filePath, head);
|
|
7950
|
+
if (!origin) return null;
|
|
7951
|
+
origins.add(origin.fromFile);
|
|
7952
|
+
if (origins.size > 1) return null;
|
|
7953
|
+
}
|
|
7954
|
+
const fromFile = [...origins][0];
|
|
7955
|
+
return { type: head, ...(fromFile && { fromFile }) };
|
|
7956
|
+
}
|
|
6481
7957
|
return { type: head };
|
|
6482
7958
|
}
|
|
6483
7959
|
|
|
@@ -6492,7 +7968,7 @@ function _typeOfCallResultFold(index, fileEntry, filePath, record, ctx, consumer
|
|
|
6492
7968
|
if (ctx.memo.has(record)) return ctx.memo.get(record);
|
|
6493
7969
|
if (ctx.visiting.has(record) || ctx.visiting.size > 64) return null;
|
|
6494
7970
|
ctx.visiting.add(record);
|
|
6495
|
-
let out
|
|
7971
|
+
let out;
|
|
6496
7972
|
try {
|
|
6497
7973
|
out = _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, consumerAwaited);
|
|
6498
7974
|
} finally {
|
|
@@ -6560,7 +8036,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6560
8036
|
}
|
|
6561
8037
|
// Structural module-qualified producer: z.string() — resolve through the
|
|
6562
8038
|
// file's import bindings (flow-map rails, incl. the #222 externality test).
|
|
6563
|
-
if (!nominal && record.isMethod && record.receiver &&
|
|
8039
|
+
if (!nominal && record.isMethod && record.receiver &&
|
|
8040
|
+
(record.receiverIsModule || _isStructuralImportReceiver(fileEntry, record))) {
|
|
6564
8041
|
const binding = (fileEntry?.importBindings || []).find(b => b.name === record.receiver);
|
|
6565
8042
|
const rel = binding && fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
|
|
6566
8043
|
if (binding && !rel) {
|
|
@@ -6585,14 +8062,27 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6585
8062
|
if (language === 'python' && !consumerAwaited && matches.some(d => d.isAsync)) return null;
|
|
6586
8063
|
const heads = new Set();
|
|
6587
8064
|
for (const d of matches) {
|
|
6588
|
-
const h = _structuralTypeHead(d.returnType, {
|
|
8065
|
+
const h = _structuralTypeHead(d.returnType, {
|
|
8066
|
+
unwrapAsync: consumerAwaited, index, language, originFile: d.file,
|
|
8067
|
+
});
|
|
6589
8068
|
if (!h) return null;
|
|
6590
8069
|
heads.add(h);
|
|
6591
8070
|
}
|
|
6592
8071
|
if (heads.size !== 1) return null;
|
|
6593
8072
|
const head = [...heads][0];
|
|
6594
8073
|
if (/^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
|
|
6595
|
-
|
|
8074
|
+
const origins = new Set();
|
|
8075
|
+
const typeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
8076
|
+
if (typeDefs.length > 0) {
|
|
8077
|
+
for (const d of matches) {
|
|
8078
|
+
const origin = _resolveFlowTypeOrigin(index, d.file || filePath, head);
|
|
8079
|
+
if (!origin) return null;
|
|
8080
|
+
origins.add(origin.fromFile);
|
|
8081
|
+
if (origins.size > 1) return null;
|
|
8082
|
+
}
|
|
8083
|
+
}
|
|
8084
|
+
const fromFile = origins.size === 1 ? [...origins][0] : undefined;
|
|
8085
|
+
return { type: head, ...(fromFile && { fromFile }) };
|
|
6596
8086
|
}
|
|
6597
8087
|
// self/this/cls receiver: resolve through the enclosing class (+ walk).
|
|
6598
8088
|
if (record.isMethod && ['self', 'this', 'cls'].includes(record.receiver)) {
|
|
@@ -6626,7 +8116,8 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6626
8116
|
if (!rt && record.receiverCall && (!record.receiver || record.receiverIsChainRoot)) {
|
|
6627
8117
|
rt = _foldChainedReceiverType(index, fileEntry, filePath, record, ctx);
|
|
6628
8118
|
}
|
|
6629
|
-
if (!rt && record.receiver && !record.receiverIsChainRoot
|
|
8119
|
+
if (!rt && record.receiver && !record.receiverIsChainRoot &&
|
|
8120
|
+
!record.receiverPatternShadow && !record.receiverFlowInvalidated) {
|
|
6630
8121
|
const flowMap = ctx.getFlowMap();
|
|
6631
8122
|
const fe = flowMap && _lookupReturnTypeFlow(flowMap, record);
|
|
6632
8123
|
if (fe && fe.externalVia) return { externalVia: fe.externalVia };
|
|
@@ -6637,6 +8128,7 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6637
8128
|
return _methodReturnOnType(index, rt.type, rt.fromFile, name, language,
|
|
6638
8129
|
{ filePath, consumerAwaited });
|
|
6639
8130
|
}
|
|
8131
|
+
if (nominal) return null;
|
|
6640
8132
|
// One-hop agreement (the #207/#219 discipline, one level deeper):
|
|
6641
8133
|
// every method owner project-wide annotated and agreeing.
|
|
6642
8134
|
const methodDefs = (index.symbols.get(name) || [])
|
|
@@ -6657,14 +8149,27 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6657
8149
|
if (language === 'python' && !consumerAwaited && methodDefs.some(d => d.isAsync)) return null;
|
|
6658
8150
|
const heads = new Set();
|
|
6659
8151
|
for (const d of methodDefs) {
|
|
6660
|
-
const h = _structuralTypeHead(d.returnType, {
|
|
8152
|
+
const h = _structuralTypeHead(d.returnType, {
|
|
8153
|
+
unwrapAsync: consumerAwaited, index, language, originFile: d.file,
|
|
8154
|
+
});
|
|
6661
8155
|
if (!h) return null;
|
|
6662
8156
|
heads.add(h);
|
|
6663
8157
|
if (heads.size > 1) return null;
|
|
6664
8158
|
}
|
|
6665
8159
|
const head = [...heads][0];
|
|
6666
8160
|
if (/^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
|
|
6667
|
-
|
|
8161
|
+
const origins = new Set();
|
|
8162
|
+
const typeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
8163
|
+
if (typeDefs.length > 0) {
|
|
8164
|
+
for (const d of methodDefs) {
|
|
8165
|
+
const origin = _resolveFlowTypeOrigin(index, d.file || filePath, head);
|
|
8166
|
+
if (!origin) return null;
|
|
8167
|
+
origins.add(origin.fromFile);
|
|
8168
|
+
if (origins.size > 1) return null;
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
const fromFile = origins.size === 1 ? [...origins][0] : undefined;
|
|
8172
|
+
return { type: head, ...(fromFile && { fromFile }) };
|
|
6668
8173
|
}
|
|
6669
8174
|
// Plain producer: Go same-package only; others unique-project-def with
|
|
6670
8175
|
// same-file narrowing; Java bare calls reach the enclosing class first.
|
|
@@ -6698,8 +8203,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6698
8203
|
return { type: parsed.name, ...(origin.fromFile && { fromFile: origin.fromFile }) };
|
|
6699
8204
|
}
|
|
6700
8205
|
if (language === 'python' && !consumerAwaited && chosen.isAsync) return null;
|
|
6701
|
-
let head = _structuralTypeHead(chosen.returnType, {
|
|
8206
|
+
let head = _structuralTypeHead(chosen.returnType, {
|
|
8207
|
+
unwrapAsync: consumerAwaited, index, language, originFile: chosen.file,
|
|
8208
|
+
});
|
|
6702
8209
|
if (!head || /^[A-Z][A-Z0-9]?$/.test(head) || _STRUCTURAL_FLOW_REJECT.has(head)) return null;
|
|
8210
|
+
const typeDefs = (index.symbols.get(head) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
8211
|
+
if (typeDefs.length > 0) {
|
|
8212
|
+
const origin = _resolveFlowTypeOrigin(index, chosen.file || filePath, head);
|
|
8213
|
+
if (!origin) return null;
|
|
8214
|
+
return { type: head, ...(origin.fromFile && { fromFile: origin.fromFile }) };
|
|
8215
|
+
}
|
|
6703
8216
|
return { type: head };
|
|
6704
8217
|
}
|
|
6705
8218
|
|
|
@@ -6712,17 +8225,41 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
6712
8225
|
*/
|
|
6713
8226
|
function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
|
|
6714
8227
|
if (!call.receiverCall || !call.receiverCallLine || !ctx.records) return null;
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
8228
|
+
// A large builder-style function may contain hundreds of chained calls.
|
|
8229
|
+
// Scanning every record for every hop made one function O(calls²). Build
|
|
8230
|
+
// a context-local producer lookup once, then resolve each hop directly.
|
|
8231
|
+
if (!ctx.producerIndex) {
|
|
8232
|
+
ctx.producerIndex = new Map();
|
|
8233
|
+
for (const record of ctx.records) {
|
|
8234
|
+
if (record.isMacro || record.inMacro) continue;
|
|
8235
|
+
const key = `${record.line}\x00${record.name}\x00${record.isMethod ? 1 : 0}`;
|
|
8236
|
+
let group = ctx.producerIndex.get(key);
|
|
8237
|
+
if (!group) {
|
|
8238
|
+
group = [];
|
|
8239
|
+
ctx.producerIndex.set(key, group);
|
|
8240
|
+
}
|
|
8241
|
+
group.push(record);
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
// Kind match: the consumer knows whether its producer was a method-shaped
|
|
8245
|
+
// call. `.arg(arg("x"))` has both a chained method `arg` and a plain
|
|
8246
|
+
// closure call `arg` on one line; only the right kind folds.
|
|
8247
|
+
const producerKey = `${call.receiverCallLine}\x00${call.receiverCall}\x00${call.receiverCallIsMethod ? 1 : 0}`;
|
|
8248
|
+
const prods = (ctx.producerIndex.get(producerKey) || []).filter(r => r !== call);
|
|
6722
8249
|
if (prods.length === 0) return null;
|
|
6723
8250
|
const results = prods.map(r =>
|
|
6724
8251
|
_typeOfCallResultFold(index, fileEntry, filePath, r, ctx, call.receiverCallAwaited));
|
|
6725
|
-
if (!results.every(Boolean))
|
|
8252
|
+
if (!results.every(Boolean)) {
|
|
8253
|
+
// A module owns its exported name. If module-qualified producer
|
|
8254
|
+
// resolution cannot type `z.number()`, the global same-name method
|
|
8255
|
+
// fallback is not allowed to borrow Mocker.number (or another
|
|
8256
|
+
// module/version) as its return type. Keep the consumer untyped and
|
|
8257
|
+
// visible instead of manufacturing exclusion-grade evidence.
|
|
8258
|
+
if (prods.some(r => r.receiverIsModule || _isStructuralImportReceiver(fileEntry, r))) {
|
|
8259
|
+
return { suppressFallback: true };
|
|
8260
|
+
}
|
|
8261
|
+
return null;
|
|
8262
|
+
}
|
|
6726
8263
|
if (results.every(r => r.externalVia)) return { externalVia: results[0].externalVia };
|
|
6727
8264
|
if (results.some(r => r.externalVia)) return null;
|
|
6728
8265
|
if (new Set(results.map(r => r.type)).size !== 1) return null;
|
|
@@ -6730,6 +8267,16 @@ function _foldChainedReceiverType(index, fileEntry, filePath, call, ctx) {
|
|
|
6730
8267
|
return { type: results[0].type, ...(fromFiles.size === 1 && results[0].fromFile && { fromFile: results[0].fromFile }) };
|
|
6731
8268
|
}
|
|
6732
8269
|
|
|
8270
|
+
// A lower-case named import can be a namespace-like API object (`z.string()`)
|
|
8271
|
+
// even when the parser cannot label it receiverIsModule. Its terminal member
|
|
8272
|
+
// is still owned by that import; it must never borrow a same-named method's
|
|
8273
|
+
// return annotation from an unrelated class. Capitalized named imports remain
|
|
8274
|
+
// eligible for class/static-method resolution.
|
|
8275
|
+
function _isStructuralImportReceiver(fileEntry, record) {
|
|
8276
|
+
if (!record?.receiver || !/^[a-z_$]/.test(record.receiver)) return false;
|
|
8277
|
+
return (fileEntry?.importBindings || []).some(b => b.name === record.receiver);
|
|
8278
|
+
}
|
|
8279
|
+
|
|
6733
8280
|
/**
|
|
6734
8281
|
* Is this field symbol callable by its own name (obj.f(...) reaches the
|
|
6735
8282
|
* field's function value)? Arrow-function class fields are callable by
|
|
@@ -6759,12 +8306,12 @@ function _buildTypedLocalTypeMap(index, def, calls) {
|
|
|
6759
8306
|
for (const call of calls) {
|
|
6760
8307
|
if (call.line < def.startLine || call.line > def.endLine) continue;
|
|
6761
8308
|
|
|
6762
|
-
//
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
8309
|
+
// Parser receiverType evidence belongs to that exact call record and
|
|
8310
|
+
// is consumed directly by findCallers/findCallees. Do not smear it
|
|
8311
|
+
// across the whole function: block shadowing (`if let Some(v) = ...`)
|
|
8312
|
+
// and reassignment can reuse the same variable name with a different
|
|
8313
|
+
// type. This map is reserved for explicit constructor-result inference
|
|
8314
|
+
// below, where the producer assignment is the evidence source.
|
|
6768
8315
|
|
|
6769
8316
|
// Collect types from constructor calls: x := NewFoo() → x maps to Foo
|
|
6770
8317
|
// Handles: x := NewFoo(), x, err := NewFoo(), x := pkg.NewFoo(), x, err := pkg.NewFoo()
|