diff --git a/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts b/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts index cb6ef0e1c..7afb88b81 100644 --- a/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts +++ b/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts @@ -141,10 +141,17 @@ export function resolveFetchWrapperTargets( } return targets; }; - const declared = new Set(); + // Each export name maps to the declaration it names; a name two declarations + // claim keeps a name-only identity, so it never merges with another export. + const declared = new Map(); for (const definition of definitions) if (definition.moduleScope) - for (const name of definition.exportedNames) declared.add(`${definition.filePath}\0${name}`); + for (const name of definition.exportedNames) { + const key = `${definition.filePath}\0${name}`; + const identity = `${definition.filePath}\0${definition.definitionId}`; + const existing = declared.get(key); + declared.set(key, existing === undefined || existing === identity ? identity : `${key}\0`); + } const named = new Map(); const stars = new Map(); for (const reexport of reexports) { @@ -181,8 +188,16 @@ export function resolveFetchWrapperTargets( named.get(key) ?? (name === 'default' ? undefined : stars.get(filePath)) ?? []; return { frame: { filePath, name, candidates, cursor: 0, found: new Map() } }; }; + // Star exports conflict only when they reach different bindings, so two + // export names of one declaration are one candidate. const record = (frame: ExportFrame, target: CalleeTarget | undefined) => { - if (target) frame.found.set(JSON.stringify(target), target); + if (!target) return; + const identity = + target.kind === 'resolved-import' + ? declared.get(`${target.filePath}\0${target.importedName}`) + : undefined; + const key = identity ?? JSON.stringify(target); + if (!frame.found.has(key)) frame.found.set(key, target); }; const first = open(startFile, startName); if ('value' in first) return first.value; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts new file mode 100644 index 000000000..48c0bfba7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts @@ -0,0 +1,6 @@ +export function distinctRequest(url: string) { + return fetch(url); +} +export function distinctSend(url: string) { + return fetch(url); +} diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts new file mode 100644 index 000000000..2f1a5bbd0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts @@ -0,0 +1,2 @@ +export * from './left'; +export * from './right'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts new file mode 100644 index 000000000..0d83255e4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts @@ -0,0 +1 @@ +export { distinctRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts new file mode 100644 index 000000000..8034cf2f9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts @@ -0,0 +1 @@ +export { distinctSend as distinctRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts new file mode 100644 index 000000000..87f39c442 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts @@ -0,0 +1,4 @@ +function diamondRequest(url: string) { + return fetch(url); +} +export { diamondRequest, diamondRequest as diamondSend }; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts new file mode 100644 index 000000000..2f1a5bbd0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts @@ -0,0 +1,2 @@ +export * from './left'; +export * from './right'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts new file mode 100644 index 000000000..9f61165e1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts @@ -0,0 +1 @@ +export { diamondRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts new file mode 100644 index 000000000..8e39815e0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts @@ -0,0 +1 @@ +export { diamondSend as diamondRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts index 17089694f..9e28aef07 100644 --- a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts @@ -22,3 +22,5 @@ app.get('/api/hoc', (_req, res) => res.json({})); app.get('/api/cjs', (_req, res) => res.json({})); app.get('/api/key', (_req, res) => res.json({})); app.get('/api/node', (_req, res) => res.json({})); +app.get('/api/diamond', (_req, res) => res.json({})); +app.get('/api/diamond-distinct', (_req, res) => res.json({})); diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts new file mode 100644 index 000000000..37cf4681d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts @@ -0,0 +1,4 @@ +import { diamondRequest } from '../lib/diamond'; +import { distinctRequest } from '../lib/diamond-distinct'; +export const loadDiamond = () => diamondRequest('/api/diamond'); +export const loadDistinct = () => distinctRequest('/api/diamond-distinct'); diff --git a/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts b/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts index f4cefe6f2..587bf367a 100644 --- a/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts +++ b/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts @@ -70,7 +70,7 @@ describe('Base-URL wrapper consumer FETCHES edge extraction', () => { }); describe('Fetch wrapper lexical and project target identity', () => { - it('resolves relative, alias, workspace, barrel, overload, multi-declarator, HOC, CommonJS and package-fetch targets while rejecting shadowed and ambiguous names', async () => { + it('resolves relative, alias, workspace, barrel, export-alias diamond, overload, multi-declarator, HOC, CommonJS and package-fetch targets while rejecting shadowed and ambiguous names', async () => { const result = await runPipelineFromRepo( path.join(FIXTURES, 'fetch-wrapper-target-identity'), () => {}, @@ -88,6 +88,7 @@ describe('Fetch wrapper lexical and project target identity', () => { 'web/barrel.ts -> /api/barrel-imported', 'web/barrel.ts -> /api/barrel-star', 'web/cjs.js -> /api/cjs', + 'web/diamond.ts -> /api/diamond', 'web/hoc.ts -> /api/hoc', 'web/multi.ts -> /api/multi', 'web/nested.ts -> /api/nested', diff --git a/gitnexus/test/unit/fetch-wrapper-index.test.ts b/gitnexus/test/unit/fetch-wrapper-index.test.ts index b23c73439..9b3661d5f 100644 --- a/gitnexus/test/unit/fetch-wrapper-index.test.ts +++ b/gitnexus/test/unit/fetch-wrapper-index.test.ts @@ -388,6 +388,29 @@ describe('fetch wrapper barrel resolution', () => { expect(resolveIn(reexports)).toEqual(expected); }); + // `export * from './left'; export * from './right'`, where left re-exports + // `request` and right re-exports `send as request` from the same module. + const diamond = [ + forward('index.ts', '*', './left', '*'), + forward('index.ts', '*', './right', '*'), + forward('left.ts', 'request', './http'), + forward('right.ts', 'request', './http', 'send'), + ]; + it.each([ + [ + 'two export names of one declaration', + [{ ...declares('http.ts', 'request'), exportedNames: ['request', 'send'] }], + at('http.ts'), + ], + [ + 'two declarations', + [declares('http.ts', 'request'), declares('http.ts', 'send')], + { kind: 'blocked' }, + ], + ])('resolves a star-export diamond over %s by binding identity', (_shape, defs, expected) => { + expect(resolveIn(diamond, 'request', defs)).toEqual(expected); + }); + it('never forwards default through a star export', () => { expect( resolveIn([forward('index.ts', '*', './request', '*')], 'default', [