ucn 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -11
- package/core/build-worker.js +11 -2
- package/core/cache.js +21 -1
- package/core/callers.js +618 -43
- package/core/deadcode.js +25 -2
- package/core/entrypoints.js +9 -1
- package/core/output/analysis.js +4 -0
- package/core/project.js +13 -3
- package/core/search.js +149 -26
- package/languages/java.js +32 -3
- package/languages/javascript.js +257 -34
- package/languages/python.js +77 -10
- package/languages/rust.js +99 -1
- package/package.json +4 -4
package/core/callers.js
CHANGED
|
@@ -133,6 +133,55 @@ function getCachedCalls(index, filePath, options = {}) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function _javaPackageKey(relativePath) {
|
|
137
|
+
const normalized = String(relativePath || '').replace(/\\/g, '/');
|
|
138
|
+
const marker = '/java/';
|
|
139
|
+
const at = normalized.lastIndexOf(marker);
|
|
140
|
+
const packagePath = at >= 0 ? normalized.slice(at + marker.length) : normalized;
|
|
141
|
+
return path.posix.dirname(packagePath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _javaConstructorDisposition(index, filePath, fileEntry, call, targetDefs) {
|
|
145
|
+
const typeKinds = new Set(['class', 'record', 'enum']);
|
|
146
|
+
const targets = (targetDefs || []).filter(d => typeKinds.has(d.type));
|
|
147
|
+
if (targets.length === 0) return 'target';
|
|
148
|
+
const all = (index.symbols.get(call.name) || []).filter(d => typeKinds.has(d.type));
|
|
149
|
+
const targetKeys = new Set(targets.map(d => `${d.file}:${d.startLine}`));
|
|
150
|
+
const isTarget = d => targetKeys.has(`${d.file}:${d.startLine}`);
|
|
151
|
+
|
|
152
|
+
if (call.receiver) {
|
|
153
|
+
const owned = all.filter(d => d.enclosingType === call.receiver);
|
|
154
|
+
if (owned.some(isTarget)) return 'target';
|
|
155
|
+
if (owned.length > 0) return 'other';
|
|
156
|
+
// Lowercase qualifiers are package fragments; the Java parser retains
|
|
157
|
+
// only the terminal qualifier, so identity is not provable here.
|
|
158
|
+
return 'unknown';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const bindings = (fileEntry.importBindings || []).filter(b =>
|
|
162
|
+
b.name === call.name || b.alias === call.name);
|
|
163
|
+
if (bindings.length > 0) {
|
|
164
|
+
const resolvedFiles = bindings.map(b => fileEntry.moduleResolved?.[b.module])
|
|
165
|
+
.filter(Boolean).map(rel => path.join(index.root, rel));
|
|
166
|
+
if (resolvedFiles.some(fp => targets.some(d => d.file === fp))) return 'target';
|
|
167
|
+
if (resolvedFiles.length > 0) return 'other';
|
|
168
|
+
return 'unknown';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const caller = index.findEnclosingFunction(filePath, call.line, true);
|
|
172
|
+
if (targets.some(d => d.file === filePath &&
|
|
173
|
+
(!d.enclosingType || d.enclosingType === caller?.className))) return 'target';
|
|
174
|
+
|
|
175
|
+
const callerPackage = _javaPackageKey(fileEntry.relativePath);
|
|
176
|
+
const samePackage = all.filter(d => !d.enclosingType &&
|
|
177
|
+
_javaPackageKey(d.relativePath) === callerPackage);
|
|
178
|
+
if (samePackage.some(isTarget)) return 'target';
|
|
179
|
+
if (samePackage.length > 0) return 'other';
|
|
180
|
+
|
|
181
|
+
if (all.length > 0 && all.every(isTarget)) return 'target';
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
|
|
136
185
|
/**
|
|
137
186
|
* Find all call sites that invoke the named symbol.
|
|
138
187
|
*
|
|
@@ -313,6 +362,24 @@ function findCallers(index, name, options = {}) {
|
|
|
313
362
|
exportAliasRenamers.get(e.alias).add(fp);
|
|
314
363
|
}
|
|
315
364
|
}
|
|
365
|
+
// CommonJS callable default export: `const express = require('./lib')`
|
|
366
|
+
// followed by `express()` denotes the local function assigned to
|
|
367
|
+
// `module.exports`, even though the call-site spelling differs. This is
|
|
368
|
+
// exact module ownership: require bindings preserve their default-like
|
|
369
|
+
// shape and the resolved module records the assigned local definition.
|
|
370
|
+
for (const [fp, fe] of index.files) {
|
|
371
|
+
for (const b of (fe.importBindings || [])) {
|
|
372
|
+
if (!b.defaultLike) continue;
|
|
373
|
+
const rel = fe.moduleResolved && fe.moduleResolved[b.module];
|
|
374
|
+
if (!rel) continue;
|
|
375
|
+
const moduleFile = path.join(index.root, rel);
|
|
376
|
+
if (!aliasTargetFiles.has(moduleFile)) continue;
|
|
377
|
+
const exported = index.files.get(moduleFile)?.exportDetails || [];
|
|
378
|
+
if (!exported.some(e => e.type === 'module.exports' && e.name === name)) continue;
|
|
379
|
+
if (!importAliasLocals.has(fp)) importAliasLocals.set(fp, new Set());
|
|
380
|
+
importAliasLocals.get(fp).add(b.alias || b.name);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
316
383
|
// Files that can reach each renaming module through imports — re-export
|
|
317
384
|
// chains run deep (test → mini/index → external → schemas), so a fixed
|
|
318
385
|
// hop count misses real surfaces. Bounded reverse-import BFS; matching
|
|
@@ -342,6 +409,14 @@ function findCallers(index, name, options = {}) {
|
|
|
342
409
|
for (const local of locals) aliasNames.add(local);
|
|
343
410
|
}
|
|
344
411
|
const hasAliasSurfaces = aliasNames.size > 0;
|
|
412
|
+
const shadowCanReachPinnedTarget = (filePath, call) => {
|
|
413
|
+
if (call.resolvedName === name || call.resolvedNames?.includes(name)) return true;
|
|
414
|
+
const scope = call.enclosingFunction;
|
|
415
|
+
return !!scope && (options.targetDefinitions || definitions).some(d =>
|
|
416
|
+
d.file === filePath && d.name === call.name &&
|
|
417
|
+
d.startLine >= scope.startLine && d.endLine <= scope.endLine &&
|
|
418
|
+
d.startLine <= call.line);
|
|
419
|
+
};
|
|
345
420
|
|
|
346
421
|
// Phase 1: Find matching calls without reading file content.
|
|
347
422
|
// Collect pending callers keyed by file — content is read only in Phase 2.
|
|
@@ -415,6 +490,64 @@ function findCallers(index, name, options = {}) {
|
|
|
415
490
|
}
|
|
416
491
|
if (!calledAs) continue;
|
|
417
492
|
}
|
|
493
|
+
if (!calledAs && call.name !== name &&
|
|
494
|
+
(call.resolvedName === name ||
|
|
495
|
+
(call.resolvedNames && call.resolvedNames.includes(name)))) {
|
|
496
|
+
calledAs = call.name;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Java constructor identity is package/import/nesting scoped.
|
|
500
|
+
// A bare `new Tag()` imported from parser.Tag cannot construct
|
|
501
|
+
// either Evaluator.Tag or Token.Tag merely because all three
|
|
502
|
+
// share the terminal name. Keep unresolved ownership visible;
|
|
503
|
+
// only compiler-shaped scope evidence enters the confirmed tier.
|
|
504
|
+
if (call.isConstructor && fileEntry.language === 'java') {
|
|
505
|
+
const constructorDisposition = _javaConstructorDisposition(
|
|
506
|
+
index, filePath, fileEntry, call,
|
|
507
|
+
options.targetDefinitions || definitions);
|
|
508
|
+
if (constructorDisposition === 'other') {
|
|
509
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (constructorDisposition === 'unknown') {
|
|
513
|
+
if (collectAccount) {
|
|
514
|
+
routeUnverified(filePath, fileEntry, call,
|
|
515
|
+
'ambiguous-binding', calledAs);
|
|
516
|
+
}
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// A named function expression's name is visible only inside
|
|
522
|
+
// its own body (ECMA-262). A pinned NFE target can be called
|
|
523
|
+
// only by code within its range (recursion); any other site
|
|
524
|
+
// provably binds a different definition of the name.
|
|
525
|
+
const pinnedForScope = options.targetDefinitions || definitions;
|
|
526
|
+
if (!call.isMethod && options.targetDefinitions &&
|
|
527
|
+
pinnedForScope.length > 0 &&
|
|
528
|
+
pinnedForScope.every(d => d.bodyScopedName) &&
|
|
529
|
+
!pinnedForScope.some(d => d.file === filePath &&
|
|
530
|
+
call.line >= d.startLine && call.line <= d.endLine)) {
|
|
531
|
+
recordExcluded(filePath, call.line, 'other-definition');
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// A cast receiver type is a compile-true STATIC type, but a
|
|
536
|
+
// cast naming a type with NO project definition places the
|
|
537
|
+
// value in code UCN cannot see (java.lang.Integer, external
|
|
538
|
+
// interfaces). Name-knowledge is never routing evidence
|
|
539
|
+
// (#222(4)): the invisible type may extend the target's
|
|
540
|
+
// class, so an unresolved cast neither confirms nor excludes
|
|
541
|
+
// — drop to the receiver-evidence-free physics (#210
|
|
542
|
+
// external-contract / #204 owner rules decide, both modes),
|
|
543
|
+
// exactly the pre-cast-capture record shape. Resolved casts
|
|
544
|
+
// keep their honest typed routing.
|
|
545
|
+
if (call.receiverTypeCast && call.receiverType &&
|
|
546
|
+
!(index.symbols.get(call.receiverType) || [])
|
|
547
|
+
.some(d => NON_CALLABLE_TYPES.has(d.type) && d.type !== 'field')) {
|
|
548
|
+
call = { ...call, receiver: undefined, receiverType: undefined,
|
|
549
|
+
receiverTypeCast: undefined };
|
|
550
|
+
}
|
|
418
551
|
|
|
419
552
|
// Return-type flow: an untyped method receiver may be a
|
|
420
553
|
// variable assigned from a call with a known return annotation
|
|
@@ -486,6 +619,55 @@ function findCallers(index, name, options = {}) {
|
|
|
486
619
|
}
|
|
487
620
|
}
|
|
488
621
|
|
|
622
|
+
// An unqualified constructor name can still have exact import
|
|
623
|
+
// provenance: `from io import StringIO; out = StringIO()`.
|
|
624
|
+
// A resolved project import pins the type to that file; a
|
|
625
|
+
// clearly external import blocks project-local single-owner
|
|
626
|
+
// confirmation. Annotation-only receivers do not take this
|
|
627
|
+
// path because they may hold a project subtype.
|
|
628
|
+
if (call.isMethod && call.receiverConstructed && call.receiverType &&
|
|
629
|
+
!call.receiverTypeQualifier &&
|
|
630
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
631
|
+
const bindings = (fileEntry.importBindings || []).filter(b =>
|
|
632
|
+
b.name === call.receiverType || b.alias === call.receiverType);
|
|
633
|
+
if (bindings.length > 0) {
|
|
634
|
+
const project = bindings.map(b => ({
|
|
635
|
+
binding: b,
|
|
636
|
+
rel: fileEntry.moduleResolved && fileEntry.moduleResolved[b.module],
|
|
637
|
+
})).find(x => x.rel);
|
|
638
|
+
if (project) {
|
|
639
|
+
call = { ...call,
|
|
640
|
+
receiverType: project.binding.name,
|
|
641
|
+
receiverTypeFlowFile: path.join(index.root, project.rel),
|
|
642
|
+
};
|
|
643
|
+
} else {
|
|
644
|
+
const projectish = bindings.some(b => {
|
|
645
|
+
const mod = String(b.module || '');
|
|
646
|
+
const first = mod.split(/[./]/).filter(Boolean)[0];
|
|
647
|
+
return mod.startsWith('.') ||
|
|
648
|
+
(first && _projectTopLevelNames(index).has(first));
|
|
649
|
+
});
|
|
650
|
+
const via = `${bindings[0].module}.${bindings[0].name}`;
|
|
651
|
+
call = { ...call, receiverType: undefined,
|
|
652
|
+
...(projectish
|
|
653
|
+
? { receiverQualifiedFlow: `${via} — unresolved module` }
|
|
654
|
+
: { receiverExternalFlow: via }) };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// An untyped value produced by a context manager (`with ...
|
|
660
|
+
// as out`) has no receiver identity. A unique project method
|
|
661
|
+
// spelling is not enough to confirm it; keep the edge visible
|
|
662
|
+
// until an annotation/constructor/return flow proves the type.
|
|
663
|
+
if (collectAccount && call.isMethod && call.receiverWithBinding &&
|
|
664
|
+
!call.receiverType && !call.receiverExternalFlow && !call.receiverQualifiedFlow) {
|
|
665
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
666
|
+
dispatchVia: 'context-manager result',
|
|
667
|
+
});
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
|
|
489
671
|
// Chained-receiver typing (fix #219): the receiver IS a call —
|
|
490
672
|
// `me._def.args.parseAsync(args, params).catch(...)` — so the
|
|
491
673
|
// producer's DECLARED return annotation types it (Promise →
|
|
@@ -750,6 +932,24 @@ function findCallers(index, name, options = {}) {
|
|
|
750
932
|
// same-name symbol shadows it invisibly.
|
|
751
933
|
const cbTargetFiles = new Set(cbTargetDefs.map(d => d.file).filter(Boolean));
|
|
752
934
|
const cbSameFile = cbTargetFiles.has(filePath);
|
|
935
|
+
// A module-local definition owns a bare callback name
|
|
936
|
+
// before any file-level import graph evidence. Importing
|
|
937
|
+
// the target's package elsewhere in the file cannot make
|
|
938
|
+
// `register(group)` refer past a local `def group()`.
|
|
939
|
+
// Ownership requires a def a BARE name can actually bind
|
|
940
|
+
// (#218a/#222(3)/#269(2)): class members, property-
|
|
941
|
+
// assigned functions, and body-scoped function-expression
|
|
942
|
+
// names never own module scope — a same-file `class X {
|
|
943
|
+
// validate() {} }` must not exclude the imported
|
|
944
|
+
// validate passed as a callback.
|
|
945
|
+
if (!cbSameFile && (index.symbols.get(call.name) || []).some(d =>
|
|
946
|
+
d.file === filePath && !cbTargetFiles.has(d.file) &&
|
|
947
|
+
d.startLine <= call.line &&
|
|
948
|
+
!d.className && !d.memberAssigned && !d.bodyScopedName &&
|
|
949
|
+
!NON_CALLABLE_TYPES.has(d.type))) {
|
|
950
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
753
953
|
const cbSamePackage = !cbSameFile &&
|
|
754
954
|
langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
|
|
755
955
|
cbTargetDefs.some(d => d.file &&
|
|
@@ -772,7 +972,12 @@ function findCallers(index, name, options = {}) {
|
|
|
772
972
|
// same-name definition in this file (or one this file
|
|
773
973
|
// imports) that is NOT the target — same disposition
|
|
774
974
|
// as the import-graph disambiguation for plain calls.
|
|
775
|
-
|
|
975
|
+
// Bare-name bindable defs only (#222(3)): a foreign
|
|
976
|
+
// class's same-name METHOD is unreachable by a bare
|
|
977
|
+
// reference and proves no mis-link.
|
|
978
|
+
const cbOtherDefFiles = new Set((index.symbols.get(call.name) || [])
|
|
979
|
+
.filter(d => !d.className && !d.memberAssigned &&
|
|
980
|
+
!d.bodyScopedName && !NON_CALLABLE_TYPES.has(d.type))
|
|
776
981
|
.map(d => d.file).filter(f => f && !cbTargetFiles.has(f)));
|
|
777
982
|
if (cbOtherDefFiles.has(filePath) ||
|
|
778
983
|
(cbImports && setSome(cbImports, imp => cbOtherDefFiles.has(imp)))) {
|
|
@@ -791,7 +996,7 @@ function findCallers(index, name, options = {}) {
|
|
|
791
996
|
// locals and inner-arrow params (fix #203 — parser-side
|
|
792
997
|
// lexical scope walk sets call.localShadow; JS block-accurate,
|
|
793
998
|
// Python function-wide assignment semantics).
|
|
794
|
-
if (call.localShadow ||
|
|
999
|
+
if ((call.localShadow && !shadowCanReachPinnedTarget(filePath, call)) ||
|
|
795
1000
|
(callerSymbol && Array.isArray(callerSymbol.paramsStructured) &&
|
|
796
1001
|
callerSymbol.paramsStructured.some(p => p && p.name === call.name))) {
|
|
797
1002
|
recordExcluded(filePath, call.line, 'local-shadow');
|
|
@@ -822,7 +1027,8 @@ function findCallers(index, name, options = {}) {
|
|
|
822
1027
|
// an isFunctionReference-only argument ref (`router.use(path,
|
|
823
1028
|
// f)` inside `use(fn)`'s forEach closure) used to slip past to
|
|
824
1029
|
// binding resolution and exact-confirm on the shadowed name.
|
|
825
|
-
if (call.localShadow && !call.isPotentialCallback
|
|
1030
|
+
if (call.localShadow && !call.isPotentialCallback &&
|
|
1031
|
+
!shadowCanReachPinnedTarget(filePath, call)) {
|
|
826
1032
|
recordExcluded(filePath, call.line, 'local-shadow');
|
|
827
1033
|
continue;
|
|
828
1034
|
}
|
|
@@ -838,7 +1044,7 @@ function findCallers(index, name, options = {}) {
|
|
|
838
1044
|
// Super records resolve only through the parent-chain walk.
|
|
839
1045
|
const selfReceivers = new Set(['self', 'cls', 'this']);
|
|
840
1046
|
const indirectStructuralReceiver = call.isMethod && !call.receiver &&
|
|
841
|
-
!!(call.receiverRoot || call.receiverField || call.receiverCall);
|
|
1047
|
+
!!(call.receiverRoot || call.receiverField || call.receiverCall || call.receiverType);
|
|
842
1048
|
const skipLocalBinding = (call.receiver && !selfReceivers.has(call.receiver)) ||
|
|
843
1049
|
indirectStructuralReceiver;
|
|
844
1050
|
if (!bindingId && !skipLocalBinding) {
|
|
@@ -1389,7 +1595,7 @@ function findCallers(index, name, options = {}) {
|
|
|
1389
1595
|
// modules must not exclude: a binding to an unresolved module
|
|
1390
1596
|
// whose first segment matches a project directory routes
|
|
1391
1597
|
// visible instead.
|
|
1392
|
-
if (!bindingId && !call.isMethod &&
|
|
1598
|
+
if (!bindingId && !call.isMethod &&
|
|
1393
1599
|
langTraits(fileEntry.language)?.typeSystem === 'structural' &&
|
|
1394
1600
|
(fileEntry.importBindings || []).length > 0) {
|
|
1395
1601
|
// Renamed destructures look up by the RESOLVED name (fix
|
|
@@ -1400,7 +1606,31 @@ function findCallers(index, name, options = {}) {
|
|
|
1400
1606
|
// ownership chase and the call scope-confirmed against
|
|
1401
1607
|
// hooks.js's unrelated validate.
|
|
1402
1608
|
const lookupName = call.resolvedName || call.name;
|
|
1403
|
-
|
|
1609
|
+
// Alias-matched surfaces (calledAs — import/export renames)
|
|
1610
|
+
// spell the call by the LOCAL alias while importBindings
|
|
1611
|
+
// store the ORIGINAL name: `import { _gt as gt }` binds
|
|
1612
|
+
// name '_gt' with alias 'gt'. Match the alias side too so
|
|
1613
|
+
// the #217 chase adjudicates the rename by NAME (borrowed
|
|
1614
|
+
// renames still exclude other-definition-import) instead
|
|
1615
|
+
// of the empty lookup excluding every legitimate renamed
|
|
1616
|
+
// call as name-not-in-scope.
|
|
1617
|
+
let nameBindings = fileEntry.importBindings.filter(b =>
|
|
1618
|
+
b.name === lookupName ||
|
|
1619
|
+
(calledAs && b.alias === lookupName));
|
|
1620
|
+
// Python bindings carry no alias field — the pairing
|
|
1621
|
+
// lives in fileEntry.importAliases ({original, local});
|
|
1622
|
+
// map the alias back to its original's binding so the
|
|
1623
|
+
// chase adjudicates `from lib import transform as t`.
|
|
1624
|
+
if (calledAs && nameBindings.length === 0 &&
|
|
1625
|
+
(fileEntry.importAliases || []).length > 0) {
|
|
1626
|
+
const originals = new Set(fileEntry.importAliases
|
|
1627
|
+
.filter(a => a && a.local === lookupName && a.original)
|
|
1628
|
+
.map(a => a.original));
|
|
1629
|
+
if (originals.size > 0) {
|
|
1630
|
+
nameBindings = fileEntry.importBindings.filter(b =>
|
|
1631
|
+
originals.has(b.name));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1404
1634
|
// Exact alias pairing (fix #269): two renames of one
|
|
1405
1635
|
// source name from DIFFERENT modules — the record's local
|
|
1406
1636
|
// alias (call.name) picks its own binding; source-name
|
|
@@ -1871,7 +2101,8 @@ function findCallers(index, name, options = {}) {
|
|
|
1871
2101
|
// from the producing annotation's scope — the name
|
|
1872
2102
|
// was written THERE, not in the consuming file.
|
|
1873
2103
|
const identity = _resolveReceiverTypeIdentity(
|
|
1874
|
-
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs
|
|
2104
|
+
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs,
|
|
2105
|
+
call.receiverTypeFlowFile ? undefined : call.line);
|
|
1875
2106
|
if (identity === 'other') {
|
|
1876
2107
|
receiverTypeValidated = false;
|
|
1877
2108
|
} else if (identity === 'unknown') {
|
|
@@ -1899,6 +2130,19 @@ function findCallers(index, name, options = {}) {
|
|
|
1899
2130
|
}
|
|
1900
2131
|
}
|
|
1901
2132
|
}
|
|
2133
|
+
} else if (structural) {
|
|
2134
|
+
// Name-only subtype sets cannot represent an
|
|
2135
|
+
// imported parent that is exposed under a
|
|
2136
|
+
// different type name. Resolve the receiver's
|
|
2137
|
+
// exact declaration and walk its file-scoped
|
|
2138
|
+
// ancestry before treating it as unrelated.
|
|
2139
|
+
const identity = _resolveStructuralFlowTypeIdentity(
|
|
2140
|
+
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs);
|
|
2141
|
+
if (identity === 'target') {
|
|
2142
|
+
receiverTypeValidated = true;
|
|
2143
|
+
} else if (identity === 'unknown') {
|
|
2144
|
+
receiverTypeUnresolved = true;
|
|
2145
|
+
}
|
|
1902
2146
|
}
|
|
1903
2147
|
const matchesTarget = receiverTypeValidated ||
|
|
1904
2148
|
((structural || viaFieldHop) && _isAncestorOfTargetClass(index, knownType, targetDefs));
|
|
@@ -2052,7 +2296,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2052
2296
|
// target's package, not a same-named foreign type.
|
|
2053
2297
|
let identity = 'target';
|
|
2054
2298
|
if (targetDefs.some(d => (d.className || (d.receiver || '').replace(/^\*/, '')) === inferredType)) {
|
|
2055
|
-
identity = _resolveReceiverTypeIdentity(index, filePath, inferredType, targetDefs);
|
|
2299
|
+
identity = _resolveReceiverTypeIdentity(index, filePath, inferredType, targetDefs, call.line);
|
|
2056
2300
|
}
|
|
2057
2301
|
if (identity === 'target') {
|
|
2058
2302
|
inferredMatch = true;
|
|
@@ -2110,7 +2354,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2110
2354
|
// its type is unknown, identity proves nothing.
|
|
2111
2355
|
if (matchesTarget && call.isPathCall &&
|
|
2112
2356
|
langTraits(fileEntry.language)?.typeQualifiedCallStyle === 'path') {
|
|
2113
|
-
const identity = _resolveReceiverTypeIdentity(index, filePath, call.receiver, targetDefs);
|
|
2357
|
+
const identity = _resolveReceiverTypeIdentity(index, filePath, call.receiver, targetDefs, call.line);
|
|
2114
2358
|
if (identity === 'other') {
|
|
2115
2359
|
isUncertain = true;
|
|
2116
2360
|
typeMismatch = true;
|
|
@@ -2383,7 +2627,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2383
2627
|
// name fallback above handles multi-definition names; this
|
|
2384
2628
|
// covers single-definition targets that skip it.
|
|
2385
2629
|
if (typeQualifiedReceiver) {
|
|
2386
|
-
const identity = _resolveReceiverTypeIdentity(index, filePath, receiverName, targetDefs2);
|
|
2630
|
+
const identity = _resolveReceiverTypeIdentity(index, filePath, receiverName, targetDefs2, call.line);
|
|
2387
2631
|
if (identity === 'other') {
|
|
2388
2632
|
recordExcluded(filePath, call.line, 'path-type-mismatch');
|
|
2389
2633
|
continue;
|
|
@@ -2723,6 +2967,30 @@ function findCallers(index, name, options = {}) {
|
|
|
2723
2967
|
if (call.isMethod && !call.receiverIsModule && !recvSubmoduleRel) {
|
|
2724
2968
|
const tTypes = dispatchTargetTypes(targetDefs2);
|
|
2725
2969
|
const typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
|
|
2970
|
+
// A local receiver whose nearest assignment the flow
|
|
2971
|
+
// machinery examined and could NOT type holds a value
|
|
2972
|
+
// of unknown provenance (`local = factory()`) — the
|
|
2973
|
+
// #222(4) evidence class with a local producer, so
|
|
2974
|
+
// single-owner spelling must not confirm it. A bare
|
|
2975
|
+
// never-assigned parameter has no such evidence and
|
|
2976
|
+
// keeps the measured #204/#209 single-owner physics
|
|
2977
|
+
// (`run(codec) { codec.decodeFrames(...) }` stays
|
|
2978
|
+
// confirmed when Codec is the one project owner).
|
|
2979
|
+
if (!typeQualifiedReceiver && call.receiverLocalBinding &&
|
|
2980
|
+
!call.receiverType && !fieldHopType && !fieldDispatchType &&
|
|
2981
|
+
!call.receiverExternalFlow && !call.receiverQualifiedFlow) {
|
|
2982
|
+
let demoteFlowMap = returnFlowCache.get(filePath);
|
|
2983
|
+
if (demoteFlowMap === undefined) {
|
|
2984
|
+
demoteFlowMap = _buildReturnTypeFlowMap(index, filePath, calls);
|
|
2985
|
+
returnFlowCache.set(filePath, demoteFlowMap);
|
|
2986
|
+
}
|
|
2987
|
+
if (_receiverAssignedUntyped(demoteFlowMap, call)) {
|
|
2988
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
2989
|
+
dispatchVia: call.receiverRootType || 'local receiver',
|
|
2990
|
+
});
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2726
2994
|
// External-producer receiver (fix #222, httpx-measured
|
|
2727
2995
|
// — the #220 Go rule for structural languages): the
|
|
2728
2996
|
// variable was assigned from a call into an external
|
|
@@ -2898,9 +3166,13 @@ function findCallers(index, name, options = {}) {
|
|
|
2898
3166
|
// Method calls where binding resolution was skipped (non-self receiver)
|
|
2899
3167
|
// and the receiver has no binding evidence → uncertain (JS/TS/Python only)
|
|
2900
3168
|
isUncertain: !!isUncertain || uncertainMethodReceiver,
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3169
|
+
// Only a receiver type that was validated against the
|
|
3170
|
+
// pinned target earns receiver-hint confidence. Raw
|
|
3171
|
+
// nominal annotations may name an interface, external
|
|
3172
|
+
// type, or unresolved same-name owner; declared-field
|
|
3173
|
+
// hops and local inference participate once validated.
|
|
3174
|
+
hasReceiverType: !!(receiverTypeValidated ||
|
|
3175
|
+
resolvedByTypedAttribute || nominalInferredMatch),
|
|
2904
3176
|
hasReceiverEvidence: !!(call.receiver &&
|
|
2905
3177
|
(fileEntry.bindings || []).some(b => b.name === call.receiver)),
|
|
2906
3178
|
hasImportEvidence: !!bindingId || hasImportLink || !!call.moduleOwnedPath,
|
|
@@ -3312,6 +3584,21 @@ function findCallees(index, definition, options = {}) {
|
|
|
3312
3584
|
if (!isDirectMatch && !isNestedCallback) continue;
|
|
3313
3585
|
if (calleeAccount) calleeAccount.totalSites++;
|
|
3314
3586
|
|
|
3587
|
+
// Parser-proven lexical shadow: a parameter/local named `option`
|
|
3588
|
+
// cannot call an imported/project `option`. A nested local
|
|
3589
|
+
// function with that name is the one safe exception and remains
|
|
3590
|
+
// eligible for exact same-file resolution below.
|
|
3591
|
+
if (call.localShadow && !call.resolvedName && !call.resolvedNames) {
|
|
3592
|
+
const localTarget = (index.symbols.get(call.name) || []).some(s =>
|
|
3593
|
+
s.file === def.file && !NON_CALLABLE_TYPES.has(s.type) &&
|
|
3594
|
+
s.startLine >= def.startLine && s.endLine <= def.endLine &&
|
|
3595
|
+
s.startLine <= call.line);
|
|
3596
|
+
if (!localTarget) {
|
|
3597
|
+
noteSite(siteId, 'excluded', 'local-shadow', call);
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3315
3602
|
// Query-time return flow for an ordinary receiver assignment:
|
|
3316
3603
|
// `v := New(); v.ReadConfig()`. The compiler-declared return type
|
|
3317
3604
|
// is stronger than constructor-name guesses and must participate
|
|
@@ -3511,7 +3798,8 @@ function findCallees(index, definition, options = {}) {
|
|
|
3511
3798
|
externalContract: true,
|
|
3512
3799
|
});
|
|
3513
3800
|
continue;
|
|
3514
|
-
} else if (
|
|
3801
|
+
} else if (!call.receiverType && localTypes &&
|
|
3802
|
+
localTypes.has(call.receiver) && !directReceiverFlow?.type) {
|
|
3515
3803
|
// Resolve method calls on locally-constructed objects:
|
|
3516
3804
|
// bt = Backtester(...); bt.run_backtest() → Backtester.run_backtest
|
|
3517
3805
|
// Go: f.Run() where f is *Framework → Framework.Run (receiver match)
|
|
@@ -3728,7 +4016,21 @@ function findCallees(index, definition, options = {}) {
|
|
|
3728
4016
|
});
|
|
3729
4017
|
continue;
|
|
3730
4018
|
}
|
|
3731
|
-
//
|
|
4019
|
+
// A compiler-typed nominal receiver that does not resolve
|
|
4020
|
+
// to a method on that type must never fall through to the
|
|
4021
|
+
// generic same-name ranking. The receiver may name an
|
|
4022
|
+
// external interface or an unindexed ancestor, so keep
|
|
4023
|
+
// the dispatch visible without inventing one concrete
|
|
4024
|
+
// project callee.
|
|
4025
|
+
if (collectAccount && langTraits(language)?.typeSystem === 'nominal') {
|
|
4026
|
+
noteUnverified(siteId, call, 'possible-dispatch', {
|
|
4027
|
+
dispatchVia: typeName,
|
|
4028
|
+
dispatchCandidates: _countDispatchCandidates(
|
|
4029
|
+
index, typeName, symbols || []),
|
|
4030
|
+
});
|
|
4031
|
+
continue;
|
|
4032
|
+
}
|
|
4033
|
+
// Structural legacy mode retains its historical fallback.
|
|
3732
4034
|
} else if (call.receiverCall && (!call.receiver || call.receiverIsChainRoot) && !call.isConstructor &&
|
|
3733
4035
|
langTraits(language)?.typeSystem === 'nominal') {
|
|
3734
4036
|
// Chained receiver, nominal callee direction (fix #268,
|
|
@@ -3851,7 +4153,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
3851
4153
|
});
|
|
3852
4154
|
continue;
|
|
3853
4155
|
}
|
|
3854
|
-
if (collectAccount && call.receiver && !call.receiverType &&
|
|
4156
|
+
if (collectAccount && call.receiver && !call.receiverIsModule && !call.receiverType &&
|
|
3855
4157
|
!call.receiverCall && !call.isPotentialCallback &&
|
|
3856
4158
|
!['self', 'cls', 'this', 'super', 'Self'].includes(call.receiver)) {
|
|
3857
4159
|
const owners = new Set((index.symbols.get(call.name) || [])
|
|
@@ -3882,11 +4184,21 @@ function findCallees(index, definition, options = {}) {
|
|
|
3882
4184
|
const shadowsBuiltin = !call.receiver && (
|
|
3883
4185
|
fileEntry?.importBindings?.some(b => b.name === call.name) ||
|
|
3884
4186
|
fileEntry?.bindings?.some(b => b.name === call.name));
|
|
3885
|
-
if (!selfShaped && !
|
|
4187
|
+
if (!selfShaped && !call.receiverIsModule && !shadowsBuiltin &&
|
|
4188
|
+
index.isKeyword(call.name, language)) {
|
|
3886
4189
|
noteSite(siteId, 'external', null, call);
|
|
3887
4190
|
continue;
|
|
3888
4191
|
}
|
|
3889
4192
|
|
|
4193
|
+
// A local alias such as `get_style = console.get_style` preserves
|
|
4194
|
+
// the callable relationship, but the surface spelling no longer
|
|
4195
|
+
// lets a definition oracle verify the target directly. Keep it
|
|
4196
|
+
// visible without presenting it as an exact callee.
|
|
4197
|
+
if (call.aliasCall) {
|
|
4198
|
+
noteUnverified(siteId, call, 'alias-call');
|
|
4199
|
+
continue;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
3890
4202
|
// Use resolved name (from alias tracking) if available
|
|
3891
4203
|
// For multi-target aliases (ternary), pick the first that exists in symbol table
|
|
3892
4204
|
let effectiveName = call.resolvedName || call.name;
|
|
@@ -3959,7 +4271,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
3959
4271
|
existing.count += 1;
|
|
3960
4272
|
if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
|
|
3961
4273
|
} else {
|
|
3962
|
-
callees.set(key, { name:
|
|
4274
|
+
callees.set(key, { name: match.name, bindingId: match.bindingId, count: 1,
|
|
3963
4275
|
...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
|
|
3964
4276
|
}
|
|
3965
4277
|
}
|
|
@@ -3991,7 +4303,7 @@ function findCallees(index, definition, options = {}) {
|
|
|
3991
4303
|
existing.count += 1;
|
|
3992
4304
|
if (collectAccount) { existing.sites.push(call.line); existing.siteIds.push(siteId); }
|
|
3993
4305
|
} else {
|
|
3994
|
-
callees.set(key, { name:
|
|
4306
|
+
callees.set(key, { name: match.name, bindingId: match.bindingId, count: 1,
|
|
3995
4307
|
...(collectAccount && { sites: [call.line], siteIds: [siteId] }) });
|
|
3996
4308
|
}
|
|
3997
4309
|
}
|
|
@@ -5363,6 +5675,28 @@ function _lookupReturnTypeFlow(map, call) {
|
|
|
5363
5675
|
return undefined;
|
|
5364
5676
|
}
|
|
5365
5677
|
|
|
5678
|
+
// True when the receiver's NEAREST preceding assignment is a flow tombstone —
|
|
5679
|
+
// the variable provably holds a value the type machinery examined and could
|
|
5680
|
+
// not type (`local = factory()`). This is the #222(4) unknown-provenance
|
|
5681
|
+
// evidence class with a local producer: it demotes single-owner confirmation.
|
|
5682
|
+
// A never-assigned parameter has no entries and keeps the documented
|
|
5683
|
+
// #204/#209 single-owner physics — parameter-ness alone is not provenance
|
|
5684
|
+
// evidence against the one project owner.
|
|
5685
|
+
function _receiverAssignedUntyped(map, call) {
|
|
5686
|
+
if (!map || !call.receiver) return false;
|
|
5687
|
+
const fnScope = call.enclosingFunction ? `${call.enclosingFunction.startLine}` : '';
|
|
5688
|
+
for (const scope of fnScope === '' ? [''] : [fnScope, '']) {
|
|
5689
|
+
const entries = map.get(`${scope}:${call.receiver}`);
|
|
5690
|
+
if (!entries) continue;
|
|
5691
|
+
let best = null;
|
|
5692
|
+
for (const e of entries) {
|
|
5693
|
+
if (e.line < call.line && (!best || e.line > best.line)) best = e;
|
|
5694
|
+
}
|
|
5695
|
+
if (best) return !!best.invalidated;
|
|
5696
|
+
}
|
|
5697
|
+
return false;
|
|
5698
|
+
}
|
|
5699
|
+
|
|
5366
5700
|
// Return-annotation names that must never type a receiver in nominal flow:
|
|
5367
5701
|
// builtin interfaces/primitives whose project implementors UCN cannot see —
|
|
5368
5702
|
// a receiver typed `error` CAN dispatch into a project type's Error() method,
|
|
@@ -5522,6 +5856,48 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
|
|
|
5522
5856
|
}
|
|
5523
5857
|
const sameFile = typeDefs.find(d => d.file === producerFile);
|
|
5524
5858
|
if (sameFile) return finish({ fromFile: sameFile.file });
|
|
5859
|
+
// An explicit imported name outranks directory proximity. Structural
|
|
5860
|
+
// modules commonly contain several same-named classes in sibling files;
|
|
5861
|
+
// `import { Hono } from './hono'` still pins one declaration exactly.
|
|
5862
|
+
const producerEntry = index.files.get(producerFile);
|
|
5863
|
+
const namedBindings = (producerEntry?.importBindings || []).filter(b =>
|
|
5864
|
+
b.name === typeName || b.alias === typeName);
|
|
5865
|
+
if (namedBindings.length > 0) {
|
|
5866
|
+
const resolvedFiles = new Set();
|
|
5867
|
+
for (const b of namedBindings) {
|
|
5868
|
+
const rel = producerEntry.moduleResolved?.[b.module];
|
|
5869
|
+
if (rel) resolvedFiles.add(path.join(index.root, rel));
|
|
5870
|
+
}
|
|
5871
|
+
const bound = typeDefs.filter(d => resolvedFiles.has(d.file));
|
|
5872
|
+
if (new Set(bound.map(d => d.file)).size === 1) {
|
|
5873
|
+
return finish({ fromFile: bound[0].file });
|
|
5874
|
+
}
|
|
5875
|
+
// The binding may land on a barrel. Chase the imported NAME through
|
|
5876
|
+
// modeled re-exports, and pin only one reachable declaration.
|
|
5877
|
+
const reachable = typeDefs.filter(d => [...resolvedFiles].some(start =>
|
|
5878
|
+
_nameBindingReaches(index, start, typeName, new Set([d.file])) === 'yes'));
|
|
5879
|
+
if (new Set(reachable.map(d => d.file)).size === 1) {
|
|
5880
|
+
return finish({ fromFile: reachable[0].file });
|
|
5881
|
+
}
|
|
5882
|
+
// An UN-modelable export surface is 'unknown', never negative
|
|
5883
|
+
// evidence (#217) — Rust `pub use` chains defeat the name-level
|
|
5884
|
+
// chase, and returning null here unpinned every `use clap::Command`
|
|
5885
|
+
// fold root (clap confirmed placement 1432 → 10). Fall back to the
|
|
5886
|
+
// measured #258 FILE-level unique-reach rail, anchored at the
|
|
5887
|
+
// binding's own resolved module files so same-directory declarations
|
|
5888
|
+
// stay shadowed (the structural parallel-API protection) and only
|
|
5889
|
+
// the declared module's closure can pin.
|
|
5890
|
+
if (resolvedFiles.size > 0) {
|
|
5891
|
+
const fileReachable = typeDefs.filter(d => [...resolvedFiles].some(start =>
|
|
5892
|
+
start === d.file || _importReaches(index, start, new Set([d.file]))));
|
|
5893
|
+
if (new Set(fileReachable.map(d => d.file)).size === 1) {
|
|
5894
|
+
return finish({ fromFile: fileReachable[0].file });
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
// An explicit binding shadows same-directory declarations even when
|
|
5898
|
+
// its external or dynamic export surface cannot be resolved.
|
|
5899
|
+
return finish(null);
|
|
5900
|
+
}
|
|
5525
5901
|
const dir = path.dirname(producerFile);
|
|
5526
5902
|
const sameDir = typeDefs.filter(d => path.dirname(d.file) === dir);
|
|
5527
5903
|
if (sameDir.length === 1) return finish({ fromFile: sameDir[0].file });
|
|
@@ -6060,19 +6436,46 @@ function _sameNominalPackageDir(dirA, dirB, language) {
|
|
|
6060
6436
|
return a === norm(dirB);
|
|
6061
6437
|
}
|
|
6062
6438
|
|
|
6063
|
-
function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs) {
|
|
6439
|
+
function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs, line) {
|
|
6064
6440
|
const typeDefs = (index.symbols.get(knownType) || []).filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
6065
6441
|
if (typeDefs.length <= 1) return 'target';
|
|
6066
|
-
const
|
|
6442
|
+
const fileEntry = index.files.get(filePath);
|
|
6443
|
+
const language = fileEntry?.language;
|
|
6067
6444
|
const targetDirs = new Set(targetDefs.map(d => d.file && path.dirname(d.file)).filter(Boolean));
|
|
6068
6445
|
const inTargetPkg = (d) => d.file && [...targetDirs].some(dir =>
|
|
6069
6446
|
_sameNominalPackageDir(path.dirname(d.file), dir, language));
|
|
6070
|
-
|
|
6447
|
+
// Explicit imports outrank package proximity. This matters in Java files
|
|
6448
|
+
// that sit beside `Token.Comment` but import `org.jsoup.nodes.Comment`.
|
|
6449
|
+
// A nested type is not a package member and cannot win a bare type lookup.
|
|
6450
|
+
const namedBindings = (fileEntry?.importBindings || []).filter(b =>
|
|
6451
|
+
b.name === knownType || b.alias === knownType);
|
|
6452
|
+
if (namedBindings.length > 0) {
|
|
6453
|
+
const resolved = new Set(namedBindings.map(b => fileEntry.moduleResolved?.[b.module])
|
|
6454
|
+
.filter(Boolean).map(rel => path.join(index.root, rel)));
|
|
6455
|
+
const imported = typeDefs.filter(d => resolved.has(d.file));
|
|
6456
|
+
if (imported.length > 0) return imported.some(inTargetPkg) ? 'target' : 'other';
|
|
6457
|
+
return 'unknown';
|
|
6458
|
+
}
|
|
6459
|
+
const callerClass = line != null
|
|
6460
|
+
? index.findEnclosingFunction(filePath, line, true)?.className : null;
|
|
6461
|
+
const sameFile = typeDefs.filter(d => d.file === filePath &&
|
|
6462
|
+
(!d.enclosingType || d.enclosingType === callerClass));
|
|
6071
6463
|
if (sameFile.length > 0) return sameFile.some(inTargetPkg) ? 'target' : 'other';
|
|
6072
6464
|
const callerDir = path.dirname(filePath);
|
|
6073
6465
|
const sameDir = typeDefs.filter(d => d.file &&
|
|
6466
|
+
!d.enclosingType &&
|
|
6074
6467
|
_sameNominalPackageDir(path.dirname(d.file), callerDir, language));
|
|
6075
6468
|
if (sameDir.length > 0) return sameDir.some(inTargetPkg) ? 'target' : 'other';
|
|
6469
|
+
if (language === 'java') {
|
|
6470
|
+
const wildcardPackages = (fileEntry?.imports || [])
|
|
6471
|
+
.filter(m => String(m).endsWith('.*'))
|
|
6472
|
+
.map(m => String(m).slice(0, -2));
|
|
6473
|
+
if (wildcardPackages.length > 0) {
|
|
6474
|
+
const imported = typeDefs.filter(d => !d.enclosingType && d.relativePath &&
|
|
6475
|
+
wildcardPackages.includes(_javaPackageKey(d.relativePath).replace(/\//g, '.')));
|
|
6476
|
+
if (imported.length > 0) return imported.some(inTargetPkg) ? 'target' : 'other';
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6076
6479
|
const imports = index.importGraph.get(filePath);
|
|
6077
6480
|
if (imports) {
|
|
6078
6481
|
const imported = typeDefs.filter(d => d.file && imports.has(d.file));
|
|
@@ -6107,7 +6510,7 @@ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, target
|
|
|
6107
6510
|
const files = new Set();
|
|
6108
6511
|
for (const mf of methodFiles) {
|
|
6109
6512
|
for (const td of typeDefs) {
|
|
6110
|
-
if (td.file === mf
|
|
6513
|
+
if (td.file === mf) files.add(td.file);
|
|
6111
6514
|
}
|
|
6112
6515
|
if (files.size === 0) files.add(mf);
|
|
6113
6516
|
}
|
|
@@ -6118,28 +6521,70 @@ function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, target
|
|
|
6118
6521
|
if (!file) return 'unknown';
|
|
6119
6522
|
const files = targetFiles.get(name) || new Set();
|
|
6120
6523
|
if (files.has(file)) return 'target';
|
|
6121
|
-
|
|
6122
|
-
return sameDir ? 'target' : 'other';
|
|
6524
|
+
return 'other';
|
|
6123
6525
|
};
|
|
6124
6526
|
const direct = ownerIdentity(knownType, origin.fromFile);
|
|
6125
|
-
if (direct) return direct;
|
|
6527
|
+
if (direct === 'target') return direct;
|
|
6528
|
+
|
|
6529
|
+
const targetKeys = new Set(targetDefs
|
|
6530
|
+
.filter(d => d.file && d.startLine != null)
|
|
6531
|
+
.map(d => `${d.file}:${d.startLine}`));
|
|
6532
|
+
const overridesPinnedSlot = (name, file) => {
|
|
6533
|
+
if (!file) return false;
|
|
6534
|
+
const methodName = targetDefs[0]?.name;
|
|
6535
|
+
if (!methodName) return false;
|
|
6536
|
+
return (index.symbols.get(methodName) || []).some(d =>
|
|
6537
|
+
d.file === file && d.className === name &&
|
|
6538
|
+
!targetKeys.has(`${d.file}:${d.startLine}`));
|
|
6539
|
+
};
|
|
6540
|
+
|
|
6541
|
+
// Resolve a parent as written in the child file. Import bindings know the
|
|
6542
|
+
// module file, while export details recover the source-side type name for
|
|
6543
|
+
// `export { Internal as PublicBase }`. Keeping both pieces prevents
|
|
6544
|
+
// same-named classes in other files from being conflated.
|
|
6545
|
+
const parentOrigin = (parent, childFile) => {
|
|
6546
|
+
const fe = index.files.get(childFile);
|
|
6547
|
+
if (fe) {
|
|
6548
|
+
const alias = (fe.importAliases || []).find(a => a.local === parent);
|
|
6549
|
+
const importedName = alias?.original || parent;
|
|
6550
|
+
const binding = (fe.importBindings || []).find(b =>
|
|
6551
|
+
b.name === importedName || b.alias === parent);
|
|
6552
|
+
const rel = binding && fe.moduleResolved?.[binding.module];
|
|
6553
|
+
if (rel) {
|
|
6554
|
+
const parentFile = path.join(index.root, rel);
|
|
6555
|
+
const parentEntry = index.files.get(parentFile);
|
|
6556
|
+
const exported = (parentEntry?.exportDetails || []).find(e =>
|
|
6557
|
+
(e.alias || e.name) === importedName);
|
|
6558
|
+
const canonical = exported?.name || importedName;
|
|
6559
|
+
const ownsCanonical = (index.symbols.get(canonical) || []).some(d =>
|
|
6560
|
+
IDENTITY_TYPE_KINDS.has(d.type) && d.file === parentFile);
|
|
6561
|
+
return { name: ownsCanonical ? canonical : importedName, file: parentFile };
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
const parentFile = index._resolveClassFile
|
|
6565
|
+
? index._resolveClassFile(parent, childFile) : undefined;
|
|
6566
|
+
return { name: parent, file: parentFile };
|
|
6567
|
+
};
|
|
6126
6568
|
|
|
6127
6569
|
const queue = [{ name: knownType, file: origin.fromFile }];
|
|
6128
6570
|
const visited = new Set();
|
|
6129
|
-
let sawUnresolved =
|
|
6571
|
+
let sawUnresolved = direct === 'unknown';
|
|
6130
6572
|
while (queue.length > 0 && visited.size < 128) {
|
|
6131
6573
|
const cur = queue.shift();
|
|
6132
6574
|
const key = `${cur.file || ''}\0${cur.name}`;
|
|
6133
6575
|
if (visited.has(key)) continue;
|
|
6134
6576
|
visited.add(key);
|
|
6577
|
+
// An override on the concrete receiver intercepts dispatch before the
|
|
6578
|
+
// ancestor slot. In that case ancestry is evidence for the other
|
|
6579
|
+
// definition, not the pinned one.
|
|
6580
|
+
if (overridesPinnedSlot(cur.name, cur.file)) continue;
|
|
6135
6581
|
const parents = index._getInheritanceParents(cur.name, cur.file) || [];
|
|
6136
6582
|
for (const parent of parents) {
|
|
6137
|
-
const
|
|
6138
|
-
|
|
6139
|
-
const verdict = ownerIdentity(parent, pFile);
|
|
6583
|
+
const edge = parentOrigin(parent, cur.file);
|
|
6584
|
+
const verdict = ownerIdentity(edge.name, edge.file);
|
|
6140
6585
|
if (verdict === 'target') return 'target';
|
|
6141
6586
|
if (verdict === 'unknown') sawUnresolved = true;
|
|
6142
|
-
if (
|
|
6587
|
+
if (edge.file) queue.push(edge);
|
|
6143
6588
|
else sawUnresolved = true;
|
|
6144
6589
|
}
|
|
6145
6590
|
}
|
|
@@ -6355,7 +6800,7 @@ function _goQualifierNamesImport(index, fieldFile, qualifier) {
|
|
|
6355
6800
|
function _calleeStructuralModuleRoute(index, fileEntry, call, language) {
|
|
6356
6801
|
const bindings = (fileEntry?.importBindings || []).filter(b => b.name === call.receiver);
|
|
6357
6802
|
if (bindings.length === 0) return { unknown: true };
|
|
6358
|
-
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, call.name);
|
|
6803
|
+
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, call.name, false);
|
|
6359
6804
|
}
|
|
6360
6805
|
|
|
6361
6806
|
function _calleeStructuralImportedNameRoute(index, fileEntry, call, language) {
|
|
@@ -6366,15 +6811,10 @@ function _calleeStructuralImportedNameRoute(index, fileEntry, call, language) {
|
|
|
6366
6811
|
if (paired.length > 0) bindings = paired;
|
|
6367
6812
|
}
|
|
6368
6813
|
if (bindings.length === 0) return null;
|
|
6369
|
-
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, lookupName);
|
|
6814
|
+
return _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, lookupName, true);
|
|
6370
6815
|
}
|
|
6371
6816
|
|
|
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));
|
|
6817
|
+
function _calleeStructuralBindingRoute(index, fileEntry, call, language, bindings, exportName, allowDefaultExport) {
|
|
6378
6818
|
const matches = new Map();
|
|
6379
6819
|
let sawProjectish = false;
|
|
6380
6820
|
let sawUnknown = false;
|
|
@@ -6392,17 +6832,105 @@ function _calleeStructuralBindingRoute(index, fileEntry, call, language, binding
|
|
|
6392
6832
|
}
|
|
6393
6833
|
sawProjectish = true;
|
|
6394
6834
|
const moduleFile = path.join(index.root, rel);
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6835
|
+
const routed = _calleeExportDefinitions(index, moduleFile, exportName, language, call, {
|
|
6836
|
+
allowDefaultExport: allowDefaultExport && !!binding.defaultLike,
|
|
6837
|
+
});
|
|
6838
|
+
for (const d of routed.matches) {
|
|
6839
|
+
matches.set(`${d.file}:${d.startLine}`, d);
|
|
6399
6840
|
}
|
|
6841
|
+
if (routed.unknown) sawUnknown = true;
|
|
6400
6842
|
}
|
|
6401
6843
|
if (matches.size > 0) return { matches: [...matches.values()] };
|
|
6402
6844
|
if (!sawProjectish && !sawUnknown) return { external: true };
|
|
6403
6845
|
return { unknown: true };
|
|
6404
6846
|
}
|
|
6405
6847
|
|
|
6848
|
+
// Resolve a module attribute to its implementation definition while
|
|
6849
|
+
// preserving identity across ESM/TS barrel renames and Python re-export
|
|
6850
|
+
// imports. Searching only symbols named with the public spelling loses
|
|
6851
|
+
// `export { _gt as gt }`, because its implementation is named `_gt`.
|
|
6852
|
+
function _calleeExportDefinitions(index, startAbs, exposedName, language, call, options = {}) {
|
|
6853
|
+
const matches = new Map();
|
|
6854
|
+
const visited = new Set();
|
|
6855
|
+
let unknown = false;
|
|
6856
|
+
let frontier = [[startAbs, exposedName]];
|
|
6857
|
+
const shapeMatches = d =>
|
|
6858
|
+
(!NON_CALLABLE_TYPES.has(d.type) ||
|
|
6859
|
+
(call.isConstructor && d.type === 'class') ||
|
|
6860
|
+
(langTraits(language)?.classesCallableWithoutNew && d.type === 'class')) &&
|
|
6861
|
+
_calleeLanguageCompatible(index, d, language);
|
|
6862
|
+
|
|
6863
|
+
for (let depth = 0; depth <= 4 && frontier.length > 0; depth++) {
|
|
6864
|
+
const next = [];
|
|
6865
|
+
for (const [abs, attr] of frontier) {
|
|
6866
|
+
const stateKey = `${abs}\x00${attr}`;
|
|
6867
|
+
if (visited.has(stateKey)) continue;
|
|
6868
|
+
visited.add(stateKey);
|
|
6869
|
+
const fe = index.files.get(abs);
|
|
6870
|
+
if (!fe) { unknown = true; continue; }
|
|
6871
|
+
|
|
6872
|
+
const enqueue = (module, nextAttr) => {
|
|
6873
|
+
const rel = fe.moduleResolved && fe.moduleResolved[module];
|
|
6874
|
+
if (!rel) {
|
|
6875
|
+
const mod = String(module || '');
|
|
6876
|
+
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
6877
|
+
if (mod.startsWith('.') ||
|
|
6878
|
+
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) unknown = true;
|
|
6879
|
+
return;
|
|
6880
|
+
}
|
|
6881
|
+
next.push([path.join(index.root, rel), nextAttr]);
|
|
6882
|
+
};
|
|
6883
|
+
|
|
6884
|
+
const details = fe.exportDetails || [];
|
|
6885
|
+
const localExposed = fe.language === 'python' ||
|
|
6886
|
+
(fe.exports || []).includes(attr) ||
|
|
6887
|
+
details.some(e => !e.source && (e.alias || e.name) === attr);
|
|
6888
|
+
if (localExposed) {
|
|
6889
|
+
for (const d of (index.symbols.get(attr) || [])) {
|
|
6890
|
+
if (d.file === abs && shapeMatches(d)) {
|
|
6891
|
+
matches.set(`${d.file}:${d.startLine}`, d);
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
|
|
6896
|
+
// A simple CommonJS require binds the value assigned directly to
|
|
6897
|
+
// module.exports. This applies to a bare imported-name call only,
|
|
6898
|
+
// never to namespace.member() routing.
|
|
6899
|
+
if (options.allowDefaultExport && depth === 0) {
|
|
6900
|
+
for (const e of details) {
|
|
6901
|
+
if (e.type !== 'module.exports' || !e.name) continue;
|
|
6902
|
+
for (const d of (index.symbols.get(e.name) || [])) {
|
|
6903
|
+
if (d.file === abs && shapeMatches(d)) {
|
|
6904
|
+
matches.set(`${d.file}:${d.startLine}`, d);
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6910
|
+
for (const e of details) {
|
|
6911
|
+
if (!e.source) continue;
|
|
6912
|
+
if (e.type === 're-export' && (e.alias || e.name) === attr) enqueue(e.source, e.name);
|
|
6913
|
+
else if (e.type === 're-export-all') {
|
|
6914
|
+
if (e.alias) { if (e.alias === attr) unknown = true; }
|
|
6915
|
+
else enqueue(e.source, attr);
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
const aliases = fe.importAliases || [];
|
|
6919
|
+
for (const b of (fe.importBindings || [])) {
|
|
6920
|
+
const exposed = [b.alias || b.name,
|
|
6921
|
+
...aliases.filter(a => a.original === b.name).map(a => a.local)];
|
|
6922
|
+
if (exposed.includes(attr)) enqueue(b.module, b.name);
|
|
6923
|
+
}
|
|
6924
|
+
if ((fe.importNames || []).includes('*')) unknown = true;
|
|
6925
|
+
if ((fe.moduleAssignedNames || []).includes(attr)) unknown = true;
|
|
6926
|
+
if ((index.symbols.get('__getattr__') || []).some(s => s.file === abs && !s.className)) unknown = true;
|
|
6927
|
+
}
|
|
6928
|
+
frontier = next;
|
|
6929
|
+
}
|
|
6930
|
+
if (frontier.length > 0) unknown = true;
|
|
6931
|
+
return { matches: [...matches.values()], unknown };
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6406
6934
|
function _calleeGoPackageMatch(index, call, importModule) {
|
|
6407
6935
|
const allSymbols = index.symbols.get(call.name);
|
|
6408
6936
|
if (!allSymbols) return null;
|
|
@@ -7260,6 +7788,28 @@ function _buildTargetTypeSet(index, targetDefs, definitions) {
|
|
|
7260
7788
|
}
|
|
7261
7789
|
}
|
|
7262
7790
|
}
|
|
7791
|
+
// Rust Deref changes method lookup identity: methods on Target are
|
|
7792
|
+
// callable through the wrapper value. Close only over wrappers whose
|
|
7793
|
+
// indexed type definitions all agree on one Deref target.
|
|
7794
|
+
if (targetTypes.size > 0) {
|
|
7795
|
+
const derefPairs = [];
|
|
7796
|
+
for (const [wrapper, defs] of index.symbols) {
|
|
7797
|
+
const typeDefs = defs.filter(d => IDENTITY_TYPE_KINDS.has(d.type));
|
|
7798
|
+
if (typeDefs.length === 0 || !typeDefs.every(d => d.derefTarget)) continue;
|
|
7799
|
+
const targets = new Set(typeDefs.map(d => d.derefTarget));
|
|
7800
|
+
if (targets.size === 1) derefPairs.push([wrapper, [...targets][0]]);
|
|
7801
|
+
}
|
|
7802
|
+
let changed = derefPairs.length > 0;
|
|
7803
|
+
while (changed) {
|
|
7804
|
+
changed = false;
|
|
7805
|
+
for (const [wrapper, target] of derefPairs) {
|
|
7806
|
+
if (targetTypes.has(target) && !targetTypes.has(wrapper)) {
|
|
7807
|
+
targetTypes.add(wrapper);
|
|
7808
|
+
changed = true;
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
}
|
|
7263
7813
|
// Type aliases are the SAME type (Rust `pub type StyledString =
|
|
7264
7814
|
// SpannedString<Style>`, Go `type A = B`) — compiler-checked identity,
|
|
7265
7815
|
// not a subtype edge. Close over them in BOTH directions: a method on
|
|
@@ -7720,6 +8270,21 @@ const _STRUCTURAL_FLOW_REJECT = new Set([
|
|
|
7720
8270
|
'this', 'Self', 'Object', 'None',
|
|
7721
8271
|
]);
|
|
7722
8272
|
|
|
8273
|
+
// Small, stable stdlib contracts used only to type the value of a chained
|
|
8274
|
+
// receiver. These are runtime identity facts, not guesses about user code.
|
|
8275
|
+
function _builtinMethodReturnType(language, receiverType, methodName) {
|
|
8276
|
+
if (language !== 'python') return null;
|
|
8277
|
+
const owner = String(receiverType || '').split('.').pop();
|
|
8278
|
+
if (methodName === 'getvalue') {
|
|
8279
|
+
if (owner === 'BytesIO') return 'bytes';
|
|
8280
|
+
if (owner === 'StringIO') return 'str';
|
|
8281
|
+
}
|
|
8282
|
+
if (owner === 'bytes' && methodName === 'decode') return 'str';
|
|
8283
|
+
if (owner === 'str' && methodName === 'encode') return 'bytes';
|
|
8284
|
+
if (['list', 'dict', 'set'].includes(owner) && methodName === 'copy') return owner;
|
|
8285
|
+
return null;
|
|
8286
|
+
}
|
|
8287
|
+
|
|
7723
8288
|
/**
|
|
7724
8289
|
* Type a chained receiver from its producer's declared return annotation
|
|
7725
8290
|
* (fix #219): `parseAsync(args).catch(...)` — the receiver of .catch IS the
|
|
@@ -8125,6 +8690,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
8125
8690
|
}
|
|
8126
8691
|
if (rt && rt.externalVia) return rt;
|
|
8127
8692
|
if (rt && rt.type) {
|
|
8693
|
+
// Stdlib return contracts apply only when no project type shadows
|
|
8694
|
+
// the name (#232 shadowing rule): a project class named StringIO
|
|
8695
|
+
// keeps its own annotations authoritative.
|
|
8696
|
+
const projectShadow = (index.symbols.get(
|
|
8697
|
+
String(rt.type).split('.').pop()) || [])
|
|
8698
|
+
.some(d => NON_CALLABLE_TYPES.has(d.type) && d.type !== 'field');
|
|
8699
|
+
if (!projectShadow) {
|
|
8700
|
+
const builtinReturn = _builtinMethodReturnType(language, rt.type, name);
|
|
8701
|
+
if (builtinReturn) return { type: builtinReturn };
|
|
8702
|
+
}
|
|
8128
8703
|
return _methodReturnOnType(index, rt.type, rt.fromFile, name, language,
|
|
8129
8704
|
{ filePath, consumerAwaited });
|
|
8130
8705
|
}
|