From a3326516e22bcc98772507b7d7f215df762a1cac Mon Sep 17 00:00:00 2001 From: ziho Date: Fri, 3 Apr 2026 13:21:32 +0900 Subject: [PATCH] fix: group barrel re-exports by declaration source file to prevent same-named symbols from being incorrectly merged --- .../source/exclude-barrel-re-exports.ts | 4 +- .../source/exclude-barrel-re-exports.spec.ts | 49 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/core/parser/source/exclude-barrel-re-exports.ts b/packages/cli/src/core/parser/source/exclude-barrel-re-exports.ts index de1ef12..540a906 100644 --- a/packages/cli/src/core/parser/source/exclude-barrel-re-exports.ts +++ b/packages/cli/src/core/parser/source/exclude-barrel-re-exports.ts @@ -2,7 +2,9 @@ import { groupBy } from "es-toolkit"; import { ExportDeclaration } from "../../types/parser.types.js"; export function excludeBarrelReExports(exportDeclarations: ExportDeclaration[]): ExportDeclaration[] { - return Object.values(groupBy(exportDeclarations, exp => `${exp.symbolName}:${exp.kind}`)) + return Object.values( + groupBy(exportDeclarations, exp => `${exp.symbolName}:${exp.kind}:${exp.declaration.getSourceFile().getFilePath()}`) + ) .flatMap(declarations => { if (declarations.length === 1) { return declarations; diff --git a/packages/cli/src/tests/core/parser/source/exclude-barrel-re-exports.spec.ts b/packages/cli/src/tests/core/parser/source/exclude-barrel-re-exports.spec.ts index fcb698e..dccb167 100644 --- a/packages/cli/src/tests/core/parser/source/exclude-barrel-re-exports.spec.ts +++ b/packages/cli/src/tests/core/parser/source/exclude-barrel-re-exports.spec.ts @@ -104,8 +104,53 @@ describe("excludeBarrelReExports", () => { expect(result.find(exp => exp.kind === "type")).toBeDefined(); expect(result.find(exp => exp.kind === "function")).toBeDefined(); }); + + it("should keep same-named exports when they originate from different source declarations", () => { + const result = excludeBarrelReExports([ + // `foo` from /src/a/foo.ts, re-exported through barrels + mockExport("foo", "variable", "/src/index.ts", "/src/a/foo.ts"), + mockExport("foo", "variable", "/src/a/index.ts", "/src/a/foo.ts"), + // `foo` from /src/b/bar.ts (different source, same symbol name) + mockExport("foo", "variable", "/src/b/bar.ts", "/src/b/bar.ts"), + mockExport("foo", "variable", "/src/b/index.ts", "/src/b/bar.ts"), + ]); + + expect(result).toHaveLength(2); + + const declFiles = result.map(exp => getDeclFilePath(exp)); + expect(declFiles).toContain("/src/a/foo.ts"); + expect(declFiles).toContain("/src/b/bar.ts"); + }); + + it("should prefer non-index.ts file for each source declaration group", () => { + const result = excludeBarrelReExports([ + mockExport("foo", "variable", "/src/lib/impl.ts", "/src/lib/impl.ts"), + mockExport("foo", "variable", "/src/lib/index.ts", "/src/lib/impl.ts"), + ]); + + expect(result).toHaveLength(1); + expect(result[0].filePath).toContain("impl.ts"); + }); }); -function mockExport(symbolName: string, kind: ExportDeclaration["kind"], filePath: string): ExportDeclaration { - return { symbolName, kind, filePath: filePath as StandardizedFilePath } as ExportDeclaration; +function mockExport( + symbolName: string, + kind: ExportDeclaration["kind"], + filePath: string, + declFilePath?: string +): ExportDeclaration { + return { + symbolName, + kind, + filePath: filePath as StandardizedFilePath, + declaration: { + getSourceFile: () => ({ + getFilePath: () => (declFilePath ?? filePath) as StandardizedFilePath, + }), + }, + } as ExportDeclaration; +} + +function getDeclFilePath(exp: ExportDeclaration): string { + return exp.declaration.getSourceFile().getFilePath(); }