ucn 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +4 -1
package/core/callers.js
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const { detectLanguage, getParser, getLanguageModule, langTraits } = require('../languages');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
|
-
const { NON_CALLABLE_TYPES, isOverrideMarked } = require('./shared');
|
|
13
|
+
const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath } = require('./shared');
|
|
14
14
|
const { scoreEdge, tierForResolution, TIER } = require('./confidence');
|
|
15
15
|
const { findGoModule } = require('./imports');
|
|
16
16
|
|
|
@@ -460,6 +460,17 @@ function findCallers(index, name, options = {}) {
|
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
// Intra-class constructor mechanics are never caller edges
|
|
464
|
+
// (fix #238, jdtls-measured): an enum CONSTANT is part of the
|
|
465
|
+
// enum's own declaration (JsonToken's 10 constants confirmed
|
|
466
|
+
// 10 self-callers), and a `this(...)` delegation names the
|
|
467
|
+
// ENCLOSING class by construction. Both stay in the calls
|
|
468
|
+
// cache for deadcode/--unused reachability; `super(...)`
|
|
469
|
+
// names a DIFFERENT class and keeps its caller edge.
|
|
470
|
+
if (call.enumConstant || call.ctorDelegation === 'this') {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
463
474
|
// For potential callbacks (function passed as arg), validate against symbol table
|
|
464
475
|
// and skip complex binding resolution — just check the name exists
|
|
465
476
|
if (call.isPotentialCallback) {
|
|
@@ -623,7 +634,8 @@ function findCallers(index, name, options = {}) {
|
|
|
623
634
|
const cbSameFile = cbTargetFiles.has(filePath);
|
|
624
635
|
const cbSamePackage = !cbSameFile &&
|
|
625
636
|
langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
|
|
626
|
-
cbTargetDefs.some(d => d.file &&
|
|
637
|
+
cbTargetDefs.some(d => d.file &&
|
|
638
|
+
_sameNominalPackageDir(path.dirname(d.file), path.dirname(filePath), fileEntry.language));
|
|
627
639
|
let cbImportLink = false;
|
|
628
640
|
if (!cbSameFile && !cbSamePackage) {
|
|
629
641
|
const cbImports = index.importGraph.get(filePath);
|
|
@@ -701,10 +713,14 @@ function findCallers(index, name, options = {}) {
|
|
|
701
713
|
// e.g., analyzer.analyze_instrument() should NOT resolve to a local
|
|
702
714
|
// standalone function def `analyze_instrument` — they're different symbols.
|
|
703
715
|
// Also skip for Go package-qualified calls (isMethod:false but has receiver like 'cli')
|
|
704
|
-
|
|
716
|
+
// `super` skips too (fix #238): a super call targets the PARENT
|
|
717
|
+
// class's member by definition — the local binding of the name
|
|
718
|
+
// is the enclosing class's own member, provably the wrong def
|
|
719
|
+
// (super(config) bound to the subclass's OWN constructor).
|
|
720
|
+
// Super records resolve only through the parent-chain walk.
|
|
721
|
+
const selfReceivers = new Set(['self', 'cls', 'this']);
|
|
705
722
|
const skipLocalBinding = call.receiver && !selfReceivers.has(call.receiver);
|
|
706
723
|
if (!bindingId && !skipLocalBinding) {
|
|
707
|
-
let bindings = (fileEntry.bindings || []).filter(b => b.name === call.name);
|
|
708
724
|
// A bare call cannot bind to a METHOD def where bare names
|
|
709
725
|
// never reach methods (fix #220, cobra-measured): Go's
|
|
710
726
|
// func (c *Command) MarkFlagDirname and func MarkFlagDirname
|
|
@@ -717,15 +733,24 @@ function findCallers(index, name, options = {}) {
|
|
|
717
733
|
// cells.cell_len, not Text.cell_len) and a JS class
|
|
718
734
|
// member is not a file binding either — the structural
|
|
719
735
|
// dispatch gates can't own this case because a matched
|
|
720
|
-
// bindingId bypasses them.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
736
|
+
// bindingId bypasses them. Fix #229: the filter applies
|
|
737
|
+
// per source file — sibling-file bindings from Go's
|
|
738
|
+
// package-scope concat below carry interface members and
|
|
739
|
+
// methods too (`type Notifier interface { Notify() }`
|
|
740
|
+
// next to `func Notify()` stole the bindingId and
|
|
741
|
+
// excluded the true caller as other-definition).
|
|
742
|
+
const bareNeverMethod = !call.isMethod &&
|
|
743
|
+
!langTraits(fileEntry.language)?.bareCallReachesMethods;
|
|
744
|
+
const defsOfName = bareNeverMethod ? (index.symbols.get(call.name) || []) : null;
|
|
745
|
+
const dropMethodBindings = (list, file) => {
|
|
746
|
+
if (!bareNeverMethod || list.length === 0) return list;
|
|
747
|
+
return list.filter(b => {
|
|
748
|
+
const sym = defsOfName.find(s => s.file === file && s.startLine === b.startLine);
|
|
726
749
|
return !(sym && (sym.className || sym.receiver));
|
|
727
750
|
});
|
|
728
|
-
}
|
|
751
|
+
};
|
|
752
|
+
let bindings = dropMethodBindings(
|
|
753
|
+
(fileEntry.bindings || []).filter(b => b.name === call.name), filePath);
|
|
729
754
|
// For Go, also check sibling files in same directory (same package scope)
|
|
730
755
|
if (bindings.length === 0 && langTraits(fileEntry.language)?.packageScope === 'directory') {
|
|
731
756
|
const dir = path.dirname(filePath);
|
|
@@ -734,7 +759,8 @@ function findCallers(index, name, options = {}) {
|
|
|
734
759
|
if (fp !== filePath) {
|
|
735
760
|
const fe = index.files.get(fp);
|
|
736
761
|
if (fe) {
|
|
737
|
-
const sibling = (
|
|
762
|
+
const sibling = dropMethodBindings(
|
|
763
|
+
(fe.bindings || []).filter(b => b.name === call.name), fp);
|
|
738
764
|
bindings = bindings.concat(sibling);
|
|
739
765
|
}
|
|
740
766
|
}
|
|
@@ -846,7 +872,7 @@ function findCallers(index, name, options = {}) {
|
|
|
846
872
|
const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
|
|
847
873
|
if (!callerSymbol?.className) {
|
|
848
874
|
// Can't resolve — include only if includeMethods requested
|
|
849
|
-
if (!options.includeMethods) {
|
|
875
|
+
if (options.collectAccount || !options.includeMethods) {
|
|
850
876
|
routeUnverified(filePath, fileEntry, call, 'method-no-evidence', calledAs);
|
|
851
877
|
continue;
|
|
852
878
|
}
|
|
@@ -876,16 +902,23 @@ function findCallers(index, name, options = {}) {
|
|
|
876
902
|
continue;
|
|
877
903
|
}
|
|
878
904
|
resolvedBySameClass = true;
|
|
879
|
-
} else if (!options.includeMethods) {
|
|
905
|
+
} else if (options.collectAccount || !options.includeMethods) {
|
|
880
906
|
routeUnverified(filePath, fileEntry, call, 'method-no-evidence', calledAs);
|
|
881
907
|
continue;
|
|
882
908
|
}
|
|
883
909
|
}
|
|
884
|
-
} else if (['self', 'cls', 'this', 'super'].includes(call.receiver)
|
|
885
|
-
|
|
910
|
+
} else if (['self', 'cls', 'this', 'super'].includes(call.receiver) ||
|
|
911
|
+
(call.receiver === 'Self' && fileEntry.language === 'rust')) {
|
|
912
|
+
// self/this/super.method() — resolve to same-class or parent method.
|
|
913
|
+
// Rust `Self::method()` (fix #232) is the path-call same-class form:
|
|
914
|
+
// Self IS the enclosing impl's type, so the #202b pinning check
|
|
915
|
+
// confirms it for the impl's class and excludes it for a pinned
|
|
916
|
+
// sibling — it must never reach the uppercase path-receiver
|
|
917
|
+
// discipline below (which excluded it as path-type-mismatch
|
|
918
|
+
// whenever the method name had several project-wide owners).
|
|
886
919
|
const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
|
|
887
920
|
if (!callerSymbol?.className) {
|
|
888
|
-
if (!options.includeMethods) {
|
|
921
|
+
if (options.collectAccount || !options.includeMethods) {
|
|
889
922
|
routeUnverified(filePath, fileEntry, call, 'method-no-evidence', calledAs);
|
|
890
923
|
continue;
|
|
891
924
|
}
|
|
@@ -937,10 +970,16 @@ function findCallers(index, name, options = {}) {
|
|
|
937
970
|
fileEntry.language === 'python') {
|
|
938
971
|
const tDefs = options.targetDefinitions || definitions;
|
|
939
972
|
const targetClasses = new Set(tDefs.map(d => d.className).filter(Boolean));
|
|
973
|
+
// `super` dispatches statically UP the chain — the
|
|
974
|
+
// ancestor/descendant dynamic-dispatch exemptions
|
|
975
|
+
// are inverted for it: a super call can never bind
|
|
976
|
+
// a def below the matched parent (fix #238).
|
|
977
|
+
const superSkipsExemptions = call.receiver === 'super';
|
|
940
978
|
if (!targetClasses.has(matchedClass) &&
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
979
|
+
(superSkipsExemptions ||
|
|
980
|
+
(!_isAncestorOfTargetClass(index, matchedClass, tDefs) &&
|
|
981
|
+
!(fileEntry.language === 'python' &&
|
|
982
|
+
_shareProjectDescendant(index, matchedClass, targetClasses))))) {
|
|
944
983
|
recordExcluded(filePath, call.line, 'other-definition');
|
|
945
984
|
continue;
|
|
946
985
|
}
|
|
@@ -973,15 +1012,20 @@ function findCallers(index, name, options = {}) {
|
|
|
973
1012
|
// Legacy keeps confirming (drop-vs-route asymmetry).
|
|
974
1013
|
const tDefs = options.targetDefinitions || definitions;
|
|
975
1014
|
const targetClasses = new Set(tDefs.map(d => d.className).filter(Boolean));
|
|
1015
|
+
// super: static upward dispatch — no ancestor/
|
|
1016
|
+
// descendant exemptions (fix #238), routed visible
|
|
1017
|
+
// like the rest of the #213 branch (TS declare-class
|
|
1018
|
+
// merging can hide the true parent edge).
|
|
976
1019
|
if (!targetClasses.has(matchedClass) &&
|
|
977
|
-
|
|
978
|
-
|
|
1020
|
+
(call.receiver === 'super' ||
|
|
1021
|
+
(!_isAncestorOfTargetClass(index, matchedClass, tDefs) &&
|
|
1022
|
+
!_shareProjectDescendant(index, matchedClass, targetClasses)))) {
|
|
979
1023
|
routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs);
|
|
980
1024
|
continue;
|
|
981
1025
|
}
|
|
982
1026
|
}
|
|
983
1027
|
resolvedBySameClass = true;
|
|
984
|
-
} else if (!options.includeMethods) {
|
|
1028
|
+
} else if (options.collectAccount || !options.includeMethods) {
|
|
985
1029
|
routeUnverified(filePath, fileEntry, call, 'method-no-evidence', calledAs);
|
|
986
1030
|
continue;
|
|
987
1031
|
}
|
|
@@ -1037,8 +1081,23 @@ function findCallers(index, name, options = {}) {
|
|
|
1037
1081
|
fieldDispatchType = _declaredFieldInterfaceType(index, fieldHopRootType, call.receiverField, fileEntry.language);
|
|
1038
1082
|
}
|
|
1039
1083
|
|
|
1040
|
-
// Skip uncertain calls unless resolved by same-class matching or
|
|
1041
|
-
|
|
1084
|
+
// Skip uncertain calls unless resolved by same-class matching or
|
|
1085
|
+
// explicitly requested. A declared-field hop type (fix #219) or
|
|
1086
|
+
// interface-field dispatch type IS receiver evidence — those
|
|
1087
|
+
// calls flow to the receiver-class disambiguation below, which
|
|
1088
|
+
// confirms (receiver-hint), excludes (mismatch), or attributes
|
|
1089
|
+
// possible-dispatch. Before fix #229 this gate fired first
|
|
1090
|
+
// whenever the method name had no same-file binding, so the
|
|
1091
|
+
// tier of `this.logger.info()` depended on file LAYOUT (same
|
|
1092
|
+
// file confirmed, cross-file routed method-no-evidence).
|
|
1093
|
+
// A parser-typed receiver defers too (fix #232): `b?.ping()`
|
|
1094
|
+
// carries the optionality `uncertain` flag AND receiverType 'A'
|
|
1095
|
+
// — the ?. is a null guard, not evidence uncertainty, so the
|
|
1096
|
+
// record gets plain-call physics (validated match confirms,
|
|
1097
|
+
// trusted mismatch excludes). Bare `foo?.()` has no receiver
|
|
1098
|
+
// evidence and keeps routing here.
|
|
1099
|
+
if (isUncertain && !resolvedBySameClass && !options.includeUncertain &&
|
|
1100
|
+
!fieldHopType && !fieldDispatchType && !call.receiverType) {
|
|
1042
1101
|
if (stats) stats.uncertain = (stats.uncertain || 0) + 1;
|
|
1043
1102
|
routeUnverified(filePath, fileEntry, call,
|
|
1044
1103
|
call.isMethod ? 'method-no-evidence' : 'ambiguous-binding', calledAs);
|
|
@@ -1239,13 +1298,32 @@ function findCallers(index, name, options = {}) {
|
|
|
1239
1298
|
recordExcluded(filePath, call.line, 'method-kind-mismatch');
|
|
1240
1299
|
continue;
|
|
1241
1300
|
}
|
|
1242
|
-
if (!call.isMethod && targetHasClass
|
|
1243
|
-
|
|
1301
|
+
if (!call.isMethod && targetHasClass &&
|
|
1302
|
+
!(!call.receiver && langTraits(fileEntry.language)?.bareCallReachesMethods)) {
|
|
1303
|
+
// Non-method call but target is a class method — skip.
|
|
1304
|
+
// Bare-call direction honors bareCallReachesMethods
|
|
1305
|
+
// (fix #229, same as the callback-path twin): a Java
|
|
1306
|
+
// bare call CAN denote a method — static imports
|
|
1307
|
+
// (`import static app.U.twice; twice(21)`) and
|
|
1308
|
+
// inherited implicit this-calls. Package-qualified
|
|
1309
|
+
// calls (receiver set) keep the exclusion.
|
|
1244
1310
|
recordExcluded(filePath, call.line, 'method-kind-mismatch');
|
|
1245
1311
|
continue;
|
|
1246
1312
|
}
|
|
1247
1313
|
}
|
|
1248
1314
|
|
|
1315
|
+
// From-import submodule receiver (fix #224): `from . import
|
|
1316
|
+
// jobs` + `jobs.submit(...)` — the receiver resolves to a
|
|
1317
|
+
// project module FILE (graph-build composed the submodule
|
|
1318
|
+
// specifier), so it behaves as a module receiver below.
|
|
1319
|
+
// Confirm/route-enabling only: the class-method exclusion
|
|
1320
|
+
// branch keeps its parser-marked receiverIsModule condition
|
|
1321
|
+
// (a rare package attribute/submodule name collision must not
|
|
1322
|
+
// become exclusion evidence).
|
|
1323
|
+
const recvSubmoduleRel = (!call.receiverIsModule && call.isMethod && call.receiver &&
|
|
1324
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural')
|
|
1325
|
+
? _submoduleReceiverModule(index, fileEntry, call.receiver) : null;
|
|
1326
|
+
|
|
1249
1327
|
// Module receiver: httpx.get() / ns.helper() dispatches to a
|
|
1250
1328
|
// module export — it can never be a CLASS METHOD call. Applies
|
|
1251
1329
|
// only when every target is a class method; standalone-function
|
|
@@ -1275,7 +1353,8 @@ function findCallers(index, name, options = {}) {
|
|
|
1275
1353
|
// target (directly or one re-export hop) → visible, not
|
|
1276
1354
|
// excluded (deep barrel chains exceed the hop budget);
|
|
1277
1355
|
// unresolved-but-project-looking → visible (resolver gap).
|
|
1278
|
-
if (!bindingId && !resolvedBySameClass && call.isMethod &&
|
|
1356
|
+
if (!bindingId && !resolvedBySameClass && call.isMethod &&
|
|
1357
|
+
(call.receiverIsModule || recvSubmoduleRel) &&
|
|
1279
1358
|
call.receiver && langTraits(fileEntry.language)?.typeSystem === 'structural' &&
|
|
1280
1359
|
(fileEntry.importBindings || []).length > 0) {
|
|
1281
1360
|
const recvBindings = fileEntry.importBindings.filter(b => b.name === call.receiver);
|
|
@@ -1284,7 +1363,8 @@ function findCallers(index, name, options = {}) {
|
|
|
1284
1363
|
let reaches = false;
|
|
1285
1364
|
let projectish = false;
|
|
1286
1365
|
for (const b of recvBindings) {
|
|
1287
|
-
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]
|
|
1366
|
+
const rel = (fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]) ||
|
|
1367
|
+
recvSubmoduleRel;
|
|
1288
1368
|
if (!rel) {
|
|
1289
1369
|
const mod = String(b.module);
|
|
1290
1370
|
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
@@ -1448,15 +1528,17 @@ function findCallers(index, name, options = {}) {
|
|
|
1448
1528
|
const targetTypes = dispatchTargetTypes(targetDefs);
|
|
1449
1529
|
if (targetTypes.size > 0) {
|
|
1450
1530
|
// Use inferred receiverType when available (Go/Java/Rust parameter type tracking)
|
|
1451
|
-
// Generic type parameters
|
|
1452
|
-
//
|
|
1453
|
-
// typed 'T' neither validates
|
|
1454
|
-
//
|
|
1455
|
-
// T may be instantiated with anything
|
|
1456
|
-
//
|
|
1531
|
+
// Generic type parameters are not type identity in
|
|
1532
|
+
// EITHER direction (fix #220, made precise by #229):
|
|
1533
|
+
// a receiver typed 'T' or 'TStore' neither validates
|
|
1534
|
+
// against a blanket-impl target nor excludes a
|
|
1535
|
+
// concrete one — T may be instantiated with anything,
|
|
1536
|
+
// including the target class. Declared-in-enclosing-
|
|
1537
|
+
// scope check first (`fn f<TStore: Wipe>(t: &TStore)`
|
|
1538
|
+
// shadows even a same-named project type), 1-2-char
|
|
1539
|
+
// ALL-CAPS convention as fallback.
|
|
1457
1540
|
let knownType = call.receiverType || fieldHopType;
|
|
1458
|
-
if (knownType &&
|
|
1459
|
-
!(index.symbols.get(knownType) || []).some(d => IDENTITY_TYPE_KINDS.has(d.type))) knownType = null;
|
|
1541
|
+
if (knownType && _isGenericParamReceiverType(index, filePath, call.line, knownType)) knownType = null;
|
|
1460
1542
|
if (knownType) {
|
|
1461
1543
|
const viaFieldHop = !call.receiverType; // declared-field hop (fix #202)
|
|
1462
1544
|
// Exclusion requires an UNRELATED type. A receiver typed
|
|
@@ -1575,7 +1657,14 @@ function findCallers(index, name, options = {}) {
|
|
|
1575
1657
|
localTypeCache.set(cacheKey, localTypes);
|
|
1576
1658
|
}
|
|
1577
1659
|
if (localTypes) {
|
|
1578
|
-
|
|
1660
|
+
// The inference map re-derives receiver types
|
|
1661
|
+
// from the calls cache — the same generic-param
|
|
1662
|
+
// guard applies (fix #229: `t.wipe()` on
|
|
1663
|
+
// `t: &T` used to re-infer 'T' here and
|
|
1664
|
+
// exclude after the typed branch nulled it).
|
|
1665
|
+
let inferredType = localTypes.get(call.receiver);
|
|
1666
|
+
if (inferredType && _isGenericParamReceiverType(
|
|
1667
|
+
index, filePath, call.line, inferredType)) inferredType = null;
|
|
1579
1668
|
if (inferredType) {
|
|
1580
1669
|
if (targetTypes.has(inferredType)) {
|
|
1581
1670
|
// Identity discipline (fix #206) — same as the
|
|
@@ -1661,8 +1750,11 @@ function findCallers(index, name, options = {}) {
|
|
|
1661
1750
|
}
|
|
1662
1751
|
if (!matchesTarget) {
|
|
1663
1752
|
// Rust/Go path calls (Type::method() / pkg.Method()): receiver IS the type name
|
|
1664
|
-
// If it doesn't match target, it's definitely a different type — filter it
|
|
1665
|
-
|
|
1753
|
+
// If it doesn't match target, it's definitely a different type — filter it.
|
|
1754
|
+
// `Self` exempt (fix #232, the #222(2) rule): Self names the
|
|
1755
|
+
// enclosing impl's type, not a foreign one — same-class
|
|
1756
|
+
// resolution above owns it.
|
|
1757
|
+
if (call.isPathCall && /^[A-Z]/.test(call.receiver) && call.receiver !== 'Self') {
|
|
1666
1758
|
isUncertain = true;
|
|
1667
1759
|
typeMismatch = true;
|
|
1668
1760
|
if (collectAccount) {
|
|
@@ -1724,12 +1816,35 @@ function findCallers(index, name, options = {}) {
|
|
|
1724
1816
|
// syntactic args is not evidence in Go — only too-many prunes.
|
|
1725
1817
|
// Binding/same-class evidence outranks the count (then a
|
|
1726
1818
|
// mismatch more likely means our param parse is wrong).
|
|
1819
|
+
let arityNoFit = false;
|
|
1727
1820
|
if (collectAccount && !bindingId && !resolvedBySameClass &&
|
|
1728
1821
|
call.argCount != null && !call.argSpread &&
|
|
1729
1822
|
langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
|
|
1730
1823
|
!_callArityCompatible(call, targetDefs, fileEntry.language)) {
|
|
1731
|
-
|
|
1732
|
-
|
|
1824
|
+
// Fits-elsewhere carve-out (fix #229): "binds a different
|
|
1825
|
+
// symbol" needs a different symbol the call COULD bind.
|
|
1826
|
+
// When the argument count also fits no OTHER callable def
|
|
1827
|
+
// of the name project-wide, a wrong-arity call at an
|
|
1828
|
+
// EVIDENCE-BACKED site is a BROKEN CALL SITE (or a parse
|
|
1829
|
+
// gap) — the thing verify/diff-impact exist to surface
|
|
1830
|
+
// after a signature change. Marked and allowed to flow:
|
|
1831
|
+
// receiver/type-qualified evidence confirms it into
|
|
1832
|
+
// verify's arg-check (mismatch band). The exclusion stays
|
|
1833
|
+
// when a sibling overload or another definition fits the
|
|
1834
|
+
// count (jdtls-measured), and the dispatch gate below
|
|
1835
|
+
// re-excludes marked calls that would only confirm via the
|
|
1836
|
+
// single-owner rule — that rule presumes no other
|
|
1837
|
+
// candidate, which the wrong arity disproves toward
|
|
1838
|
+
// EXTERNAL code (Arrays.asList(1,2,3) vs a project 0-param
|
|
1839
|
+
// asList: unique project ownership is not evidence here).
|
|
1840
|
+
const pinnedKeys = new Set(targetDefs.map(d => `${d.file}:${d.startLine}`));
|
|
1841
|
+
const otherDefs = definitions.filter(d =>
|
|
1842
|
+
!NON_CALLABLE_TYPES.has(d.type) && !pinnedKeys.has(`${d.file}:${d.startLine}`));
|
|
1843
|
+
if (otherDefs.length > 0 && _callArityCompatible(call, otherDefs, fileEntry.language)) {
|
|
1844
|
+
recordExcluded(filePath, call.line, 'arity-mismatch');
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
arityNoFit = true;
|
|
1733
1848
|
}
|
|
1734
1849
|
|
|
1735
1850
|
// Overload discipline (fix #205, languages with arity/type
|
|
@@ -1797,7 +1912,8 @@ function findCallers(index, name, options = {}) {
|
|
|
1797
1912
|
// evidence, not a bare name match.
|
|
1798
1913
|
const hasSamePackageEvidence = !hasImportLink &&
|
|
1799
1914
|
langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
|
|
1800
|
-
targetDefs2.some(d => d.file &&
|
|
1915
|
+
targetDefs2.some(d => d.file &&
|
|
1916
|
+
_sameNominalPackageDir(path.dirname(d.file), path.dirname(filePath), fileEntry.language));
|
|
1801
1917
|
|
|
1802
1918
|
// Possible-dispatch tiering (nominal languages, contract surface
|
|
1803
1919
|
// only): methodCallInclusion='auto' confirms method calls with
|
|
@@ -1975,6 +2091,75 @@ function findCallers(index, name, options = {}) {
|
|
|
1975
2091
|
});
|
|
1976
2092
|
continue;
|
|
1977
2093
|
}
|
|
2094
|
+
// Wrong-arity call that fits no project def (fix #229
|
|
2095
|
+
// carve-out marker): the single-owner rule presumes no
|
|
2096
|
+
// other candidate exists, but the arity disproves the
|
|
2097
|
+
// project-side match — the call binds EXTERNAL code
|
|
2098
|
+
// (Arrays.asList(1,2,3)). Only receiver/type-qualified
|
|
2099
|
+
// evidence may carry a wrong-arity call to the
|
|
2100
|
+
// mismatch band; ownership alone re-excludes here.
|
|
2101
|
+
if (arityNoFit) {
|
|
2102
|
+
recordExcluded(filePath, call.line, 'arity-mismatch');
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// Bare-call name ownership, Java (fix #229): where
|
|
2109
|
+
// bareCallReachesMethods a bare call CAN denote a method —
|
|
2110
|
+
// via a static import or an inherited implicit this-call —
|
|
2111
|
+
// but file-level scope evidence cannot CONFIRM one: a bare
|
|
2112
|
+
// name in Java resolves through the class scope (own +
|
|
2113
|
+
// inherited members) or a static import, never through
|
|
2114
|
+
// package-mate visibility. Confirmed tier: the enclosing
|
|
2115
|
+
// class is a dispatch-capable receiver type for the target
|
|
2116
|
+
// (inherited this-call), or a static import of the name /
|
|
2117
|
+
// wildcard static import resolves to a target file
|
|
2118
|
+
// (compiler-grade name evidence, the #217 rule). A static
|
|
2119
|
+
// import resolving to a DIFFERENT project file owns the name
|
|
2120
|
+
// (other-definition-import); everything else routes VISIBLE
|
|
2121
|
+
// (external static imports and unresolved ancestry are not
|
|
2122
|
+
// exclusion evidence — JLS scope nesting also lets inherited
|
|
2123
|
+
// members shadow imports, so 'unknown' never excludes).
|
|
2124
|
+
if (collectAccount && !call.isMethod && !call.receiver && !bindingId &&
|
|
2125
|
+
!resolvedBySameClass &&
|
|
2126
|
+
langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
|
|
2127
|
+
langTraits(fileEntry.language)?.bareCallReachesMethods &&
|
|
2128
|
+
targetDefs2.length > 0 && targetDefs2.every(d =>
|
|
2129
|
+
!NON_CALLABLE_TYPES.has(d.type) && (d.className || d.receiver))) {
|
|
2130
|
+
const tTypes = dispatchTargetTypes(targetDefs2);
|
|
2131
|
+
const enclosingClass = callerSymbol && callerSymbol.className;
|
|
2132
|
+
if (!(enclosingClass && tTypes.has(enclosingClass))) {
|
|
2133
|
+
const targetFiles = new Set(targetDefs2.map(d => d.file).filter(Boolean));
|
|
2134
|
+
let verdict = null; // 'target' | 'other' | 'unknown'
|
|
2135
|
+
for (const im of (fileEntry.importBindings || [])) {
|
|
2136
|
+
const mod = String(im.module || '');
|
|
2137
|
+
if (im.name === call.name && mod.endsWith('.' + call.name)) {
|
|
2138
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[mod];
|
|
2139
|
+
verdict = rel
|
|
2140
|
+
? (targetFiles.has(path.join(index.root, rel)) ? 'target' : 'other')
|
|
2141
|
+
: 'unknown';
|
|
2142
|
+
break;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (verdict === null && fileEntry.moduleResolved) {
|
|
2146
|
+
for (const [mod, rel] of Object.entries(fileEntry.moduleResolved)) {
|
|
2147
|
+
if (mod.endsWith('.*') && targetFiles.has(path.join(index.root, rel))) {
|
|
2148
|
+
verdict = 'target';
|
|
2149
|
+
break;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (verdict === 'other') {
|
|
2154
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (verdict !== 'target') {
|
|
2158
|
+
routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
|
|
2159
|
+
dispatchCandidates: methodOwnerKeys().size,
|
|
2160
|
+
});
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
1978
2163
|
}
|
|
1979
2164
|
}
|
|
1980
2165
|
|
|
@@ -2046,8 +2231,9 @@ function findCallers(index, name, options = {}) {
|
|
|
2046
2231
|
// Module-qualified calls (z.string(), ns.helper()) are
|
|
2047
2232
|
// exempt: the module IS name-level evidence, and the
|
|
2048
2233
|
// module-ownership block above already routed the ones
|
|
2049
|
-
// whose module doesn't reach the target.
|
|
2050
|
-
|
|
2234
|
+
// whose module doesn't reach the target. Submodule
|
|
2235
|
+
// receivers (fix #224) are module receivers too.
|
|
2236
|
+
if (call.isMethod && !call.receiverIsModule && !recvSubmoduleRel) {
|
|
2051
2237
|
const tTypes = dispatchTargetTypes(targetDefs2);
|
|
2052
2238
|
const typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
|
|
2053
2239
|
// External-producer receiver (fix #222, httpx-measured
|
|
@@ -2064,6 +2250,27 @@ function findCallers(index, name, options = {}) {
|
|
|
2064
2250
|
});
|
|
2065
2251
|
continue;
|
|
2066
2252
|
}
|
|
2253
|
+
// Builtin-global receiver (fix #232, campaign-measured:
|
|
2254
|
+
// console.log() confirmed scope-match against a private
|
|
2255
|
+
// Logger.log — its single project-wide owner). console/
|
|
2256
|
+
// window/process/... name HOST objects, so unique
|
|
2257
|
+
// project ownership is not identity evidence for the
|
|
2258
|
+
// receiver. Shadowing keeps normal physics: a project
|
|
2259
|
+
// def, file binding, or parser-typed receiver of the
|
|
2260
|
+
// name wins. Demote-only (`window.fn = projectFn`
|
|
2261
|
+
// attachment is a real pattern — #222(4) name-knowledge
|
|
2262
|
+
// rule): visible possible-dispatch, never excluded.
|
|
2263
|
+
if (!typeQualifiedReceiver && call.receiver && !call.receiverType &&
|
|
2264
|
+
['javascript', 'typescript', 'tsx', 'html'].includes(fileEntry.language) &&
|
|
2265
|
+
JS_GLOBAL_RECEIVERS.has(call.receiver) &&
|
|
2266
|
+
(index.symbols.get(call.receiver) || []).length === 0 &&
|
|
2267
|
+
!fileEntry.bindings?.some(b => b.name === call.receiver)) {
|
|
2268
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
2269
|
+
dispatchVia: `${call.receiver} — builtin global`,
|
|
2270
|
+
externalContract: true,
|
|
2271
|
+
});
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2067
2274
|
// A method call cannot denote a standalone function
|
|
2068
2275
|
// (fix #218, rich-measured: `console.print(...)`
|
|
2069
2276
|
// confirmed scope-match against module-level print):
|
|
@@ -2072,12 +2279,28 @@ function findCallers(index, name, options = {}) {
|
|
|
2072
2279
|
// receivers are excluded above (#198); untyped ones
|
|
2073
2280
|
// route visible. Module receivers stay exempt
|
|
2074
2281
|
// (rich.print(...) IS the module function).
|
|
2282
|
+
// EXCEPTION (fix #254, W8 BUG-4 — verify's BUG-BX rule
|
|
2283
|
+
// in the engine, range-based): a receiver naming a
|
|
2284
|
+
// namespace/module block that CONTAINS the pinned def
|
|
2285
|
+
// is a qualified function call — containment is
|
|
2286
|
+
// identity evidence, and the #215 scope check ties
|
|
2287
|
+
// the receiver to the containing file. Falls through
|
|
2288
|
+
// to confirm with its import/scope evidence.
|
|
2075
2289
|
if (!typeQualifiedReceiver && targetDefs2.length > 0 &&
|
|
2076
2290
|
targetDefs2.every(d => !d.className && !d.receiver)) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2291
|
+
if (!_namespaceContainedDef(index, fileEntry, filePath,
|
|
2292
|
+
call.receiver, call.name, targetDefs2)) {
|
|
2293
|
+
// Candidates here are the standalone defs the call
|
|
2294
|
+
// MIGHT reach through an unmodeled module receiver
|
|
2295
|
+
// (dynamic import) — methodOwnerKeys counts method
|
|
2296
|
+
// owners only and reported a contradictory 0
|
|
2297
|
+
// (fix #230).
|
|
2298
|
+
routeUnverified(filePath, fileEntry, call, 'method-ambiguous', calledAs, {
|
|
2299
|
+
dispatchCandidates: methodOwnerKeys().size ||
|
|
2300
|
+
targetDefs2.filter(d => !NON_CALLABLE_TYPES.has(d.type)).length,
|
|
2301
|
+
});
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2081
2304
|
}
|
|
2082
2305
|
// External-contract single owner (fix #210): same
|
|
2083
2306
|
// physics as the nominal gate above — an override
|
|
@@ -2123,6 +2346,31 @@ function findCallers(index, name, options = {}) {
|
|
|
2123
2346
|
}
|
|
2124
2347
|
}
|
|
2125
2348
|
|
|
2349
|
+
// Receiver-less counterpart of the gate's arityNoFit guard
|
|
2350
|
+
// (fix #229): a bare/package-qualified wrong-arity call may
|
|
2351
|
+
// reach the mismatch band only on compiler-grade NAME evidence
|
|
2352
|
+
// — an import binding of the name resolving to a target file
|
|
2353
|
+
// (a `use`/static-import pins the name, so the wrong arity is
|
|
2354
|
+
// a broken call site, not another target). Scope-match alone
|
|
2355
|
+
// cannot carry it: the arity disproves the project-side match,
|
|
2356
|
+
// so the call more likely binds code UCN cannot see.
|
|
2357
|
+
if (arityNoFit && !call.isMethod) {
|
|
2358
|
+
let arityNameEvidence = false;
|
|
2359
|
+
const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
2360
|
+
for (const im of (fileEntry.importBindings || [])) {
|
|
2361
|
+
if (im.name !== call.name) continue;
|
|
2362
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[String(im.module || '')];
|
|
2363
|
+
if (rel && tFiles.has(path.join(index.root, rel))) {
|
|
2364
|
+
arityNameEvidence = true;
|
|
2365
|
+
break;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
if (!arityNameEvidence) {
|
|
2369
|
+
recordExcluded(filePath, call.line, 'arity-mismatch');
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2126
2374
|
if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
|
|
2127
2375
|
pendingByFile.get(filePath).push({
|
|
2128
2376
|
call, fileEntry, callerSymbol,
|
|
@@ -2306,7 +2554,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2306
2554
|
// Retained unverified-tier entries, sorted (relativePath, line) per the
|
|
2307
2555
|
// output ordering contract.
|
|
2308
2556
|
unverifiedEntries.sort((a, b) => {
|
|
2309
|
-
if (a.relativePath !== b.relativePath) return a.relativePath
|
|
2557
|
+
if (a.relativePath !== b.relativePath) return codeUnitCompare(a.relativePath, b.relativePath);
|
|
2310
2558
|
return (a.line || 0) - (b.line || 0);
|
|
2311
2559
|
});
|
|
2312
2560
|
Object.defineProperty(callers, 'unverifiedEntries', {
|
|
@@ -2435,6 +2683,14 @@ function findCallees(index, def, options = {}) {
|
|
|
2435
2683
|
localTypes = _buildTypedLocalTypeMap(index, def, calls);
|
|
2436
2684
|
}
|
|
2437
2685
|
|
|
2686
|
+
// Return-type flow map (lazy — only built if a single-owner
|
|
2687
|
+
// resolution needs the external-producer/typed-receiver defeater).
|
|
2688
|
+
let _flowMap;
|
|
2689
|
+
const flowMap = () => {
|
|
2690
|
+
if (_flowMap === undefined) _flowMap = _buildReturnTypeFlowMap(index, def.file, calls);
|
|
2691
|
+
return _flowMap;
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2438
2694
|
let siteOrdinal = -1;
|
|
2439
2695
|
for (const call of calls) {
|
|
2440
2696
|
siteOrdinal++;
|
|
@@ -2453,6 +2709,55 @@ function findCallees(index, def, options = {}) {
|
|
|
2453
2709
|
if (!isDirectMatch && !isNestedCallback) continue;
|
|
2454
2710
|
if (calleeAccount) calleeAccount.totalSites++;
|
|
2455
2711
|
|
|
2712
|
+
// Declared-field receiver hop (fix #231 — callee-side parity
|
|
2713
|
+
// with the caller side's #202/#219): `tm.service.Save()` /
|
|
2714
|
+
// `this._map.has()` records carry receiverRoot/receiverField —
|
|
2715
|
+
// resolve the field's DECLARED type and treat it exactly like a
|
|
2716
|
+
// parser-inferred receiverType. this-rooted structural hops
|
|
2717
|
+
// resolve the root at query time (the enclosing class — arrows
|
|
2718
|
+
// keep lexical `this`; nested function declarations are their
|
|
2719
|
+
// own symbols without className, so dynamic-this shapes resolve
|
|
2720
|
+
// to nothing). _declaredFieldType's guards apply: interface/
|
|
2721
|
+
// trait-typed and generic-param fields return null.
|
|
2722
|
+
let fieldHopType = null;
|
|
2723
|
+
if (call.isMethod && !call.receiverType && call.receiverField) {
|
|
2724
|
+
let hopRoot = call.receiverRootType;
|
|
2725
|
+
if (!hopRoot && call.receiverRoot === 'this' &&
|
|
2726
|
+
langTraits(language)?.typeSystem === 'structural') {
|
|
2727
|
+
hopRoot = index.findEnclosingFunction(def.file, call.line, true)?.className;
|
|
2728
|
+
}
|
|
2729
|
+
if (hopRoot) fieldHopType = _declaredFieldType(index, hopRoot, call.receiverField, language);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// Go package-qualified receiver: resolve the import module up
|
|
2733
|
+
// front so the dispatch chain can tell package calls apart from
|
|
2734
|
+
// type-qualified method expressions (fix #236 — a receiver that
|
|
2735
|
+
// is neither stays eligible for type-qualified resolution below).
|
|
2736
|
+
let goImportModule = null;
|
|
2737
|
+
if (call.isMethod && call.receiver && langTraits(language)?.hasReceiverPackageCalls) {
|
|
2738
|
+
const goImports = fileEntry?.imports || [];
|
|
2739
|
+
// Handle Go version suffixes: k8s.io/klog/v2 → klog, not v2
|
|
2740
|
+
goImportModule = goImports.find(mod => {
|
|
2741
|
+
const parts = mod.split('/');
|
|
2742
|
+
const last = parts[parts.length - 1];
|
|
2743
|
+
const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
2744
|
+
return pkgName === call.receiver;
|
|
2745
|
+
}) || null;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// Type-qualified receiver resolution (fix #236): the receiver
|
|
2749
|
+
// NAMES a type — Foo::new() / Kit.make() / Helper.process().
|
|
2750
|
+
// Only consulted when no stronger evidence (local type, parser
|
|
2751
|
+
// receiverType, field hop, import package) claims the call.
|
|
2752
|
+
let typeQual = null;
|
|
2753
|
+
if (call.isMethod && !call.isConstructor && call.receiver &&
|
|
2754
|
+
!call.receiverType && !fieldHopType && !goImportModule &&
|
|
2755
|
+
!call.receiverIsModule && !call.selfAttribute &&
|
|
2756
|
+
!['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver) &&
|
|
2757
|
+
!(localTypes && localTypes.has(call.receiver))) {
|
|
2758
|
+
typeQual = _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2456
2761
|
// Smart method call handling:
|
|
2457
2762
|
// - Go: include all method calls (Go doesn't use this/self/cls)
|
|
2458
2763
|
// - self/this.method(): resolve to same-class method (handled below)
|
|
@@ -2461,8 +2766,10 @@ function findCallees(index, def, options = {}) {
|
|
|
2461
2766
|
if (call.isMethod) {
|
|
2462
2767
|
if (call.selfAttribute && language === 'python') {
|
|
2463
2768
|
// Will be resolved in second pass below
|
|
2464
|
-
} else if (['self', 'cls', 'this'].includes(call.receiver)
|
|
2769
|
+
} else if (['self', 'cls', 'this'].includes(call.receiver) ||
|
|
2770
|
+
(call.receiver === 'Self' && language === 'rust')) {
|
|
2465
2771
|
// self.method() / cls.method() / this.method() — resolve to same-class method below
|
|
2772
|
+
// Rust Self::method() resolves same-impl the same way (fix #236, the #232 callee analog)
|
|
2466
2773
|
} else if (call.receiver === 'super') {
|
|
2467
2774
|
// super().method() — resolve to parent class method below
|
|
2468
2775
|
} else if (localTypes && localTypes.has(call.receiver)) {
|
|
@@ -2500,18 +2807,29 @@ function findCallees(index, def, options = {}) {
|
|
|
2500
2807
|
callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1,
|
|
2501
2808
|
...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
|
|
2502
2809
|
}
|
|
2810
|
+
} else if (_nonCallableFieldMember(index, typeName, call.name, language)) {
|
|
2811
|
+
// The known receiver type declares the name as its own
|
|
2812
|
+
// non-callable FIELD — a member reference, never a
|
|
2813
|
+
// callee (fix #231: `delete(cs.cache, key)` captured
|
|
2814
|
+
// cs.cache as a method-value callee; cache is
|
|
2815
|
+
// CacheService's map-typed field, which shadows any
|
|
2816
|
+
// same-named project function through this receiver).
|
|
2817
|
+
noteSite(siteId, 'excluded', 'member-reference', call);
|
|
2503
2818
|
} else if (collectAccount) {
|
|
2504
2819
|
// Locally-typed receiver, but the type defines no such
|
|
2505
2820
|
// method in the index — visible, never silently dropped.
|
|
2506
2821
|
noteUnverified(siteId, call, 'uncertain-receiver');
|
|
2507
2822
|
}
|
|
2508
2823
|
continue;
|
|
2509
|
-
} else if (call.receiverType) {
|
|
2824
|
+
} else if (call.receiverType || fieldHopType) {
|
|
2510
2825
|
// Use parser-inferred receiverType for method resolution
|
|
2511
2826
|
// Go/Java/Rust: from param/receiver type declarations
|
|
2512
2827
|
// JS/TS: from `new Foo()` assignments or TypeScript type annotations
|
|
2513
2828
|
// Python: from constructor calls or type annotations
|
|
2514
|
-
|
|
2829
|
+
// fieldHopType: the declared type of a one-hop field
|
|
2830
|
+
// receiver (fix #231 — tm.service.Save() resolves Save
|
|
2831
|
+
// through the `service *DataService` declaration)
|
|
2832
|
+
const typeName = call.receiverType || fieldHopType;
|
|
2515
2833
|
const symbols = index.symbols.get(call.name);
|
|
2516
2834
|
const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
2517
2835
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
@@ -2544,20 +2862,26 @@ function findCallees(index, def, options = {}) {
|
|
|
2544
2862
|
}
|
|
2545
2863
|
continue;
|
|
2546
2864
|
}
|
|
2865
|
+
// No match on the typed receiver. A name the type declares
|
|
2866
|
+
// as its own non-callable FIELD is a member reference,
|
|
2867
|
+
// never a callee (fix #231); a builtin hop type with no
|
|
2868
|
+
// project match is an external call (this._map.has on
|
|
2869
|
+
// `_map: WeakMap<...>` — the #219 caller-side analog).
|
|
2870
|
+
if (_nonCallableFieldMember(index, typeName, call.name, language)) {
|
|
2871
|
+
noteSite(siteId, 'excluded', 'member-reference', call);
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (fieldHopType && BUILTIN_RECEIVER_TYPES.has(typeName)) {
|
|
2875
|
+
noteSite(siteId, 'external', null, call);
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2547
2878
|
// No match found with inferred type — fall through to include as unresolved
|
|
2548
|
-
} else if (
|
|
2879
|
+
} else if (goImportModule) {
|
|
2549
2880
|
// Go package-qualified calls: klog.Infof(), wait.UntilWithContext()
|
|
2550
|
-
//
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
const importModule = goImports.find(mod => {
|
|
2555
|
-
const parts = mod.split('/');
|
|
2556
|
-
const last = parts[parts.length - 1];
|
|
2557
|
-
const pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
2558
|
-
return pkgName === call.receiver;
|
|
2559
|
-
});
|
|
2560
|
-
if (importModule) {
|
|
2881
|
+
// The receiver is an import alias (resolved above) — find
|
|
2882
|
+
// definitions from that package.
|
|
2883
|
+
const importModule = goImportModule;
|
|
2884
|
+
{
|
|
2561
2885
|
// Receiver is an import alias — resolve to definitions from that package
|
|
2562
2886
|
const symbols = index.symbols.get(call.name);
|
|
2563
2887
|
if (symbols) {
|
|
@@ -2600,14 +2924,47 @@ function findCallees(index, def, options = {}) {
|
|
|
2600
2924
|
noteSite(siteId, 'external', null, call);
|
|
2601
2925
|
continue;
|
|
2602
2926
|
}
|
|
2927
|
+
} else if (typeQual) {
|
|
2928
|
+
// Type-qualified receiver (fix #236): the receiver NAMES a
|
|
2929
|
+
// type, so the type owns the call — Foo::new() is Foo's
|
|
2930
|
+
// new; String::new() / Math.max() are external and must
|
|
2931
|
+
// never confirm a project method through a bare name
|
|
2932
|
+
// binding (the caller side excludes the identical edges
|
|
2933
|
+
// as path-type-mismatch — the two directions now agree).
|
|
2934
|
+
if (typeQual.match) {
|
|
2935
|
+
const match = typeQual.match;
|
|
2936
|
+
const key = match.bindingId || `${typeQual.typeName}.${call.name}`;
|
|
2937
|
+
const existing = callees.get(key);
|
|
2938
|
+
if (existing) {
|
|
2939
|
+
existing.count += 1;
|
|
2940
|
+
if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
|
|
2941
|
+
} else {
|
|
2942
|
+
callees.set(key, { name: call.name, bindingId: match.bindingId, count: 1,
|
|
2943
|
+
...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
|
|
2944
|
+
}
|
|
2945
|
+
continue;
|
|
2946
|
+
}
|
|
2947
|
+
if (typeQual.external) {
|
|
2948
|
+
noteSite(siteId, 'external', null, call);
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
noteUnverified(siteId, call, typeQual.unverified);
|
|
2952
|
+
continue;
|
|
2603
2953
|
} else if (langTraits(language)?.methodCallInclusion === 'explicit' && !options.includeMethods) {
|
|
2604
2954
|
noteSite(siteId, 'filtered', 'method-calls-excluded', call);
|
|
2605
2955
|
continue;
|
|
2606
2956
|
}
|
|
2607
2957
|
}
|
|
2608
2958
|
|
|
2609
|
-
// Skip keywords and built-ins
|
|
2610
|
-
|
|
2959
|
+
// Skip keywords and built-ins — EXCEPT self/super-received method
|
|
2960
|
+
// calls, which the same-class/super passes below resolve to real
|
|
2961
|
+
// definitions (fix #238: `super().__init__(x)` and the JS/TS
|
|
2962
|
+
// `super(...)` 'constructor' record were routed external here
|
|
2963
|
+
// because __init__/constructor sit in the builtin name sets).
|
|
2964
|
+
const selfShaped = call.isMethod &&
|
|
2965
|
+
(['self', 'cls', 'this', 'super'].includes(call.receiver) ||
|
|
2966
|
+
(call.receiver === 'Self' && language === 'rust'));
|
|
2967
|
+
if (!selfShaped && index.isKeyword(call.name, language)) {
|
|
2611
2968
|
noteSite(siteId, 'external', null, call);
|
|
2612
2969
|
continue;
|
|
2613
2970
|
}
|
|
@@ -2651,7 +3008,9 @@ function findCallees(index, def, options = {}) {
|
|
|
2651
3008
|
}
|
|
2652
3009
|
|
|
2653
3010
|
// Collect self/this.method() calls for same-class resolution
|
|
2654
|
-
|
|
3011
|
+
// (Rust Self::method() resolves the same way — fix #236)
|
|
3012
|
+
if (call.isMethod && (['self', 'cls', 'this'].includes(call.receiver) ||
|
|
3013
|
+
(call.receiver === 'Self' && language === 'rust'))) {
|
|
2655
3014
|
if (!selfMethodCalls) selfMethodCalls = [];
|
|
2656
3015
|
selfMethodCalls.push({ call, siteId });
|
|
2657
3016
|
continue;
|
|
@@ -2685,6 +3044,25 @@ function findCallees(index, def, options = {}) {
|
|
|
2685
3044
|
// Different strategies by language family:
|
|
2686
3045
|
if (bindings.length === 0 && call.isMethod) {
|
|
2687
3046
|
if (langTraits(language)?.typeSystem === 'structural') {
|
|
3047
|
+
// A KNOWN receiver type routes before any name heuristic
|
|
3048
|
+
// (fix #257 — the caller side's #198 trust rule brought to
|
|
3049
|
+
// findCallees): `canonicalSymbols.set(...)` on a `new Map()`
|
|
3050
|
+
// local resolved exact-binding into a test fixture's
|
|
3051
|
+
// CacheService.set. Builtin-typed receivers are host calls;
|
|
3052
|
+
// a receiver typed to a project class resolves to that class
|
|
3053
|
+
// (or an ancestor defining the method), never by bare name.
|
|
3054
|
+
const route = _calleeReceiverTypeRoute(index, call, localTypes, language);
|
|
3055
|
+
if (route?.external) {
|
|
3056
|
+
noteSite(siteId, 'external', null, call);
|
|
3057
|
+
continue;
|
|
3058
|
+
} else if (route?.resolve) {
|
|
3059
|
+
bindingResolved = route.resolve.bindingId;
|
|
3060
|
+
calleeKey = bindingResolved ||
|
|
3061
|
+
`${route.resolve.className}.${effectiveName}`;
|
|
3062
|
+
} else if (route?.uncertain) {
|
|
3063
|
+
isUncertain = true;
|
|
3064
|
+
uncertainReason = 'uncertain-receiver';
|
|
3065
|
+
} else {
|
|
2688
3066
|
// JS/TS/Python: mark uncertain unless receiver has import/binding
|
|
2689
3067
|
// evidence in file scope AND that binding can plausibly have this method.
|
|
2690
3068
|
// Prevents false positives like m.get() → repository.get() when m is
|
|
@@ -2700,6 +3078,7 @@ function findCallees(index, def, options = {}) {
|
|
|
2700
3078
|
// Functions don't have user-defined methods (return value is unknown)
|
|
2701
3079
|
isUncertain = true;
|
|
2702
3080
|
}
|
|
3081
|
+
}
|
|
2703
3082
|
} else {
|
|
2704
3083
|
// Go/Java/Rust: nominal type systems make single-def method links
|
|
2705
3084
|
// reliable. Only mark uncertain when multiple definitions exist
|
|
@@ -2708,11 +3087,12 @@ function findCallees(index, def, options = {}) {
|
|
|
2708
3087
|
if (defs && defs.length > 1) {
|
|
2709
3088
|
// Go: if receiverType is known, check if it matches exactly one def
|
|
2710
3089
|
// This resolves ambiguity like Framework.Run vs Scheduler.Run
|
|
2711
|
-
const rType = call.receiverType || localTypes?.get(call.receiver);
|
|
3090
|
+
const rType = call.receiverType || fieldHopType || localTypes?.get(call.receiver);
|
|
2712
3091
|
if (rType && langTraits(language)?.typeSystem === 'nominal') {
|
|
2713
3092
|
const matchingDef = defs.find(d =>
|
|
2714
|
-
d.className === rType ||
|
|
2715
|
-
(d.receiver && d.receiver.replace(/^\*/, '') === rType))
|
|
3093
|
+
(d.className === rType ||
|
|
3094
|
+
(d.receiver && d.receiver.replace(/^\*/, '') === rType)) &&
|
|
3095
|
+
_calleeLanguageCompatible(index, d, language));
|
|
2716
3096
|
if (matchingDef) {
|
|
2717
3097
|
// Resolved to specific type — not uncertain
|
|
2718
3098
|
calleeKey = matchingDef.bindingId || `${rType}.${call.name}`;
|
|
@@ -2757,14 +3137,13 @@ function findCallees(index, def, options = {}) {
|
|
|
2757
3137
|
// spraying every same-name def (#223, ripgrep-measured
|
|
2758
3138
|
// on the callee eval arm: HiArgs::from_low_args calls
|
|
2759
3139
|
// three sibling types' from_low_args — every def was
|
|
2760
|
-
// claimed at all three sites).
|
|
2761
|
-
// no class → unchanged full fan-out; bare calls (Java
|
|
3140
|
+
// claimed at all three sites). Bare calls (Java
|
|
2762
3141
|
// implicit-this overloads) keep fanning out.
|
|
2763
3142
|
let otherBindings = bindings.filter(b =>
|
|
2764
3143
|
b.startLine !== def.startLine
|
|
2765
3144
|
);
|
|
2766
3145
|
const fanReceiver = call.receiver || call.receiverType;
|
|
2767
|
-
if (fanReceiver && otherBindings.length >
|
|
3146
|
+
if (fanReceiver && otherBindings.length > 0) {
|
|
2768
3147
|
const symsForName = index.symbols.get(call.name) || [];
|
|
2769
3148
|
const classMatched = otherBindings.filter(b => {
|
|
2770
3149
|
const bSym = symsForName.find(s => s.bindingId === b.id);
|
|
@@ -2772,8 +3151,24 @@ function findCallees(index, def, options = {}) {
|
|
|
2772
3151
|
(bSym.receiver && bSym.receiver.replace(/^\*/, '')));
|
|
2773
3152
|
return cls === fanReceiver;
|
|
2774
3153
|
});
|
|
2775
|
-
if (classMatched.length > 0)
|
|
3154
|
+
if (classMatched.length > 0) {
|
|
3155
|
+
otherBindings = classMatched;
|
|
3156
|
+
} else if (call.isMethod && call.receiver) {
|
|
3157
|
+
// Untyped-receiver same-name method call:
|
|
3158
|
+
// name-equality with the enclosing def is not
|
|
3159
|
+
// receiver evidence (fix #237 — CacheService
|
|
3160
|
+
// .get's `cache.get(key)` sprayed a confirmed
|
|
3161
|
+
// edge onto ApiClient.get, leaking reachability
|
|
3162
|
+
// credit across classes). Route through the
|
|
3163
|
+
// uncertain machinery: multi-owner names stay
|
|
3164
|
+
// visible method-ambiguous; the single-owner
|
|
3165
|
+
// rule (with its defeaters) may still confirm.
|
|
3166
|
+
isUncertain = true;
|
|
3167
|
+
uncertainReason = 'method-ambiguous';
|
|
3168
|
+
otherBindings = null;
|
|
3169
|
+
}
|
|
2776
3170
|
}
|
|
3171
|
+
if (otherBindings) {
|
|
2777
3172
|
for (const ob of otherBindings) {
|
|
2778
3173
|
const existing = callees.get(ob.id);
|
|
2779
3174
|
if (existing) {
|
|
@@ -2794,6 +3189,9 @@ function findCallees(index, def, options = {}) {
|
|
|
2794
3189
|
noteSite(siteId, 'excluded', 'self-recursion', call);
|
|
2795
3190
|
}
|
|
2796
3191
|
continue; // Already added all overloads, skip normal add
|
|
3192
|
+
}
|
|
3193
|
+
// otherBindings === null: fall through to the
|
|
3194
|
+
// single-owner check + uncertain handling below.
|
|
2797
3195
|
} else if (def.className && !call.isMethod) {
|
|
2798
3196
|
// Implicit same-class call (Java: execute() means this.execute())
|
|
2799
3197
|
// Try to resolve to a binding in the same class via symbol lookup
|
|
@@ -2835,6 +3233,24 @@ function findCallees(index, def, options = {}) {
|
|
|
2835
3233
|
}
|
|
2836
3234
|
}
|
|
2837
3235
|
|
|
3236
|
+
// Single project-wide owner (fix #236 — the caller side's
|
|
3237
|
+
// #204/#209 rule on the callee side): an untyped-receiver method
|
|
3238
|
+
// call whose name has exactly ONE owner type resolves to that
|
|
3239
|
+
// owner's method — `k.run()` where only Kit defines run. Without
|
|
3240
|
+
// it, trace trees stopped expanding at statically-resolvable
|
|
3241
|
+
// calls the caller direction confirms.
|
|
3242
|
+
if (isUncertain && call.isMethod && call.receiver && !bindingResolved) {
|
|
3243
|
+
const fm = flowMap();
|
|
3244
|
+
const flowEntry = fm ? _lookupReturnTypeFlow(fm, call) : undefined;
|
|
3245
|
+
const owner = _calleeSingleOwnerMatch(index, def, fileEntry, call, effectiveName, language, flowEntry);
|
|
3246
|
+
if (owner) {
|
|
3247
|
+
isUncertain = false;
|
|
3248
|
+
bindingResolved = owner.bindingId;
|
|
3249
|
+
calleeKey = owner.bindingId ||
|
|
3250
|
+
`${owner.className || (owner.receiver || '').replace(/^\*/, '')}.${effectiveName}`;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
|
|
2838
3254
|
if (isUncertain) {
|
|
2839
3255
|
if (collectAccount) {
|
|
2840
3256
|
// Contract mode: uncertain callee edges are never silently
|
|
@@ -3155,7 +3571,7 @@ function findCallees(index, def, options = {}) {
|
|
|
3155
3571
|
// Stable ordering (output contract): by name, then reason.
|
|
3156
3572
|
const unverifiedList = [...unverifiedCallees.values()]
|
|
3157
3573
|
.map(e => ({ ...e, sites: [...e.sites].sort((a, b) => a - b) }))
|
|
3158
|
-
.sort((a, b) => a.name
|
|
3574
|
+
.sort((a, b) => codeUnitCompare(a.name, b.name) || codeUnitCompare(a.reason, b.reason));
|
|
3159
3575
|
Object.defineProperty(result, 'calleeAccount', {
|
|
3160
3576
|
value: calleeAccount, enumerable: false, writable: true, configurable: true,
|
|
3161
3577
|
});
|
|
@@ -3702,6 +4118,16 @@ function _qualifiedProducerDefs(index, fileEntry, receiver, defs) {
|
|
|
3702
4118
|
// Builtin receiver types from literal/annotation inference (Python builtins,
|
|
3703
4119
|
// JS globals, TS predefined types). Definitionally not project classes, so a
|
|
3704
4120
|
// mismatch against a project class target is always positive evidence.
|
|
4121
|
+
// ECMAScript host/ambient OBJECT globals (fix #232): a method call on one of
|
|
4122
|
+
// these names — unshadowed by any project def or file binding — reaches host
|
|
4123
|
+
// code, not a project method. Name-knowledge only, so demote-only: routes
|
|
4124
|
+
// possible-dispatch, never excludes (window.fn = projectFn is a real pattern).
|
|
4125
|
+
const JS_GLOBAL_RECEIVERS = new Set([
|
|
4126
|
+
'console', 'window', 'document', 'globalThis', 'process', 'navigator',
|
|
4127
|
+
'Math', 'JSON', 'Reflect', 'Intl', 'localStorage', 'sessionStorage',
|
|
4128
|
+
'crypto', 'performance', 'history', 'location', 'screen',
|
|
4129
|
+
]);
|
|
4130
|
+
|
|
3705
4131
|
const BUILTIN_RECEIVER_TYPES = new Set([
|
|
3706
4132
|
'dict', 'list', 'set', 'tuple', 'str', 'int', 'float', 'bool', 'bytes', 'frozenset',
|
|
3707
4133
|
'Array', 'String', 'Object', 'RegExp', 'Number', 'Boolean', 'Map', 'Set', 'Promise',
|
|
@@ -3902,6 +4328,28 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
3902
4328
|
return unknown ? 'unknown' : 'no';
|
|
3903
4329
|
}
|
|
3904
4330
|
|
|
4331
|
+
/**
|
|
4332
|
+
* From-import submodule receivers (fix #224): `from . import jobs` binds
|
|
4333
|
+
* jobs.py as a plain NAME — the parser can't mark it a module alias (a
|
|
4334
|
+
* from-import name may be a symbol), but the resolver proved it at build
|
|
4335
|
+
* time: graph-build records the composed submodule specifier ('.jobs') in
|
|
4336
|
+
* fileEntry.moduleResolved when it resolves to a project file. A hit makes
|
|
4337
|
+
* the receiver a MODULE receiver at query time. Returns the ROOT-RELATIVE
|
|
4338
|
+
* module file or null. Trait-gated (`submoduleImports` — Python only).
|
|
4339
|
+
*/
|
|
4340
|
+
function _submoduleReceiverModule(index, fileEntry, receiverName) {
|
|
4341
|
+
if (!receiverName || !fileEntry || !fileEntry.moduleResolved) return null;
|
|
4342
|
+
if (!langTraits(fileEntry.language)?.submoduleImports) return null;
|
|
4343
|
+
for (const b of (fileEntry.importBindings || [])) {
|
|
4344
|
+
if (!b || b.name !== receiverName || b.module == null) continue;
|
|
4345
|
+
const mod = String(b.module);
|
|
4346
|
+
const spec = mod.endsWith('.') ? mod + receiverName : mod + '.' + receiverName;
|
|
4347
|
+
const rel = fileEntry.moduleResolved[spec];
|
|
4348
|
+
if (rel) return rel;
|
|
4349
|
+
}
|
|
4350
|
+
return null;
|
|
4351
|
+
}
|
|
4352
|
+
|
|
3905
4353
|
/**
|
|
3906
4354
|
* Bounded-depth reachability over the import graph: can `fromAbs` reach any
|
|
3907
4355
|
* target file through re-export/import chains? Barrel hierarchies routinely
|
|
@@ -3951,6 +4399,96 @@ function _projectTopLevelNames(index) {
|
|
|
3951
4399
|
}
|
|
3952
4400
|
|
|
3953
4401
|
const IDENTITY_TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'enum']);
|
|
4402
|
+
|
|
4403
|
+
/**
|
|
4404
|
+
* Parse a generics/type-parameter list text (`<T: Wipe, U>`, `<T extends X>`,
|
|
4405
|
+
* Go `[T any, U comparable]`) into the set of declared type-parameter NAMES.
|
|
4406
|
+
* Rust lifetimes (`'a`) and const params (`const N: usize`) are not receiver
|
|
4407
|
+
* types and are skipped.
|
|
4408
|
+
*/
|
|
4409
|
+
function _genericParamNames(genericsText) {
|
|
4410
|
+
if (!genericsText || typeof genericsText !== 'string') return null;
|
|
4411
|
+
const inner = genericsText.trim().replace(/^[<[]/, '').replace(/[>\]]$/, '');
|
|
4412
|
+
const names = new Set();
|
|
4413
|
+
let depth = 0, start = 0;
|
|
4414
|
+
const parts = [];
|
|
4415
|
+
for (let i = 0; i < inner.length; i++) {
|
|
4416
|
+
const ch = inner[i];
|
|
4417
|
+
if (ch === '<' || ch === '[' || ch === '(') depth++;
|
|
4418
|
+
else if (ch === '>' || ch === ']' || ch === ')') depth--;
|
|
4419
|
+
else if (ch === ',' && depth === 0) { parts.push(inner.slice(start, i)); start = i + 1; }
|
|
4420
|
+
}
|
|
4421
|
+
parts.push(inner.slice(start));
|
|
4422
|
+
for (let p of parts) {
|
|
4423
|
+
p = p.trim();
|
|
4424
|
+
if (!p || p.startsWith("'") || p.startsWith('const ')) continue;
|
|
4425
|
+
const m = p.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
|
|
4426
|
+
if (m) names.add(m[1]);
|
|
4427
|
+
}
|
|
4428
|
+
return names.size > 0 ? names : null;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
/**
|
|
4432
|
+
* Is typeName a declared GENERIC TYPE PARAMETER in scope at this call site —
|
|
4433
|
+
* on the enclosing function itself (`fn f<TStore: Wipe>(t: &TStore)`) or on
|
|
4434
|
+
* its class/struct (`impl<T> Processor<T>` methods see the struct's `<T>`)?
|
|
4435
|
+
* A generic param is never type identity in either direction (fix #229, the
|
|
4436
|
+
* #220(1) convention rule made precise): it may be instantiated with the
|
|
4437
|
+
* target class, so it neither validates nor excludes — and the declaration
|
|
4438
|
+
* shadows any same-named project type inside the function.
|
|
4439
|
+
*/
|
|
4440
|
+
function _isEnclosingGenericParam(index, filePath, line, typeName) {
|
|
4441
|
+
const enclosing = index.findEnclosingFunction(filePath, line, true);
|
|
4442
|
+
if (!enclosing) return false;
|
|
4443
|
+
const own = _genericParamNames(enclosing.generics);
|
|
4444
|
+
if (own && own.has(typeName)) return true;
|
|
4445
|
+
if (enclosing.className) {
|
|
4446
|
+
const classDefs = (index.symbols.get(enclosing.className) || []).filter(d =>
|
|
4447
|
+
IDENTITY_TYPE_KINDS.has(d.type) && d.file === filePath);
|
|
4448
|
+
for (const cd of classDefs) {
|
|
4449
|
+
const cg = _genericParamNames(cd.generics);
|
|
4450
|
+
if (cg && cg.has(typeName)) return true;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
return false;
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
/**
|
|
4457
|
+
* Receiver-type identity guard shared by the parser-typed branch and the
|
|
4458
|
+
* local-inference fallback: a name is NOT usable as type identity when it is
|
|
4459
|
+
* a generic type param — declared in the enclosing scope, or matching the
|
|
4460
|
+
* 1-2-char ALL-CAPS convention (T, K, V, T1) with no project type def (the
|
|
4461
|
+
* declaring scope may be outside what UCN parsed).
|
|
4462
|
+
*/
|
|
4463
|
+
function _isGenericParamReceiverType(index, filePath, line, typeName) {
|
|
4464
|
+
if (!typeName) return false;
|
|
4465
|
+
if (/^[A-Z][A-Z0-9]?$/.test(typeName) &&
|
|
4466
|
+
!(index.symbols.get(typeName) || []).some(d => IDENTITY_TYPE_KINDS.has(d.type))) return true;
|
|
4467
|
+
return _isEnclosingGenericParam(index, filePath, line, typeName);
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
/**
|
|
4471
|
+
* Java same-package check across Maven/Gradle source roots (fix #246):
|
|
4472
|
+
* src/main/java/<pkg> and src/test/java/<pkg> hold the SAME package —
|
|
4473
|
+
* javac compiles both source sets onto one classpath, so a test file sees
|
|
4474
|
+
* the main tree's package members without an import. Two dirs are
|
|
4475
|
+
* same-package when equal, or (Java only) when both sit under a
|
|
4476
|
+
* `src/<set>/java/` source root with the same module prefix and the same
|
|
4477
|
+
* package-relative path. Different modules keep distinct prefixes, so
|
|
4478
|
+
* same-named packages across a monorepo stay separate.
|
|
4479
|
+
*/
|
|
4480
|
+
function _sameNominalPackageDir(dirA, dirB, language) {
|
|
4481
|
+
if (dirA === dirB) return true;
|
|
4482
|
+
if (language !== 'java') return false;
|
|
4483
|
+
const norm = (d) => {
|
|
4484
|
+
const m = d.match(/^(.*?)[\/\\]src[\/\\][^\/\\]+[\/\\]java(?:[\/\\](.*))?$/);
|
|
4485
|
+
return m ? `${m[1]}${m[2] || ''}` : null;
|
|
4486
|
+
};
|
|
4487
|
+
const a = norm(dirA);
|
|
4488
|
+
if (a === null) return false;
|
|
4489
|
+
return a === norm(dirB);
|
|
4490
|
+
}
|
|
4491
|
+
|
|
3954
4492
|
function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs) {
|
|
3955
4493
|
const typeDefs = (index.symbols.get(knownType) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
3956
4494
|
if (typeDefs.length <= 1) return 'target';
|
|
@@ -4079,6 +4617,360 @@ function _declaredFieldType(index, rootType, fieldName, language) {
|
|
|
4079
4617
|
return typeName;
|
|
4080
4618
|
}
|
|
4081
4619
|
|
|
4620
|
+
/**
|
|
4621
|
+
* Is a member reference `<recv>.name` on a KNOWN receiver type provably a
|
|
4622
|
+
* non-callable FIELD — never an edge to any project callable? (fix #231:
|
|
4623
|
+
* `delete(cs.cache, key)` captures cs.cache as a potential method-value
|
|
4624
|
+
* callee, but `cache` is CacheService's own map-typed field.) The member
|
|
4625
|
+
* access resolves to the MEMBER — a same-named function elsewhere in the
|
|
4626
|
+
* project is unreachable through this receiver (Go field names shadow
|
|
4627
|
+
* promoted methods; Java field/method namespaces are separate but a
|
|
4628
|
+
* paren-less member access is always the field). Only certainty excludes:
|
|
4629
|
+
* every same-type member of the name must be a field whose declared type is
|
|
4630
|
+
* present, not a function type (Go `func(...)`, Rust fn/Fn*, structural
|
|
4631
|
+
* arrow/Callable/Function — the #219 callable-owner shapes), and — for
|
|
4632
|
+
* structural languages — trusted for exclusion (#198: builtin or project
|
|
4633
|
+
* class; `any`/alias/interface heads prove nothing, and an untyped JS field
|
|
4634
|
+
* could hold a same-named function via `this.cb = cb`, the #218c
|
|
4635
|
+
* member-alias family).
|
|
4636
|
+
*/
|
|
4637
|
+
function _nonCallableFieldMember(index, typeName, name, language) {
|
|
4638
|
+
const defs = index.symbols.get(name);
|
|
4639
|
+
if (!defs || defs.length === 0) return false;
|
|
4640
|
+
const onType = defs.filter(d => d.className === typeName ||
|
|
4641
|
+
(d.receiver && d.receiver.replace(/^\*/, '') === typeName));
|
|
4642
|
+
if (onType.length === 0) return false;
|
|
4643
|
+
for (const d of onType) {
|
|
4644
|
+
if (d.type !== 'field' && d.memberType !== 'field' && d.memberType !== 'private field') return false;
|
|
4645
|
+
if (!d.fieldType) return false;
|
|
4646
|
+
if (_callableFieldDef(index, d)) return false;
|
|
4647
|
+
const raw = String(d.fieldType).trim();
|
|
4648
|
+
if (/^func\b/.test(raw)) return false;
|
|
4649
|
+
if (/\bfn\s*\(|\b(?:Fn|FnMut|FnOnce)\s*[(<]/.test(raw)) return false;
|
|
4650
|
+
if (langTraits(language)?.typeSystem === 'structural') {
|
|
4651
|
+
const head = _normalizeFieldTypeName(raw, language);
|
|
4652
|
+
if (!head || _STRUCTURAL_FLOW_REJECT.has(head) ||
|
|
4653
|
+
!_receiverTypeTrustedForExclusion(index, head)) return false;
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
return true;
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
/**
|
|
4660
|
+
* Callee-side type-qualified receiver resolution (fix #236 — the caller
|
|
4661
|
+
* side's #206/#208/#220/#222 identity discipline brought to findCallees).
|
|
4662
|
+
* A receiver that NAMES a type owns the call: `Foo::new()` is Foo's new and
|
|
4663
|
+
* nothing else's, `String::new()` can never be a project method, `Kit.make()`
|
|
4664
|
+
* through an imported class binding is Kit's make. Returns:
|
|
4665
|
+
* { match, typeName } — confirm this definition
|
|
4666
|
+
* { external: true } — type-qualified call on a builtin/external type
|
|
4667
|
+
* { unverified: reason } — visible, never confirmed through a name binding
|
|
4668
|
+
* null — receiver is not provably a type; no opinion
|
|
4669
|
+
* Shape gates follow typeQualifiedCallStyle (#206): Rust requires the path
|
|
4670
|
+
* form (a dot-call receiver matching a type name is a variable); Go method
|
|
4671
|
+
* expressions pass the receiver instance as the first argument, so zero-arg
|
|
4672
|
+
* calls on type-named receivers are variables. `use X as Y` aliases are
|
|
4673
|
+
* judged by the original name (#222b); pure type-alias sets close over their
|
|
4674
|
+
* base (#208). Structural class receivers additionally need scope evidence
|
|
4675
|
+
* (#215): the class defined in this file or a file binding of the name —
|
|
4676
|
+
* an unbound capitalized receiver may be a parameter or local.
|
|
4677
|
+
*/
|
|
4678
|
+
/**
|
|
4679
|
+
* Namespace/module-container resolution (fix #254, W8 BUG-4 — verify's
|
|
4680
|
+
* BUG-BX rule brought into the engine, range-based): `Utils.slug()` where a
|
|
4681
|
+
* `namespace Utils` block CONTAINS a definition of `slug` is a qualified
|
|
4682
|
+
* FUNCTION call, not a method call — containment is identity evidence, not
|
|
4683
|
+
* a naming heuristic. Requires #215 scope evidence tying the receiver to
|
|
4684
|
+
* the containing file: the call site sits in that file, or an import
|
|
4685
|
+
* binding of the receiver name resolves toward it (_importReaches — barrel
|
|
4686
|
+
* chains). Structural languages only; Rust `mod` paths keep the path
|
|
4687
|
+
* machinery.
|
|
4688
|
+
* @param {object} index - ProjectIndex instance
|
|
4689
|
+
* @param {object} fileEntry - The CALL SITE's file entry
|
|
4690
|
+
* @param {string} callFileAbs - The call site's absolute file path
|
|
4691
|
+
* @param {string} receiverName - The call's receiver text
|
|
4692
|
+
* @param {string} calleeName - The called name
|
|
4693
|
+
* @param {Array|null} restrictDefs - Candidate defs to test containment on
|
|
4694
|
+
* (the pinned targets on the caller side); null = all callable defs
|
|
4695
|
+
* @returns {object|null} The contained definition, or null
|
|
4696
|
+
*/
|
|
4697
|
+
function _namespaceContainedDef(index, fileEntry, callFileAbs, receiverName, calleeName, restrictDefs) {
|
|
4698
|
+
if (!receiverName || receiverName.includes('.') || receiverName.includes('::')) return null;
|
|
4699
|
+
const nsDefs = (index.symbols.get(receiverName) || []).filter(s =>
|
|
4700
|
+
s.type === 'namespace' || s.type === 'module');
|
|
4701
|
+
if (nsDefs.length === 0) return null;
|
|
4702
|
+
const candidates = restrictDefs ||
|
|
4703
|
+
(index.symbols.get(calleeName) || []).filter(s => !NON_CALLABLE_TYPES.has(s.type));
|
|
4704
|
+
const contained = candidates.filter(d => nsDefs.some(ns =>
|
|
4705
|
+
ns.file === d.file && ns.startLine <= d.startLine &&
|
|
4706
|
+
(ns.endLine ?? Infinity) >= (d.endLine ?? d.startLine)));
|
|
4707
|
+
if (contained.length === 0) return null;
|
|
4708
|
+
const targetAbs = new Set(contained.map(d => d.file));
|
|
4709
|
+
if (targetAbs.has(callFileAbs)) return contained[0];
|
|
4710
|
+
for (const im of (fileEntry?.importBindings || [])) {
|
|
4711
|
+
if (im.name !== receiverName) continue;
|
|
4712
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[String(im.module || '')];
|
|
4713
|
+
if (!rel) continue;
|
|
4714
|
+
const abs = path.join(index.root, rel);
|
|
4715
|
+
if (targetAbs.has(abs) || _importReaches(index, abs, targetAbs)) {
|
|
4716
|
+
return contained.find(d => d.file === abs) || contained[0];
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
return null;
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
4723
|
+
let receiver = call.receiver;
|
|
4724
|
+
if (!receiver) return null;
|
|
4725
|
+
const traits = langTraits(language);
|
|
4726
|
+
const style = traits?.typeQualifiedCallStyle;
|
|
4727
|
+
if (style === 'path' && !call.isPathCall) return null;
|
|
4728
|
+
if (style === 'method-expr' && call.argCount != null && call.argCount < 1) return null;
|
|
4729
|
+
|
|
4730
|
+
// Namespace/module container (fix #254): checked before the
|
|
4731
|
+
// capitalization gate — namespaces resolve by symbol lookup, not case
|
|
4732
|
+
// convention. A hit is a qualified function call, exempt from the
|
|
4733
|
+
// method filter downstream.
|
|
4734
|
+
if (traits?.typeSystem === 'structural') {
|
|
4735
|
+
const nsDef = _namespaceContainedDef(index, fileEntry, def.file, receiver, call.name, null);
|
|
4736
|
+
if (nsDef) return { match: nsDef, typeName: receiver };
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
const typeKindsOf = (name) => (index.symbols.get(name) || [])
|
|
4740
|
+
.filter(d => IDENTITY_TYPE_KINDS.has(d.type) || (d.type === 'type' && d.aliasOf));
|
|
4741
|
+
|
|
4742
|
+
// Multi-segment path receivers (std::sync::Arc::new): the LAST segment
|
|
4743
|
+
// is the type; the qualifier owns it (#206). A crate-internal qualifier
|
|
4744
|
+
// (crate/self/super) resolves by name below; std/core/alloc paths are
|
|
4745
|
+
// provably external even when a project type shares the name; any other
|
|
4746
|
+
// qualified name is unpinnable without a module resolver — visible when
|
|
4747
|
+
// a project type shares the name, external when none does.
|
|
4748
|
+
if (style === 'path' && receiver.includes('::')) {
|
|
4749
|
+
const segs = receiver.split('::');
|
|
4750
|
+
const lastSeg = segs[segs.length - 1];
|
|
4751
|
+
if (!/^[A-Z]/.test(lastSeg)) return null; // module-path function call
|
|
4752
|
+
if (['crate', 'self', 'super'].includes(segs[0])) {
|
|
4753
|
+
receiver = lastSeg;
|
|
4754
|
+
} else if (typeKindsOf(lastSeg).length === 0) {
|
|
4755
|
+
return { external: true };
|
|
4756
|
+
} else if (['std', 'core', 'alloc'].includes(segs[0])) {
|
|
4757
|
+
return { external: true };
|
|
4758
|
+
} else {
|
|
4759
|
+
return { unverified: 'method-ambiguous' };
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
if (!/^[A-Z]/.test(receiver)) return null;
|
|
4763
|
+
let typeDefs = typeKindsOf(receiver);
|
|
4764
|
+
if (typeDefs.length === 0) {
|
|
4765
|
+
for (const im of (fileEntry?.importBindings || [])) {
|
|
4766
|
+
if (im.name !== receiver) continue;
|
|
4767
|
+
const orig = String(im.module || '').split('::').pop();
|
|
4768
|
+
if (orig && orig !== receiver && typeKindsOf(orig).length > 0) {
|
|
4769
|
+
receiver = orig;
|
|
4770
|
+
typeDefs = typeKindsOf(orig);
|
|
4771
|
+
break;
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
if (typeDefs.length === 0) {
|
|
4776
|
+
// No project type of this name. Rust path receivers are provably
|
|
4777
|
+
// types (modules are lowercase, variables cannot be path-qualified):
|
|
4778
|
+
// a generic-param name stays visible — its instantiation could be
|
|
4779
|
+
// any project type — everything else (String::new, Arc::new) is
|
|
4780
|
+
// external. Java CamelCase receivers are classes (Math.max) —
|
|
4781
|
+
// external; ALL_CAPS receivers are constants (variables) and keep
|
|
4782
|
+
// normal resolution. Go capitalizes exported package-level VARIABLES
|
|
4783
|
+
// too, and a structural capitalized receiver may be a parameter or
|
|
4784
|
+
// local — neither acts without a project type def.
|
|
4785
|
+
if (style === 'path') {
|
|
4786
|
+
if (_isGenericParamReceiverType(index, def.file, call.line, receiver)) {
|
|
4787
|
+
return { unverified: 'method-ambiguous' };
|
|
4788
|
+
}
|
|
4789
|
+
return { external: true };
|
|
4790
|
+
}
|
|
4791
|
+
if (language === 'java' && /[a-z]/.test(receiver) &&
|
|
4792
|
+
!_isGenericParamReceiverType(index, def.file, call.line, receiver)) {
|
|
4793
|
+
return { external: true };
|
|
4794
|
+
}
|
|
4795
|
+
return null;
|
|
4796
|
+
}
|
|
4797
|
+
if (traits?.typeSystem === 'structural') {
|
|
4798
|
+
const inFile = typeDefs.some(d => d.file === def.file);
|
|
4799
|
+
const bound = (fileEntry?.bindings || []).some(b => b.name === receiver);
|
|
4800
|
+
if (!inFile && !bound) return null;
|
|
4801
|
+
}
|
|
4802
|
+
// Alias closure (#208): a pure alias set agreeing on one base is the
|
|
4803
|
+
// SAME type — the method may live on the base's inherent impl.
|
|
4804
|
+
const candidateTypes = [receiver];
|
|
4805
|
+
if (typeDefs.every(d => d.type === 'type' && d.aliasOf)) {
|
|
4806
|
+
const bases = new Set(typeDefs.map(d => d.aliasOf));
|
|
4807
|
+
if (bases.size === 1) candidateTypes.push(bases.values().next().value);
|
|
4808
|
+
}
|
|
4809
|
+
const symbols = index.symbols.get(call.name) || [];
|
|
4810
|
+
const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
4811
|
+
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
4812
|
+
const matchOn = (tn) => symbols.find(s => isCallable(s) &&
|
|
4813
|
+
(s.className === tn || (s.receiver && s.receiver.replace(/^\*/, '') === tn)));
|
|
4814
|
+
let match = null;
|
|
4815
|
+
let matchedType = null;
|
|
4816
|
+
for (const tn of candidateTypes) {
|
|
4817
|
+
match = matchOn(tn);
|
|
4818
|
+
if (match) { matchedType = tn; break; }
|
|
4819
|
+
}
|
|
4820
|
+
if (!match && traits?.typeSystem === 'nominal') {
|
|
4821
|
+
for (const tn of candidateTypes) {
|
|
4822
|
+
const parentNames = index._getInheritanceParents?.(tn, def.file);
|
|
4823
|
+
if (!parentNames) continue;
|
|
4824
|
+
for (const pName of parentNames) {
|
|
4825
|
+
match = matchOn(pName);
|
|
4826
|
+
if (match) { matchedType = pName; break; }
|
|
4827
|
+
}
|
|
4828
|
+
if (match) break;
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
if (match) return { match, typeName: matchedType };
|
|
4832
|
+
// A project type that does not define the method: a trait/interface
|
|
4833
|
+
// receiver dispatches across N impls; otherwise the method comes from a
|
|
4834
|
+
// derive/trait impl/external contract the index cannot pin. Visible —
|
|
4835
|
+
// never confirmed through an unrelated name binding.
|
|
4836
|
+
const dispatchy = typeDefs.some(d => d.type === 'trait' || d.type === 'interface');
|
|
4837
|
+
return { unverified: dispatchy ? 'method-ambiguous' : 'uncertain-receiver' };
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
/**
|
|
4841
|
+
* Single project-wide owner resolution for an untyped-receiver method call
|
|
4842
|
+
* (fix #236 — the caller side's #204/#209 rule on the callee side): when
|
|
4843
|
+
* every callable definition of the name lives on ONE owner type, `k.run()`
|
|
4844
|
+
* can only be that owner's method. Defeaters mirror the caller contract:
|
|
4845
|
+
* builtin-global receivers (#232 — name knowledge, not evidence), module
|
|
4846
|
+
* receivers (#209/#224 — module attribute lookup, not instance dispatch),
|
|
4847
|
+
* a standalone function sharing the name (rebinding can route the call
|
|
4848
|
+
* there, #218b), callable fields as second owners (#219), external-contract
|
|
4849
|
+
* override markers (#210 — the overridden definition lives OUTSIDE the
|
|
4850
|
+
* project), receivers typed by the flow map to something other than the
|
|
4851
|
+
* owner (#199/#207/#222(4)), nominal arity mismatch (#205), and a test-file
|
|
4852
|
+
* owner for a production caller. Returns the owner's definition or null.
|
|
4853
|
+
*/
|
|
4854
|
+
// Languages whose symbols are mutually callable — a JS/TS call site can bind
|
|
4855
|
+
// a symbol defined in any of these; every other language only binds its own
|
|
4856
|
+
// (fix #257: java/python/rust fixture defs of CacheService.set counted as ONE
|
|
4857
|
+
// owner for a JavaScript call — cross-language edges are never callable).
|
|
4858
|
+
const _JS_CALLABLE_FAMILY = new Set(['javascript', 'typescript', 'tsx', 'html']);
|
|
4859
|
+
|
|
4860
|
+
function _calleeLanguageCompatible(index, def, callerLanguage) {
|
|
4861
|
+
if (!callerLanguage) return true;
|
|
4862
|
+
const defLang = index.files.get(def.file)?.language;
|
|
4863
|
+
if (!defLang || defLang === callerLanguage) return true;
|
|
4864
|
+
return _JS_CALLABLE_FAMILY.has(defLang) && _JS_CALLABLE_FAMILY.has(callerLanguage);
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
/**
|
|
4868
|
+
* Ancestor-name closure of a type via the extends graph (bounded BFS) — a
|
|
4869
|
+
* receiver typed Child legitimately reaches methods defined on Base (the
|
|
4870
|
+
* #198 ancestor rule, callee direction). Includes the type itself.
|
|
4871
|
+
*/
|
|
4872
|
+
function _receiverTypeAncestors(index, typeName, maxHops = 6) {
|
|
4873
|
+
const seen = new Set([typeName]);
|
|
4874
|
+
let frontier = [typeName];
|
|
4875
|
+
for (let hop = 0; hop < maxHops && frontier.length; hop++) {
|
|
4876
|
+
const next = [];
|
|
4877
|
+
for (const cls of frontier) {
|
|
4878
|
+
const parents = index._getInheritanceParents?.(cls) || [];
|
|
4879
|
+
for (const p of parents) {
|
|
4880
|
+
const pName = typeof p === 'string' ? p : p?.name;
|
|
4881
|
+
if (pName && !seen.has(pName)) { seen.add(pName); next.push(pName); }
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
frontier = next;
|
|
4885
|
+
}
|
|
4886
|
+
return seen;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
/**
|
|
4890
|
+
* Route a structural method call by its KNOWN receiver type (fix #257).
|
|
4891
|
+
* Returns:
|
|
4892
|
+
* { resolve: def } — exactly one language-compatible project class (the
|
|
4893
|
+
* type itself or an ancestor) defines the method
|
|
4894
|
+
* { external } — builtin receiver type with no project match
|
|
4895
|
+
* (Map.set is host code)
|
|
4896
|
+
* { uncertain } — known type matching nothing or ambiguously: visible,
|
|
4897
|
+
* never confirmed by bare-name resolution
|
|
4898
|
+
* null — receiver type unknown; existing heuristics decide
|
|
4899
|
+
*/
|
|
4900
|
+
function _calleeReceiverTypeRoute(index, call, localTypes, language) {
|
|
4901
|
+
const raw = call.receiverType || localTypes?.get(call.receiver);
|
|
4902
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
4903
|
+
const head = _structuralTypeHead(raw) || raw;
|
|
4904
|
+
const norm = _PY_TYPING_BUILTINS[head] || head;
|
|
4905
|
+
const defs = (index.symbols.get(call.name) || []).filter(d =>
|
|
4906
|
+
!NON_CALLABLE_TYPES.has(d.type) && d.className &&
|
|
4907
|
+
_calleeLanguageCompatible(index, d, language));
|
|
4908
|
+
let matches = defs.filter(d => d.className === head || d.className === norm);
|
|
4909
|
+
if (matches.length === 0 && defs.length > 0) {
|
|
4910
|
+
const ancestors = _receiverTypeAncestors(index, head);
|
|
4911
|
+
matches = defs.filter(d => ancestors.has(d.className));
|
|
4912
|
+
}
|
|
4913
|
+
if (matches.length === 1) return { resolve: matches[0] };
|
|
4914
|
+
if (matches.length > 1) return { uncertain: true }; // same-name classes — identity unresolvable (#206)
|
|
4915
|
+
if (BUILTIN_RECEIVER_TYPES.has(norm)) return { external: true };
|
|
4916
|
+
return { uncertain: true }; // known non-builtin type with no project method
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
function _calleeSingleOwnerMatch(index, def, fileEntry, call, name, language, flowEntry) {
|
|
4920
|
+
if (JS_GLOBAL_RECEIVERS.has(call.receiver)) return null;
|
|
4921
|
+
if (call.receiverIsModule) return null;
|
|
4922
|
+
const traits = langTraits(language);
|
|
4923
|
+
if (traits?.typeSystem === 'structural' &&
|
|
4924
|
+
_submoduleReceiverModule(index, fileEntry, call.receiver)) return null;
|
|
4925
|
+
const defs = index.symbols.get(name) || [];
|
|
4926
|
+
const ownerKeys = new Set();
|
|
4927
|
+
const ownerDefs = [];
|
|
4928
|
+
for (const d of defs) {
|
|
4929
|
+
if (!_calleeLanguageCompatible(index, d, language)) continue; // fix #257
|
|
4930
|
+
if (NON_CALLABLE_TYPES.has(d.type)) {
|
|
4931
|
+
if (d.type === 'field' && d.className && _callableFieldDef(index, d)) {
|
|
4932
|
+
ownerKeys.add(d.className);
|
|
4933
|
+
}
|
|
4934
|
+
continue;
|
|
4935
|
+
}
|
|
4936
|
+
const o = d.className || (d.receiver && d.receiver.replace(/^\*/, ''));
|
|
4937
|
+
if (!o) return null;
|
|
4938
|
+
ownerKeys.add(o);
|
|
4939
|
+
ownerDefs.push(d);
|
|
4940
|
+
}
|
|
4941
|
+
if (ownerKeys.size !== 1 || ownerDefs.length === 0) return null;
|
|
4942
|
+
if (ownerDefs.length > 1 && traits?.hasArityOverloads) return null;
|
|
4943
|
+
if (ownerDefs.some(d => isOverrideMarked(d))) return null;
|
|
4944
|
+
if (flowEntry) {
|
|
4945
|
+
const owner = ownerKeys.values().next().value;
|
|
4946
|
+
if (flowEntry.externalVia || (flowEntry.type && flowEntry.type !== owner)) return null;
|
|
4947
|
+
}
|
|
4948
|
+
// Parser-typed receiver disagreeing with the owner defeats the match
|
|
4949
|
+
// unless the owner is an ancestor of the receiver's type (fix #257 —
|
|
4950
|
+
// the flow-map defeater above, extended to #198 parser-typed receivers:
|
|
4951
|
+
// a `new Map()` local can never single-owner-confirm a project method).
|
|
4952
|
+
if (call.receiverType && typeof call.receiverType === 'string') {
|
|
4953
|
+
const owner = ownerKeys.values().next().value;
|
|
4954
|
+
const head = _structuralTypeHead(call.receiverType) || call.receiverType;
|
|
4955
|
+
const norm = _PY_TYPING_BUILTINS[head] || head;
|
|
4956
|
+
if (head !== owner && norm !== owner &&
|
|
4957
|
+
!_receiverTypeAncestors(index, head).has(owner)) return null;
|
|
4958
|
+
}
|
|
4959
|
+
if (traits?.typeSystem === 'nominal' && call.argCount != null &&
|
|
4960
|
+
!_callArityCompatible(call, ownerDefs, language)) return null;
|
|
4961
|
+
const match = ownerDefs[0];
|
|
4962
|
+
// The owner's def IS the querying def: an untyped-receiver call cannot
|
|
4963
|
+
// prove self-recursion (a true one resolves via self/this/Self) — the
|
|
4964
|
+
// receiver is more likely an external value (fix #237).
|
|
4965
|
+
if (match.file === def.file && match.startLine === def.startLine) return null;
|
|
4966
|
+
const callerFe = index.files.get(def.file);
|
|
4967
|
+
const matchFe = index.files.get(match.file);
|
|
4968
|
+
if (matchFe && callerFe &&
|
|
4969
|
+
(isTestFile(matchFe.relativePath, matchFe.language) || isTestPath(matchFe.relativePath)) &&
|
|
4970
|
+
!(isTestFile(callerFe.relativePath, callerFe.language) || isTestPath(callerFe.relativePath))) return null;
|
|
4971
|
+
return match;
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4082
4974
|
/**
|
|
4083
4975
|
* Can this call's argument count fit any target definition's parameter
|
|
4084
4976
|
* range? (Nominal languages only — their compilers enforce arity, so a
|
|
@@ -4833,4 +5725,4 @@ function findCallbackUsages(index, name) {
|
|
|
4833
5725
|
return usages;
|
|
4834
5726
|
}
|
|
4835
5727
|
|
|
4836
|
-
module.exports = { getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages };
|
|
5728
|
+
module.exports = { getCachedCalls, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _declaredFieldType, _projectTopLevelNames };
|