Files
codegraph/__tests__/namespace-reexport.test.ts
Colby Mchenry 7c1c9a3691 fix(ts): export * as ns exports the namespace, not every member (#2192)
`export * as core from '../core/index.js'` was recorded as a plain wildcard,
so a barrel walk for `z.number()` went into core, on through core's own
`export * as regexes`, and landed on the `number` regex constant - ahead of
the `export * from './schemas.js'` that holds the schema function. zod's
`z.string()` (262 calls), `z.number()` (141), `z.email()`, `z.boolean()`...
all went to regex constants.

- A namespace re-export is now its own kind: it supplies only `ns`, and a
  member path through it (`z.coerce.number()`, `z.core.safeParse()`)
  continues in that module. A 3-segment chain is resolved when its root is an
  `import * as` binding and only through the import resolver - untyped chains
  stay unresolved as before.
- A named re-export (`export { safeParse } from '../core'`) is found for a
  namespace import's member; the walk compared against `'*'` and only reached
  those through the misread wildcard.

zod -471 +571 (every regex-constant edge now reaches the schema function;
`z.lt` -> core `_lt`, `z.coerce.string` -> coerce.ts), trpc +4 and kit +1
(namespace-import members through named re-exports). typeorm, hono,
excalidraw, element-plus, starlight unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:01:48 +00:00

104 lines
3.2 KiB
TypeScript

/**
* `export * as core from './core'` exports `core` — not every member of
* `./core` as if it were `export *`. Read as a wildcard, zod's
* `z.number()` walked into core and on into `export * as regexes`, landing
* on the `number` regex constant (262 `z.string()` calls did the same); the
* schema function it means sits in a LATER `export * from './schemas'`.
* `z.core.safeParse()` continues into the namespaced module, and a member a
* barrel forwards by name (`export { safeParse } from '../core'`) is found
* for `import * as m` too.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-ns-reexport-'));
const files: Record<string, string> = {
'package.json': JSON.stringify({ name: 'schema', private: true }),
'src/core/regexes.ts': `export const number = /^-?\\d+$/;
`,
'src/core/parse.ts': `export function safeParse(value: unknown) {
return { success: true, data: value };
}
`,
'src/core/api.ts': `export function _lt(limit: number) {
return limit;
}
`,
'src/core/index.ts': `export * as regexes from "./regexes.js";
export * from "./parse.js";
export * from "./api.js";
`,
'src/classic/schemas.ts': `export function number() {
return { kind: "number" };
}
`,
'src/classic/external.ts': `export * as core from "../core/index.js";
export * from "./schemas.js";
export { _lt as lt } from "../core/index.js";
`,
'src/mini/parse.ts': `export { safeParse } from "../core/index.js";
`,
'src/mini/external.ts': `export * as core from "../core/index.js";
export * from "./parse.js";
`,
'src/tests/classic.test.ts': `import * as z from "../classic/external.js";
export function classic() {
z.number();
z.lt(10);
z.core.safeParse(1);
}
`,
'src/tests/mini.test.ts': `import * as m from "../mini/external.js";
export function mini() {
m.safeParse(1);
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `file: qualified name` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => {
const t = cg.getNode(e.target)!;
return `${t.filePath}: ${t.qualifiedName}`;
})
.sort();
}
describe('`export * as ns` re-exports', () => {
it('export the namespace only, so a later `export *` supplies the member', () => {
expect(callsFrom('src/tests/classic.test.ts')).toEqual([
'src/classic/schemas.ts: number',
'src/core/api.ts: _lt',
'src/core/parse.ts: safeParse',
]);
});
it('find a member a barrel forwards by name for a namespace import', () => {
expect(callsFrom('src/tests/mini.test.ts')).toEqual(['src/core/parse.ts: safeParse']);
});
});