Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions scripts/check-dead-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ const REFERENCE_ROOTS = ["src", "packages", "test", "scripts", "apps/loopover-ui
const SOURCE_PATTERN = /(?<!\.d)\.tsx?$/;
const EXCLUDED_SEGMENT = /(?:^|\/)(?:node_modules|dist|dist-test)(?:\/|$)/;

/** `export const NAME` / `export function NAME` / `export async function NAME` at the top level. Types and
* interfaces are deliberately out of scope: an unused type costs nothing at runtime and TypeScript's own
* `noUnusedLocals` already covers the local case. */
const EXPORTED_RUNTIME_SYMBOL = /^export (?:async )?(?:const|function) ([A-Za-z_][A-Za-z0-9_]*)/gm;
/** `export const|function|class|enum NAME` (plus `async function` / `abstract class` / `const enum`) at the
* top level. Types and interfaces stay out of scope: an unused type costs nothing at runtime and
* TypeScript's own `noUnusedLocals` already covers the local case. */
const EXPORTED_RUNTIME_SYMBOL = /^export (?:async )?(?:abstract )?(?:const\s+enum|const|function|class|enum) ([A-Za-z_][A-Za-z0-9_]*)/gm;

/**
* Exports with no in-repo reference that are nonetheless legitimate, each with the reason.
Expand Down
77 changes: 77 additions & 0 deletions test/unit/check-dead-exports-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,80 @@ describe("findDeadExports (#9852)", () => {
expect(found.map((v) => v.symbol)).toEqual(["FOO"]);
});
});

describe("findDeadExports class/enum (#10047)", () => {
it("REGRESSION (#10047): flags an orphan export class with internalUses including the declaration", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/x.ts": "export class Orphan {}\nconst y = new Orphan();\n" }),
});
expect(found).toEqual([{ file: "src/x.ts", symbol: "Orphan", internalUses: 2 }]);
});

it("flags an orphan export enum with no other reference", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/x.ts": "export enum Kind { A }\n" }),
});
expect(found.map((v) => v.symbol)).toEqual(["Kind"]);
});

it("does not flag a class referenced from another file (external > 0 early-break)", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({
"src/x.ts": "export class Used {}\n",
"src/y.ts": "new Used();\n",
}),
});
expect(found).toEqual([]);
});

it("REGRESSION (#10047): export abstract class is matched — abstract must not defeat the anchor", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/x.ts": "export abstract class Base {}\n" }),
});
expect(found.map((v) => v.symbol)).toEqual(["Base"]);
});

it("does NOT match a non-exported class Foo {}", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/x.ts": "class Foo {}\nvoid Foo;\n" }),
});
expect(found).toEqual([]);
});

it("still does NOT match export type / export interface", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/a.ts": "export type T = 1;\nexport interface I { x: 1 }\n" }),
});
expect(found).toEqual([]);
});

it("export const enum Kind is matched as Kind (const enum before bare const in the alternation)", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/x.ts": "export const enum Kind { A }\n" }),
});
expect(found.map((v) => v.symbol)).toEqual(["Kind"]);
});

it("class with internalUses > 1 still reports the drop-export hint path", () => {
const found = findDeadExports({
sourceRoots: ["src"],
referenceRoots: ["src"],
...world({ "src/x.ts": "export class Inside {}\nconst z = new Inside();\n" }),
});
expect(found.find((v) => v.symbol === "Inside")?.internalUses).toBeGreaterThan(1);
});
});