ucn 5.0.6 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +1 -1
- package/.claude/skills/ucn/references/trust-contract.md +3 -2
- package/README.md +3 -1
- package/core/analysis.js +66 -1
- package/core/bridge.js +2 -1
- package/core/cache.js +37 -8
- package/core/callers.js +484 -37
- package/core/index-ir.js +14 -4
- package/core/ir.js +48 -2
- package/core/output/analysis.js +25 -0
- package/core/output/shared.js +2 -2
- package/languages/javascript.js +203 -18
- package/languages/python.js +79 -2
- package/mcp/server.js +1 -1
- package/package.json +1 -1
package/core/callers.js
CHANGED
|
@@ -148,6 +148,54 @@ function _javaPackageKey(relativePath) {
|
|
|
148
148
|
return path.posix.dirname(packagePath);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Resolve a capitalized Java receiver NAME through the file's static imports
|
|
153
|
+
* to a static FIELD's declared type (fix #286c, jsoup-measured: `import
|
|
154
|
+
* static ...SimpleBufferedInput.BufferPool` binds `BufferPool.borrow()` to
|
|
155
|
+
* the SoftPool-typed field — javac resolves single-static-imports ahead of
|
|
156
|
+
* the capitalized static-call reading). Returns the declared type HEAD, or
|
|
157
|
+
* null when the name isn't a static-imported field (a static import naming a
|
|
158
|
+
* nested TYPE keeps type-qualified semantics), the field has no usable
|
|
159
|
+
* declared type, or the type head is package-qualified (unpinnable here —
|
|
160
|
+
* the #268(4a) qualified-field discipline).
|
|
161
|
+
*/
|
|
162
|
+
function _javaStaticImportedFieldType(index, fileEntry, receiverName) {
|
|
163
|
+
if (!receiverName || !fileEntry) return null;
|
|
164
|
+
// Single-static-imports only: the import record cannot distinguish a
|
|
165
|
+
// STATIC wildcard (`import static Owner.*` — imports fields) from a
|
|
166
|
+
// package wildcard (`import com.ex.*` — types only), and typing a
|
|
167
|
+
// receiver from a field javac never binds could exclude a true edge.
|
|
168
|
+
const candidates = [];
|
|
169
|
+
for (const im of (fileEntry.importBindings || [])) {
|
|
170
|
+
const mod = String(im.module || '');
|
|
171
|
+
if (im.name === receiverName && mod.endsWith('.' + receiverName)) {
|
|
172
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[mod];
|
|
173
|
+
if (rel) candidates.push(rel);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const rel of candidates) {
|
|
178
|
+
const entry = index.files.get(path.join(index.root, rel));
|
|
179
|
+
if (!entry) continue;
|
|
180
|
+
// The import binding shape also matches plain class imports
|
|
181
|
+
// (`import com.ex.BufferPool`): any resolved TYPE of the name —
|
|
182
|
+
// top-level or nested — keeps static-call semantics.
|
|
183
|
+
const namedType = (entry.symbols || []).some(s =>
|
|
184
|
+
s.name === receiverName &&
|
|
185
|
+
['class', 'interface', 'enum', 'record', 'annotation'].includes(s.type));
|
|
186
|
+
if (namedType) return null;
|
|
187
|
+
const field = (entry.symbols || []).find(s =>
|
|
188
|
+
s.name === receiverName && s.className &&
|
|
189
|
+
(s.type === 'field' || s.memberType === 'field') &&
|
|
190
|
+
(s.modifiers || []).includes('static'));
|
|
191
|
+
if (!field) continue;
|
|
192
|
+
const head = String(field.fieldType || '').replace(/<[\s\S]*$/, '').trim();
|
|
193
|
+
if (!head || head.includes('.') || head.endsWith('[]')) return null;
|
|
194
|
+
return head;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
151
199
|
function _javaConstructorDisposition(index, filePath, fileEntry, call, targetDefs) {
|
|
152
200
|
const typeKinds = new Set(['class', 'record', 'enum']);
|
|
153
201
|
const targets = (targetDefs || []).filter(d => typeKinds.has(d.type));
|
|
@@ -271,6 +319,8 @@ function findCallers(index, name, options = {}) {
|
|
|
271
319
|
const typeQueryTargets = options.targetDefinitions || definitions;
|
|
272
320
|
const targetIsTypeQuery = typeQueryTargets.length > 0 &&
|
|
273
321
|
typeQueryTargets.every(target => IDENTITY_TYPE_KINDS.has(target.type));
|
|
322
|
+
const targetDefinitionFiles = new Set(
|
|
323
|
+
typeQueryTargets.map(definition => definition.file).filter(Boolean));
|
|
274
324
|
|
|
275
325
|
// Possible-dispatch tiering inputs (nominal contract surface) — all fixed
|
|
276
326
|
// per query, computed lazily once. targetTypes mirrors the receiver-class
|
|
@@ -435,7 +485,8 @@ function findCallers(index, name, options = {}) {
|
|
|
435
485
|
const moduleFile = path.join(index.root, rel);
|
|
436
486
|
if (!aliasTargetFiles.has(moduleFile)) continue;
|
|
437
487
|
const exported = index.files.get(moduleFile)?.exportDetails || [];
|
|
438
|
-
if (!exported.some(e => e.type === 'module.exports' &&
|
|
488
|
+
if (!exported.some(e => e.type === 'module.exports' &&
|
|
489
|
+
e.defaultLike && (e.localName || e.name) === name)) continue;
|
|
439
490
|
if (!importAliasLocals.has(fp)) importAliasLocals.set(fp, new Set());
|
|
440
491
|
importAliasLocals.get(fp).add(b.alias || b.name);
|
|
441
492
|
}
|
|
@@ -557,6 +608,8 @@ function findCallers(index, name, options = {}) {
|
|
|
557
608
|
try {
|
|
558
609
|
const calls = getCachedCalls(index, filePath);
|
|
559
610
|
if (!calls) continue;
|
|
611
|
+
const structuralLanguage =
|
|
612
|
+
langTraits(fileEntry.language)?.typeSystem === 'structural';
|
|
560
613
|
|
|
561
614
|
for (let call of calls) {
|
|
562
615
|
// Skip if not matching our target name (also check alias resolution)
|
|
@@ -1792,6 +1845,21 @@ function findCallers(index, name, options = {}) {
|
|
|
1792
1845
|
options.targetDefinitions || definitions);
|
|
1793
1846
|
if (resolvedByExtensionMethod) receiverTypeValidated = true;
|
|
1794
1847
|
}
|
|
1848
|
+
// A static-imported FIELD shadows the capitalized static-
|
|
1849
|
+
// qualifier reading (fix #286c, jsoup-measured): javac binds
|
|
1850
|
+
// `BufferPool.borrow()` to the SoftPool-typed field imported
|
|
1851
|
+
// by `import static ...SimpleBufferedInput.BufferPool`, never
|
|
1852
|
+
// to a class named BufferPool. The declared type is compiler-
|
|
1853
|
+
// true — feed it to the normal nominal receiver physics.
|
|
1854
|
+
if (call.isMethod && !call.receiverType && call.receiver &&
|
|
1855
|
+
fileEntry.language === 'java' && /^[A-Z]/.test(call.receiver)) {
|
|
1856
|
+
const staticFieldType = _javaStaticImportedFieldType(
|
|
1857
|
+
index, fileEntry, call.receiver);
|
|
1858
|
+
if (staticFieldType) {
|
|
1859
|
+
call = { ...call, receiverType: staticFieldType,
|
|
1860
|
+
receiverIsTypeQualified: false };
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1795
1863
|
if (collectAccount && call.isMethod && !call.receiverType &&
|
|
1796
1864
|
fileEntry.language === 'java' && /^[A-Z]/.test(call.receiver || '')) {
|
|
1797
1865
|
// Java permits inherited static methods to be invoked
|
|
@@ -2242,6 +2310,16 @@ function findCallers(index, name, options = {}) {
|
|
|
2242
2310
|
}
|
|
2243
2311
|
}
|
|
2244
2312
|
|
|
2313
|
+
// Resolve this before the generic untyped-field guard below:
|
|
2314
|
+
// `api.nested.run()` is syntactically a field receiver, but an
|
|
2315
|
+
// exported namespace chain can make both hops compiler-exact.
|
|
2316
|
+
const recvExportedNamespace = (!call.receiverIsModule && call.isMethod &&
|
|
2317
|
+
(call.receiver || call.receiverRoot) &&
|
|
2318
|
+
structuralLanguage)
|
|
2319
|
+
? _importedNamespaceMemberOwnership(
|
|
2320
|
+
index, fileEntry, call, targetDefinitionFiles)
|
|
2321
|
+
: null;
|
|
2322
|
+
|
|
2245
2323
|
// Go package-owned value receiver (`io.Discard.Write`). The
|
|
2246
2324
|
// imported package owns `Discard`; file/package method-name
|
|
2247
2325
|
// bindings cannot identify its concrete type. Keep the edge
|
|
@@ -2263,7 +2341,7 @@ function findCallers(index, name, options = {}) {
|
|
|
2263
2341
|
// allowing a same-file `query` definition to claim it.
|
|
2264
2342
|
if (collectAccount && call.isMethod && call.receiverField &&
|
|
2265
2343
|
!call.receiverType && !fieldHopType && !fieldDispatchType &&
|
|
2266
|
-
!resolvedBySameClass &&
|
|
2344
|
+
!resolvedBySameClass && !recvExportedNamespace &&
|
|
2267
2345
|
['javascript', 'typescript', 'tsx', 'html'].includes(
|
|
2268
2346
|
fileEntry.language)) {
|
|
2269
2347
|
routeUnverified(
|
|
@@ -2553,7 +2631,9 @@ function findCallers(index, name, options = {}) {
|
|
|
2553
2631
|
continue;
|
|
2554
2632
|
}
|
|
2555
2633
|
const resolvedAbs = path.join(index.root, rel);
|
|
2556
|
-
const verdict =
|
|
2634
|
+
const verdict = b.defaultLike
|
|
2635
|
+
? _defaultBindingReaches(index, resolvedAbs, tFiles)
|
|
2636
|
+
: _nameBindingReaches(index, resolvedAbs, b.name, tFiles);
|
|
2557
2637
|
if (verdict === 'yes') { reaches = true; break; }
|
|
2558
2638
|
if (verdict === 'unknown') undetermined = true;
|
|
2559
2639
|
}
|
|
@@ -2731,12 +2811,18 @@ function findCallers(index, name, options = {}) {
|
|
|
2731
2811
|
const recvSubmoduleRel = (!call.receiverIsModule && call.isMethod && call.receiver &&
|
|
2732
2812
|
langTraits(fileEntry.language)?.typeSystem === 'structural')
|
|
2733
2813
|
? _submoduleReceiverModule(index, fileEntry, call.receiver) : null;
|
|
2814
|
+
// A named/default import can itself be an exported namespace
|
|
2815
|
+
// object: `import { z } from './index'; z.string()`, where
|
|
2816
|
+
// index does `import * as z from './api'; export { z }`.
|
|
2817
|
+
// Preserve that compiler-exact namespace identity without
|
|
2818
|
+
// treating arbitrary imported object values as modules.
|
|
2734
2819
|
|
|
2735
2820
|
// Module receiver: httpx.get() / ns.helper() dispatches to a
|
|
2736
2821
|
// module export — it can never be a CLASS METHOD call. Applies
|
|
2737
2822
|
// only when every target is a class method; standalone-function
|
|
2738
2823
|
// and class (constructor) targets keep flowing on import evidence.
|
|
2739
|
-
if (!bindingId && !resolvedBySameClass && call.isMethod &&
|
|
2824
|
+
if ((!bindingId || recvExportedNamespace) && !resolvedBySameClass && call.isMethod &&
|
|
2825
|
+
(call.receiverIsModule || recvExportedNamespace) &&
|
|
2740
2826
|
langTraits(fileEntry.language)?.typeSystem === 'structural' &&
|
|
2741
2827
|
targetDefs.length > 0 && targetDefs.every(d => d.className)) {
|
|
2742
2828
|
isUncertain = true;
|
|
@@ -2758,17 +2844,22 @@ function findCallers(index, name, options = {}) {
|
|
|
2758
2844
|
// binding (name-level — the file importing the target for
|
|
2759
2845
|
// other names proves nothing): binding module external →
|
|
2760
2846
|
// excluded; resolves to a project file that doesn't reach a
|
|
2761
|
-
// target
|
|
2762
|
-
//
|
|
2847
|
+
// target by a definitive name-level `no` → excluded as another
|
|
2848
|
+
// definition; an unknown chain stays visible (deep barrels and
|
|
2849
|
+
// dynamic CJS surfaces can exceed the modeled ownership);
|
|
2763
2850
|
// unresolved-but-project-looking → visible (resolver gap).
|
|
2764
|
-
if (!bindingId && !resolvedBySameClass && call.isMethod &&
|
|
2765
|
-
(call.receiverIsModule || recvSubmoduleRel) &&
|
|
2851
|
+
if ((!bindingId || recvExportedNamespace) && !resolvedBySameClass && call.isMethod &&
|
|
2852
|
+
(call.receiverIsModule || recvSubmoduleRel || recvExportedNamespace) &&
|
|
2766
2853
|
langTraits(fileEntry.language)?.typeSystem === 'structural') {
|
|
2767
|
-
const recvBindings =
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
let
|
|
2854
|
+
const recvBindings = recvExportedNamespace
|
|
2855
|
+
? [] : _structuralModuleBindings(fileEntry, call);
|
|
2856
|
+
const tFiles = targetDefinitionFiles;
|
|
2857
|
+
if ((recvBindings.length > 0 || recvExportedNamespace) && !tFiles.has(filePath)) {
|
|
2858
|
+
let reaches = recvExportedNamespace?.verdict === 'yes';
|
|
2859
|
+
let projectish = !!recvExportedNamespace;
|
|
2860
|
+
let undetermined = recvExportedNamespace?.verdict === 'unknown';
|
|
2861
|
+
let resolvedBindings = recvExportedNamespace ? 1 : 0;
|
|
2862
|
+
let definitiveOtherBindings = recvExportedNamespace?.verdict === 'no' ? 1 : 0;
|
|
2772
2863
|
for (const b of recvBindings) {
|
|
2773
2864
|
const rel = (fileEntry.moduleResolved && fileEntry.moduleResolved[b.module]) ||
|
|
2774
2865
|
recvSubmoduleRel;
|
|
@@ -2778,10 +2869,12 @@ function findCallers(index, name, options = {}) {
|
|
|
2778
2869
|
if (mod.startsWith('.') ||
|
|
2779
2870
|
(firstSeg && _projectTopLevelNames(index).has(firstSeg))) {
|
|
2780
2871
|
projectish = true;
|
|
2872
|
+
undetermined = true;
|
|
2781
2873
|
}
|
|
2782
2874
|
continue;
|
|
2783
2875
|
}
|
|
2784
2876
|
projectish = true;
|
|
2877
|
+
resolvedBindings++;
|
|
2785
2878
|
const resolvedAbs = path.join(index.root, rel);
|
|
2786
2879
|
// Name-level ownership (fix #217 applied to module
|
|
2787
2880
|
// receivers — zod family D): `z._default(...)` asks
|
|
@@ -2791,17 +2884,29 @@ function findCallers(index, name, options = {}) {
|
|
|
2791
2884
|
// chase is definitive only on fully-modeled ESM/
|
|
2792
2885
|
// Python surfaces — 'unknown' (CJS, stars, module
|
|
2793
2886
|
// assignments) falls back to file-level reach.
|
|
2794
|
-
const
|
|
2887
|
+
const ambiguousCjsMember =
|
|
2888
|
+
_cjsMemberOwnershipAmbiguous(index, resolvedAbs, call.name);
|
|
2889
|
+
const verdict = ambiguousCjsMember
|
|
2890
|
+
? 'unknown'
|
|
2891
|
+
: _nameBindingReaches(index, resolvedAbs, call.name, tFiles);
|
|
2795
2892
|
if (verdict === 'yes' ||
|
|
2796
|
-
(verdict === 'unknown' &&
|
|
2893
|
+
(verdict === 'unknown' && !ambiguousCjsMember &&
|
|
2894
|
+
_importReaches(index, resolvedAbs, tFiles))) {
|
|
2797
2895
|
reaches = true; break;
|
|
2798
2896
|
}
|
|
2897
|
+
if (verdict === 'no') definitiveOtherBindings++;
|
|
2898
|
+
else undetermined = true;
|
|
2799
2899
|
}
|
|
2800
2900
|
if (!reaches) {
|
|
2801
2901
|
if (!projectish) {
|
|
2802
2902
|
recordExcluded(filePath, call.line, 'external-package');
|
|
2803
2903
|
continue;
|
|
2804
2904
|
}
|
|
2905
|
+
if (!undetermined && resolvedBindings > 0 &&
|
|
2906
|
+
definitiveOtherBindings === resolvedBindings) {
|
|
2907
|
+
recordExcluded(filePath, call.line, 'other-definition-import');
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2805
2910
|
if (collectAccount) {
|
|
2806
2911
|
routeUnverified(filePath, fileEntry, call, 'no-import-link', calledAs);
|
|
2807
2912
|
continue;
|
|
@@ -3077,7 +3182,8 @@ function findCallers(index, name, options = {}) {
|
|
|
3077
3182
|
if (sameNameTypeDefs.length > 1 ||
|
|
3078
3183
|
(call.receiverTypeQualifier && call.receiverTypeFlowFile)) {
|
|
3079
3184
|
const identity = _resolveStructuralFlowTypeIdentity(
|
|
3080
|
-
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs
|
|
3185
|
+
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs,
|
|
3186
|
+
call.receiverTypeFlowFile ? undefined : call.receiverTypeQualifier);
|
|
3081
3187
|
if (identity === 'other') {
|
|
3082
3188
|
receiverTypeValidated = false;
|
|
3083
3189
|
} else if (identity === 'unknown') {
|
|
@@ -3093,7 +3199,8 @@ function findCallers(index, name, options = {}) {
|
|
|
3093
3199
|
// exact declaration and walk its file-scoped
|
|
3094
3200
|
// ancestry before treating it as unrelated.
|
|
3095
3201
|
const identity = _resolveStructuralFlowTypeIdentity(
|
|
3096
|
-
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs
|
|
3202
|
+
index, call.receiverTypeFlowFile || filePath, knownType, targetDefs,
|
|
3203
|
+
call.receiverTypeFlowFile ? undefined : call.receiverTypeQualifier);
|
|
3097
3204
|
if (identity === 'target') {
|
|
3098
3205
|
receiverTypeValidated = true;
|
|
3099
3206
|
} else if (identity === 'unknown') {
|
|
@@ -4073,7 +4180,8 @@ function findCallers(index, name, options = {}) {
|
|
|
4073
4180
|
// module-ownership block above already routed the ones
|
|
4074
4181
|
// whose module doesn't reach the target. Submodule
|
|
4075
4182
|
// receivers (fix #224) are module receivers too.
|
|
4076
|
-
if (call.isMethod && !call.receiverIsModule &&
|
|
4183
|
+
if (call.isMethod && !call.receiverIsModule &&
|
|
4184
|
+
!recvSubmoduleRel && !call.moduleOwnedPath) {
|
|
4077
4185
|
const tTypes = dispatchTargetTypes(targetDefs2);
|
|
4078
4186
|
const typeQualifiedReceiver = !!(call.receiver && tTypes.has(call.receiver));
|
|
4079
4187
|
const knownDispatchType = call.receiverType ||
|
|
@@ -4153,6 +4261,22 @@ function findCallers(index, name, options = {}) {
|
|
|
4153
4261
|
(index.symbols.get(call.receiver) || []).length === 0 &&
|
|
4154
4262
|
!fileEntry.bindings?.some(b => b.name === call.receiver) &&
|
|
4155
4263
|
!call.receiverMemberAssigned) {
|
|
4264
|
+
// A candidate target that IS a member assignment
|
|
4265
|
+
// onto this same global (fix #286a, fastify-
|
|
4266
|
+
// measured: `console.log = () => {}` in a test's
|
|
4267
|
+
// beforeEach) patches the host object process-
|
|
4268
|
+
// wide — the assignment's own file cannot scope
|
|
4269
|
+
// it, so every `console.log(...)` site MAY
|
|
4270
|
+
// dispatch into the project def. Demote-only:
|
|
4271
|
+
// visible possible-dispatch, never confirmed,
|
|
4272
|
+
// never excluded.
|
|
4273
|
+
if (targetDefs2.some(d => d.memberAssigned &&
|
|
4274
|
+
d.assignedReceiver === call.receiver)) {
|
|
4275
|
+
routeUnverified(filePath, fileEntry, call, 'possible-dispatch', calledAs, {
|
|
4276
|
+
dispatchVia: `${call.receiver} — builtin global patched in project`,
|
|
4277
|
+
});
|
|
4278
|
+
continue;
|
|
4279
|
+
}
|
|
4156
4280
|
recordExcluded(filePath, call.line, 'external-package');
|
|
4157
4281
|
continue;
|
|
4158
4282
|
}
|
|
@@ -4879,11 +5003,15 @@ function findCallees(index, definition, options = {}) {
|
|
|
4879
5003
|
|
|
4880
5004
|
// Parser-proven lexical shadow: a parameter/local named `option`
|
|
4881
5005
|
// cannot call an imported/project `option`. A nested local
|
|
4882
|
-
// function
|
|
4883
|
-
//
|
|
5006
|
+
// function — or a nested CLASS, whose bare name constructs it
|
|
5007
|
+
// (fix #286f, flask-measured: `class Foo` inside test_custom_tag,
|
|
5008
|
+
// `Foo("bar")` excluded local-shadow while the oracle pins the
|
|
5009
|
+
// nested class) — is the safe exception and remains eligible for
|
|
5010
|
+
// exact same-file resolution below.
|
|
4884
5011
|
if (call.localShadow && !call.resolvedName && !call.resolvedNames) {
|
|
4885
5012
|
const localTarget = (index.symbols.get(call.name) || []).some(s =>
|
|
4886
|
-
s.file === def.file &&
|
|
5013
|
+
s.file === def.file &&
|
|
5014
|
+
(s.type === 'class' || !NON_CALLABLE_TYPES.has(s.type)) &&
|
|
4887
5015
|
s.startLine >= def.startLine && s.endLine <= def.endLine &&
|
|
4888
5016
|
s.startLine <= call.line);
|
|
4889
5017
|
if (!localTarget) {
|
|
@@ -7834,7 +7962,7 @@ function _rustBindingResolvedFiles(index, fileEntry, filePath, binding) {
|
|
|
7834
7962
|
* trusted (a use/import of an external type can shadow it invisibly)
|
|
7835
7963
|
* - no project type def at all: external name — safe, can't conflate
|
|
7836
7964
|
*/
|
|
7837
|
-
function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
|
|
7965
|
+
function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier = undefined) {
|
|
7838
7966
|
const opCache = index._opFlowTypeOriginCache;
|
|
7839
7967
|
const cacheKey = `${producerFile}\x00${typeName}\x00${qualifier || ''}`;
|
|
7840
7968
|
if (opCache?.has(cacheKey)) return opCache.get(cacheKey);
|
|
@@ -7898,6 +8026,36 @@ function _resolveFlowTypeOrigin(index, producerFile, typeName, qualifier) {
|
|
|
7898
8026
|
}
|
|
7899
8027
|
}
|
|
7900
8028
|
}
|
|
8029
|
+
// Structural module qualifier (fix #286e, flask-measured):
|
|
8030
|
+
// `app: flask.Flask` names the type through the imported module.
|
|
8031
|
+
// Resolve the module binding and pin the unique reachable
|
|
8032
|
+
// declaration — name-chase through modeled re-exports first, the
|
|
8033
|
+
// bounded file-level closure as fallback (the #277 rails). An
|
|
8034
|
+
// unresolvable qualifier is 'unknown', never proximity-guessed.
|
|
8035
|
+
if (fe && ['python', 'javascript', 'typescript', 'tsx'].includes(fe.language)) {
|
|
8036
|
+
const resolvedRels = new Set();
|
|
8037
|
+
const direct = fe.moduleResolved?.[qualifier];
|
|
8038
|
+
if (direct) resolvedRels.add(direct);
|
|
8039
|
+
for (const b of (fe.importBindings || [])) {
|
|
8040
|
+
if (b.name !== qualifier && b.alias !== qualifier) continue;
|
|
8041
|
+
const rel = fe.moduleResolved?.[b.module];
|
|
8042
|
+
if (rel) resolvedRels.add(rel);
|
|
8043
|
+
}
|
|
8044
|
+
for (const rel of resolvedRels) {
|
|
8045
|
+
const start = path.join(index.root, rel);
|
|
8046
|
+
const named = typeDefs.filter(d => d.file === start ||
|
|
8047
|
+
_nameBindingReaches(index, start, typeName, new Set([d.file])) === 'yes');
|
|
8048
|
+
if (new Set(named.map(d => d.file)).size === 1) {
|
|
8049
|
+
return finish({ fromFile: named[0].file });
|
|
8050
|
+
}
|
|
8051
|
+
const reachable = typeDefs.filter(d => d.file === start ||
|
|
8052
|
+
_importReaches(index, start, new Set([d.file])));
|
|
8053
|
+
if (new Set(reachable.map(d => d.file)).size === 1) {
|
|
8054
|
+
return finish({ fromFile: reachable[0].file });
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
return finish(null);
|
|
8058
|
+
}
|
|
7901
8059
|
const inPkg = fe && _qualifiedProducerDefs(index, fe, qualifier, typeDefs);
|
|
7902
8060
|
if (inPkg && inPkg.length > 0 && new Set(inPkg.map(d => d.file)).size === 1) {
|
|
7903
8061
|
return finish({ fromFile: inPkg[0].file });
|
|
@@ -8276,12 +8434,24 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
8276
8434
|
// makes widgets.helper the local helper even if widgets imports a
|
|
8277
8435
|
// different helper several hops below. Parser-provided localName
|
|
8278
8436
|
// keeps this proof limited to syntactically-owned CJS exports;
|
|
8279
|
-
// dynamic assignments remain unknown
|
|
8437
|
+
// dynamic assignments remain unknown — including a COMPETING
|
|
8438
|
+
// record for the same name without static ownership
|
|
8439
|
+
// (`exports.run = run;` then `if (native) exports.run =
|
|
8440
|
+
// require('./native').run;`) and a whole-surface reassignment
|
|
8441
|
+
// (`module.exports = require('./impl')`): either one can rebind
|
|
8442
|
+
// the member at runtime, so the static record alone never proves
|
|
8443
|
+
// a dead end (fix #292).
|
|
8280
8444
|
const localExports = (fe.exportDetails || []).filter(e =>
|
|
8281
|
-
!e.source && (e.alias || e.name) === attr
|
|
8282
|
-
|
|
8283
|
-
.
|
|
8284
|
-
|
|
8445
|
+
!e.source && (e.alias || e.name) === attr);
|
|
8446
|
+
const staticOwner = localExports.some(e => e.localName &&
|
|
8447
|
+
(index.symbols.get(e.localName) || [])
|
|
8448
|
+
.some(definition => definition.file === abs &&
|
|
8449
|
+
!NON_CALLABLE_TYPES.has(definition.type)));
|
|
8450
|
+
const competingDynamic = localExports.some(e => !e.localName) ||
|
|
8451
|
+
(fe.exportDetails || []).some(e => !e.source &&
|
|
8452
|
+
e.defaultLike && e.type === 'module.exports' &&
|
|
8453
|
+
(e.alias || e.name) !== attr);
|
|
8454
|
+
if (staticOwner && !competingDynamic) {
|
|
8285
8455
|
return 'no';
|
|
8286
8456
|
}
|
|
8287
8457
|
|
|
@@ -8343,6 +8513,256 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
8343
8513
|
return unknown ? 'unknown' : 'no';
|
|
8344
8514
|
}
|
|
8345
8515
|
|
|
8516
|
+
/**
|
|
8517
|
+
* A CommonJS object export whose requested member has no statically-owned
|
|
8518
|
+
* local value cannot prove callable identity. For example,
|
|
8519
|
+
* `module.exports = { run: available ? nativeRun : fallback }` exposes the
|
|
8520
|
+
* local fallback only on one runtime branch. File-level import reachability
|
|
8521
|
+
* must not upgrade that conditional value to a confirmed edge.
|
|
8522
|
+
*/
|
|
8523
|
+
function _cjsMemberOwnershipAmbiguous(index, file, memberName) {
|
|
8524
|
+
const fe = index.files.get(file);
|
|
8525
|
+
// Both CJS assignment families count: `module.exports = { x: cond ? a : b }`
|
|
8526
|
+
// records type 'module.exports'; `exports.x = <dynamic>` records type
|
|
8527
|
+
// 'exports' (fix #292 — the sequential native/fallback feature-detect idiom).
|
|
8528
|
+
return !!fe && (fe.exportDetails || []).some(exp =>
|
|
8529
|
+
(exp.type === 'module.exports' || exp.type === 'exports') &&
|
|
8530
|
+
!exp.defaultLike && !exp.source &&
|
|
8531
|
+
(exp.alias || exp.name) === memberName && !exp.localName);
|
|
8532
|
+
}
|
|
8533
|
+
|
|
8534
|
+
/**
|
|
8535
|
+
* Resolve a member reached through an exported ESM namespace object.
|
|
8536
|
+
*
|
|
8537
|
+
* Supported exact shapes:
|
|
8538
|
+
* import * as api from './impl'; export { api };
|
|
8539
|
+
* import * as api from './impl'; export default api;
|
|
8540
|
+
* export * as api from './impl';
|
|
8541
|
+
* export { api } from './barrel'; // recursively, when api is one above
|
|
8542
|
+
*
|
|
8543
|
+
* A plain exported object/value is deliberately not recognized. The caller
|
|
8544
|
+
* must remain in the ordinary structural receiver tier unless the export path
|
|
8545
|
+
* proves that the value is a module namespace exotic object.
|
|
8546
|
+
*
|
|
8547
|
+
* @returns {{ verdict: 'yes'|'no'|'unknown' }|null}
|
|
8548
|
+
*/
|
|
8549
|
+
function _namespaceExportMemberReaches(
|
|
8550
|
+
index, startAbs, exportedName, memberName, targetFiles, maxDepth = 4,
|
|
8551
|
+
visited = new Set(), memberPath = []
|
|
8552
|
+
) {
|
|
8553
|
+
if (maxDepth < 0) return { verdict: 'unknown' };
|
|
8554
|
+
const stateKey = `${startAbs}\x00${exportedName}\x00${memberPath.join('.')}\x00${memberName}`;
|
|
8555
|
+
if (visited.has(stateKey)) return { verdict: 'unknown' };
|
|
8556
|
+
visited.add(stateKey);
|
|
8557
|
+
const fe = index.files.get(startAbs);
|
|
8558
|
+
if (!fe) return null;
|
|
8559
|
+
|
|
8560
|
+
let recognized = false;
|
|
8561
|
+
let unknown = false;
|
|
8562
|
+
let explicitExport = false;
|
|
8563
|
+
const followMember = moduleName => {
|
|
8564
|
+
const rel = fe.moduleResolved?.[moduleName];
|
|
8565
|
+
if (!rel) {
|
|
8566
|
+
unknown = true;
|
|
8567
|
+
return 'unknown';
|
|
8568
|
+
}
|
|
8569
|
+
const nextAbs = path.join(index.root, rel);
|
|
8570
|
+
if (memberPath.length > 0) {
|
|
8571
|
+
const nested = _namespaceExportMemberReaches(
|
|
8572
|
+
index, nextAbs, memberPath[0], memberName, targetFiles,
|
|
8573
|
+
maxDepth - 1, new Set(visited), memberPath.slice(1));
|
|
8574
|
+
// The outer value is proven to be a namespace, but its requested
|
|
8575
|
+
// field is an ordinary/unmodeled value rather than another proven
|
|
8576
|
+
// namespace. That is uncertainty, never exclusion evidence.
|
|
8577
|
+
return nested?.verdict || 'unknown';
|
|
8578
|
+
}
|
|
8579
|
+
return _nameBindingReaches(index, nextAbs, memberName, targetFiles, maxDepth - 1);
|
|
8580
|
+
};
|
|
8581
|
+
const absorb = verdict => {
|
|
8582
|
+
if (verdict === 'yes') return true;
|
|
8583
|
+
if (verdict === 'unknown') unknown = true;
|
|
8584
|
+
return false;
|
|
8585
|
+
};
|
|
8586
|
+
|
|
8587
|
+
for (const exp of (fe.exportDetails || [])) {
|
|
8588
|
+
// `export * as api from './impl'` is direct namespace identity.
|
|
8589
|
+
if (exp.type === 're-export-all' && exp.alias === exportedName) {
|
|
8590
|
+
explicitExport = true;
|
|
8591
|
+
recognized = true;
|
|
8592
|
+
if (absorb(followMember(exp.source))) return { verdict: 'yes' };
|
|
8593
|
+
continue;
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8596
|
+
const exposed = exp.type === 'default' ? 'default' : (exp.alias || exp.name);
|
|
8597
|
+
if (exposed !== exportedName) continue;
|
|
8598
|
+
explicitExport = true;
|
|
8599
|
+
|
|
8600
|
+
// `export { api } from './barrel'`: the source-side value may itself
|
|
8601
|
+
// be a namespace export. Recurse under its source-side name.
|
|
8602
|
+
if (exp.type === 're-export' && exp.source) {
|
|
8603
|
+
const rel = fe.moduleResolved?.[exp.source];
|
|
8604
|
+
if (!rel) {
|
|
8605
|
+
unknown = true;
|
|
8606
|
+
recognized = true;
|
|
8607
|
+
continue;
|
|
8608
|
+
}
|
|
8609
|
+
const nested = _namespaceExportMemberReaches(
|
|
8610
|
+
index, path.join(index.root, rel), exp.name, memberName,
|
|
8611
|
+
targetFiles, maxDepth - 1, new Set(visited), memberPath);
|
|
8612
|
+
if (nested) {
|
|
8613
|
+
recognized = true;
|
|
8614
|
+
if (nested.verdict === 'yes') return { verdict: 'yes' };
|
|
8615
|
+
if (nested.verdict === 'unknown') unknown = true;
|
|
8616
|
+
}
|
|
8617
|
+
continue;
|
|
8618
|
+
}
|
|
8619
|
+
|
|
8620
|
+
// `import * as api ...; export { api }` / `export default api`.
|
|
8621
|
+
if (!exp.source && (exp.type === 'named' || exp.type === 'default')) {
|
|
8622
|
+
const localName = exp.name;
|
|
8623
|
+
const namespaceBindings = (fe.importBindings || []).filter(binding =>
|
|
8624
|
+
(binding.alias || binding.name) === localName && binding.kind === 'namespace');
|
|
8625
|
+
for (const binding of namespaceBindings) {
|
|
8626
|
+
recognized = true;
|
|
8627
|
+
if (absorb(followMember(binding.module))) return { verdict: 'yes' };
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8631
|
+
|
|
8632
|
+
// `export * from './barrel'` forwards named namespace-object exports too.
|
|
8633
|
+
// Explicit local/named exports shadow star exports, so only chase stars
|
|
8634
|
+
// when this file has no explicit surface for the requested name. Default
|
|
8635
|
+
// is never forwarded by export-star.
|
|
8636
|
+
if (!explicitExport && exportedName !== 'default') {
|
|
8637
|
+
const starExports = (fe.exportDetails || []).filter(exp =>
|
|
8638
|
+
exp.type === 're-export-all' && !exp.alias && exp.source);
|
|
8639
|
+
const starVerdicts = [];
|
|
8640
|
+
for (const exp of starExports) {
|
|
8641
|
+
const rel = fe.moduleResolved?.[exp.source];
|
|
8642
|
+
if (!rel) {
|
|
8643
|
+
unknown = true;
|
|
8644
|
+
continue;
|
|
8645
|
+
}
|
|
8646
|
+
const nested = _namespaceExportMemberReaches(
|
|
8647
|
+
index, path.join(index.root, rel), exportedName, memberName,
|
|
8648
|
+
targetFiles, maxDepth - 1, new Set(visited), memberPath);
|
|
8649
|
+
if (!nested) continue;
|
|
8650
|
+
recognized = true;
|
|
8651
|
+
starVerdicts.push(nested.verdict);
|
|
8652
|
+
}
|
|
8653
|
+
if (starVerdicts.length > 0) {
|
|
8654
|
+
// With several export-star providers, another star may expose the
|
|
8655
|
+
// same name and make the ESM binding ambiguous. We currently do
|
|
8656
|
+
// not compute full `ResolveExport` sets, so multi-star barrels are
|
|
8657
|
+
// demotion-only even when one path reaches the target.
|
|
8658
|
+
if (starExports.length > 1) unknown = true;
|
|
8659
|
+
else if (starVerdicts.includes('yes')) return { verdict: 'yes' };
|
|
8660
|
+
if (starVerdicts.includes('unknown')) unknown = true;
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
|
|
8664
|
+
if (!recognized) return null;
|
|
8665
|
+
return { verdict: unknown ? 'unknown' : 'no' };
|
|
8666
|
+
}
|
|
8667
|
+
|
|
8668
|
+
/**
|
|
8669
|
+
* Determine whether a structural receiver imported by name/default is a
|
|
8670
|
+
* statically exported namespace object, and if so whether its requested member
|
|
8671
|
+
* can reach the pinned target files. Multiple live bindings must agree before
|
|
8672
|
+
* a negative becomes exclusion-grade.
|
|
8673
|
+
*/
|
|
8674
|
+
function _importedNamespaceMemberOwnership(index, fileEntry, call, targetFiles) {
|
|
8675
|
+
const receiver = call.receiver || call.receiverRoot;
|
|
8676
|
+
const memberPath = call.receiver
|
|
8677
|
+
? [] : (call.receiverFields || (call.receiverField ? [call.receiverField] : []));
|
|
8678
|
+
const bindings = (fileEntry.importBindings || []).filter(binding =>
|
|
8679
|
+
(binding.alias || binding.name) === receiver &&
|
|
8680
|
+
(binding.kind === 'named' || binding.kind === 'default' || binding.kind === 'namespace'));
|
|
8681
|
+
if (bindings.length === 0) return null;
|
|
8682
|
+
|
|
8683
|
+
let recognized = 0;
|
|
8684
|
+
let unknown = false;
|
|
8685
|
+
for (const binding of bindings) {
|
|
8686
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
8687
|
+
if (!rel) continue;
|
|
8688
|
+
const startAbs = path.join(index.root, rel);
|
|
8689
|
+
let result;
|
|
8690
|
+
if (binding.kind === 'namespace') {
|
|
8691
|
+
result = memberPath.length > 0
|
|
8692
|
+
? _namespaceExportMemberReaches(
|
|
8693
|
+
index, startAbs, memberPath[0], call.name, targetFiles,
|
|
8694
|
+
4, new Set(), memberPath.slice(1))
|
|
8695
|
+
: { verdict: _nameBindingReaches(index, startAbs, call.name, targetFiles) };
|
|
8696
|
+
} else {
|
|
8697
|
+
const exportedName = binding.kind === 'default' ? 'default' : binding.name;
|
|
8698
|
+
result = _namespaceExportMemberReaches(
|
|
8699
|
+
index, startAbs, exportedName, call.name, targetFiles,
|
|
8700
|
+
4, new Set(), memberPath);
|
|
8701
|
+
}
|
|
8702
|
+
if (!result) continue;
|
|
8703
|
+
recognized++;
|
|
8704
|
+
if (result.verdict === 'yes') return { verdict: 'yes' };
|
|
8705
|
+
if (result.verdict === 'unknown') unknown = true;
|
|
8706
|
+
}
|
|
8707
|
+
if (recognized === 0) return null;
|
|
8708
|
+
if (recognized !== bindings.length) unknown = true;
|
|
8709
|
+
return { verdict: unknown ? 'unknown' : 'no' };
|
|
8710
|
+
}
|
|
8711
|
+
|
|
8712
|
+
/**
|
|
8713
|
+
* Ownership chase for a CommonJS default-like require binding:
|
|
8714
|
+
* `const local = require('./module')`. The local binding name says nothing
|
|
8715
|
+
* about the exporting file's symbol; the direct `module.exports = value`
|
|
8716
|
+
* record does. A locally defined callable is a definitive dead end for a
|
|
8717
|
+
* target in another file, an imported value is chased, and dynamic values
|
|
8718
|
+
* remain unknown. This is exclusion-grade only when every live path is known.
|
|
8719
|
+
*/
|
|
8720
|
+
function _defaultBindingReaches(index, startAbs, targetFiles, maxDepth = 4, visited = new Set()) {
|
|
8721
|
+
if (targetFiles.has(startAbs)) return 'yes';
|
|
8722
|
+
if (maxDepth < 0 || visited.has(startAbs)) return 'unknown';
|
|
8723
|
+
visited.add(startAbs);
|
|
8724
|
+
const fe = index.files.get(startAbs);
|
|
8725
|
+
if (!fe) return 'unknown';
|
|
8726
|
+
|
|
8727
|
+
const defaults = (fe.exportDetails || []).filter(exp =>
|
|
8728
|
+
exp.type === 'module.exports' && exp.defaultLike);
|
|
8729
|
+
if (defaults.length === 0) return 'unknown';
|
|
8730
|
+
|
|
8731
|
+
let unknown = false;
|
|
8732
|
+
for (const exp of defaults) {
|
|
8733
|
+
const localName = exp.localName || exp.name;
|
|
8734
|
+
if (!localName) { unknown = true; continue; }
|
|
8735
|
+
// Without parser-proven syntactic ownership (localName), the record's
|
|
8736
|
+
// name falls back to the synthesized 'default' — which a DYNAMIC
|
|
8737
|
+
// reassignment (`module.exports = require('./impl')`) shares with an
|
|
8738
|
+
// earlier anonymous default. Only a local callable declared AT the
|
|
8739
|
+
// record's own line proves that this record is the local value
|
|
8740
|
+
// (fix #292 — a name-only match must never dead-end the live import
|
|
8741
|
+
// path of the competing dynamic record).
|
|
8742
|
+
const localCallable = (index.symbols.get(localName) || []).some(definition =>
|
|
8743
|
+
definition.file === startAbs && !NON_CALLABLE_TYPES.has(definition.type) &&
|
|
8744
|
+
(exp.localName || definition.startLine === exp.line ||
|
|
8745
|
+
(definition.startLine <= exp.line && definition.endLine >= exp.line)));
|
|
8746
|
+
if (localCallable) continue;
|
|
8747
|
+
|
|
8748
|
+
const bindings = (fe.importBindings || []).filter(binding =>
|
|
8749
|
+
binding.name === localName || binding.alias === localName);
|
|
8750
|
+
if (bindings.length === 0) { unknown = true; continue; }
|
|
8751
|
+
for (const binding of bindings) {
|
|
8752
|
+
const rel = fe.moduleResolved?.[binding.module];
|
|
8753
|
+
if (!rel) { unknown = true; continue; }
|
|
8754
|
+
const nextAbs = path.join(index.root, rel);
|
|
8755
|
+
const verdict = binding.defaultLike
|
|
8756
|
+
? _defaultBindingReaches(
|
|
8757
|
+
index, nextAbs, targetFiles, maxDepth - 1, new Set(visited))
|
|
8758
|
+
: _nameBindingReaches(index, nextAbs, binding.name, targetFiles, maxDepth - 1);
|
|
8759
|
+
if (verdict === 'yes') return 'yes';
|
|
8760
|
+
if (verdict === 'unknown') unknown = true;
|
|
8761
|
+
}
|
|
8762
|
+
}
|
|
8763
|
+
return unknown ? 'unknown' : 'no';
|
|
8764
|
+
}
|
|
8765
|
+
|
|
8346
8766
|
/**
|
|
8347
8767
|
* From-import submodule receivers (fix #224): `from . import jobs` binds
|
|
8348
8768
|
* jobs.py as a plain NAME — the parser can't mark it a module alias (a
|
|
@@ -8688,8 +9108,8 @@ function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs, li
|
|
|
8688
9108
|
* CustomCommand extends click.Command), while parallel package versions may
|
|
8689
9109
|
* reuse every class name (zod v3/v4 ZodArray -> ZodType).
|
|
8690
9110
|
*/
|
|
8691
|
-
function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, targetDefs) {
|
|
8692
|
-
const origin = _resolveFlowTypeOrigin(index, originFile, knownType);
|
|
9111
|
+
function _resolveStructuralFlowTypeIdentity(index, originFile, knownType, targetDefs, qualifier) {
|
|
9112
|
+
const origin = _resolveFlowTypeOrigin(index, originFile, knownType, qualifier);
|
|
8693
9113
|
if (!origin?.fromFile) return 'unknown';
|
|
8694
9114
|
const targetOwners = new Set(targetDefs
|
|
8695
9115
|
.map(d => d.className || (d.receiver && d.receiver.replace(/^\*/, '')))
|
|
@@ -9352,13 +9772,29 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
|
|
|
9352
9772
|
};
|
|
9353
9773
|
|
|
9354
9774
|
const details = fe.exportDetails || [];
|
|
9775
|
+
const localDetails = details.filter(e =>
|
|
9776
|
+
!e.source && (e.alias || e.name) === attr);
|
|
9777
|
+
const ambiguousCjsMember = localDetails.some(e =>
|
|
9778
|
+
(e.type === 'module.exports' || e.type === 'exports') &&
|
|
9779
|
+
!e.defaultLike && !e.localName);
|
|
9355
9780
|
const localExposed = fe.language === 'python' ||
|
|
9356
|
-
(fe.exports || []).includes(attr) ||
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9781
|
+
(fe.exports || []).includes(attr) || localDetails.length > 0;
|
|
9782
|
+
if (ambiguousCjsMember) {
|
|
9783
|
+
// `module.exports = { run: condition ? native : fallback }`
|
|
9784
|
+
// exposes no single callable identity. The caller direction
|
|
9785
|
+
// routes this visible; trace-down must make the same abstention
|
|
9786
|
+
// instead of selecting a same-named local fallback.
|
|
9787
|
+
unknown = true;
|
|
9788
|
+
} else if (localExposed) {
|
|
9789
|
+
const localNames = new Set([attr]);
|
|
9790
|
+
for (const detail of localDetails) {
|
|
9791
|
+
if (detail.localName) localNames.add(detail.localName);
|
|
9792
|
+
}
|
|
9793
|
+
for (const localName of localNames) {
|
|
9794
|
+
for (const d of (index.symbols.get(localName) || [])) {
|
|
9795
|
+
if (d.file === abs && shapeMatches(d)) {
|
|
9796
|
+
matches.set(`${d.file}:${d.startLine}`, d);
|
|
9797
|
+
}
|
|
9362
9798
|
}
|
|
9363
9799
|
}
|
|
9364
9800
|
}
|
|
@@ -9368,8 +9804,9 @@ function _calleeExportDefinitions(index, startAbs, exposedName, language, call,
|
|
|
9368
9804
|
// never to namespace.member() routing.
|
|
9369
9805
|
if (options.allowDefaultExport && depth === 0) {
|
|
9370
9806
|
for (const e of details) {
|
|
9371
|
-
if (e.type !== 'module.exports' || !e.name) continue;
|
|
9372
|
-
|
|
9807
|
+
if (e.type !== 'module.exports' || !e.defaultLike || !e.name) continue;
|
|
9808
|
+
const localName = e.localName || e.name;
|
|
9809
|
+
for (const d of (index.symbols.get(localName) || [])) {
|
|
9373
9810
|
if (d.file === abs && shapeMatches(d)) {
|
|
9374
9811
|
matches.set(`${d.file}:${d.startLine}`, d);
|
|
9375
9812
|
}
|
|
@@ -12967,6 +13404,16 @@ function _typeOfCallResultFoldInner(index, fileEntry, filePath, record, ctx, con
|
|
|
12967
13404
|
const cands = (index.symbols.get(name) || [])
|
|
12968
13405
|
.filter(d => !NON_CALLABLE_TYPES.has(d.type) && d.returnType && !d.className);
|
|
12969
13406
|
let matches = cands.filter(d => d.file === modFile);
|
|
13407
|
+
// A named/default import may itself be an exported namespace object
|
|
13408
|
+
// (`import { z } from 'zod/v3'; z.string()`). Resolve the producer
|
|
13409
|
+
// name through that exact namespace identity before the older
|
|
13410
|
+
// one-hop barrel fallback. Exported callable-alias symbols then carry
|
|
13411
|
+
// the captured class member's declared return type into the chain.
|
|
13412
|
+
if (matches.length === 0) {
|
|
13413
|
+
matches = cands.filter(definition =>
|
|
13414
|
+
_importedNamespaceMemberOwnership(
|
|
13415
|
+
index, fileEntry, record, new Set([definition.file]))?.verdict === 'yes');
|
|
13416
|
+
}
|
|
12970
13417
|
if (matches.length === 0) {
|
|
12971
13418
|
const hop = index.importGraph.get(modFile);
|
|
12972
13419
|
if (hop) matches = cands.filter(d => hop.has(d.file));
|