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/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' && e.name === name)) continue;
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 = _nameBindingReaches(index, resolvedAbs, b.name, tFiles);
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 && call.receiverIsModule &&
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 (directly or one re-export hop) → visible, not
2762
- // excluded (deep barrel chains exceed the hop budget);
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 = _structuralModuleBindings(fileEntry, call);
2768
- const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
2769
- if (recvBindings.length > 0 && !tFiles.has(filePath)) {
2770
- let reaches = false;
2771
- let projectish = false;
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 verdict = _nameBindingReaches(index, resolvedAbs, call.name, tFiles);
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' && _importReaches(index, resolvedAbs, tFiles))) {
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 && !recvSubmoduleRel) {
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 with that name is the one safe exception and remains
4883
- // eligible for exact same-file resolution below.
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 && !NON_CALLABLE_TYPES.has(s.type) &&
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 && e.localName);
8282
- if (localExports.some(e => (index.symbols.get(e.localName) || [])
8283
- .some(definition => definition.file === abs &&
8284
- !NON_CALLABLE_TYPES.has(definition.type)))) {
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
- details.some(e => !e.source && (e.alias || e.name) === attr);
9358
- if (localExposed) {
9359
- for (const d of (index.symbols.get(attr) || [])) {
9360
- if (d.file === abs && shapeMatches(d)) {
9361
- matches.set(`${d.file}:${d.startLine}`, d);
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
- for (const d of (index.symbols.get(e.name) || [])) {
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));