Initial commit: pygenium as git submodule
This commit is contained in:
623
tests/dead-code.test.ts
Normal file
623
tests/dead-code.test.ts
Normal file
@@ -0,0 +1,623 @@
|
||||
/**
|
||||
* dead-code.test.ts — integration tests for the dead-code check.
|
||||
*
|
||||
* Covers:
|
||||
* - registry/help: registering the check auto-binds `/pygienium-dead-code`
|
||||
* (zero index.ts wiring changes).
|
||||
* - deterministic detection: unused export, dead file, obsolete compat shim,
|
||||
* and unused dependency are classified; a dynamically-imported module is
|
||||
* classified `review`.
|
||||
* - E2E `--fix`: clearly-dead items are removed and listed in changes.md;
|
||||
* the dynamically-imported module is preserved and flagged for review.
|
||||
*/
|
||||
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
clearChecks,
|
||||
getAllChecks,
|
||||
registerCheck,
|
||||
getCheck,
|
||||
} from "../src/checks/registry.js";
|
||||
import {
|
||||
deadCodeCheck,
|
||||
detectDeadCode,
|
||||
findingsPath,
|
||||
changesPath,
|
||||
renderFindingsMd,
|
||||
applyDeadCodeFixes,
|
||||
} from "../src/checks/dead-code.js";
|
||||
import { buildPygieniumHelpLines } from "../src/help.js";
|
||||
import {
|
||||
setAgentRunner,
|
||||
resetAgentRunner,
|
||||
fakeAgentRunner,
|
||||
} from "../src/agent-runner.js";
|
||||
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
|
||||
import { loadRunState } from "../src/run-state.js";
|
||||
|
||||
function stubCtx(cwd: string): PygieniumCtx {
|
||||
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
||||
}
|
||||
|
||||
async function writeFixture(root: string): Promise<void> {
|
||||
await mkdir(join(root, "src", "routes"), { recursive: true });
|
||||
|
||||
// package.json — carries an unused dependency + an entry `main`.
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "fixture",
|
||||
version: "1.0.0",
|
||||
main: "src/index.ts",
|
||||
dependencies: {
|
||||
leftoverpkg: "^1.0.0",
|
||||
typescript: "^5.0.0",
|
||||
},
|
||||
devDependencies: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
|
||||
// util.ts — one used export and one unused export.
|
||||
await writeFile(
|
||||
join(root, "src", "util.ts"),
|
||||
[
|
||||
"export function add(a: number, b: number): number {",
|
||||
" return a + b;",
|
||||
"}",
|
||||
"",
|
||||
"export function unusedHelper(): string {",
|
||||
' return "never called anywhere";',
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// index.ts — the entry (reaches util and routes), its name is entry-like
|
||||
// so the zero-importer rule must NOT flag it as dead.
|
||||
await writeFile(
|
||||
join(root, "src", "index.ts"),
|
||||
[
|
||||
'import { add } from "./util";',
|
||||
'import { load } from "./routes";',
|
||||
"add(1, 2);",
|
||||
"load();",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// routes/index.ts — live barrel that exposes a lazy loader.
|
||||
await writeFile(
|
||||
join(root, "src", "routes", "index.ts"),
|
||||
['export const load = () => import("./lazy-route");', ""].join("\n"),
|
||||
);
|
||||
|
||||
// lazy-route.ts — a shim reachable ONLY through a dynamic import. It must
|
||||
// be classified `review` and never auto-removed.
|
||||
await writeFile(
|
||||
join(root, "src", "routes", "lazy-route.ts"),
|
||||
["export function registerRoute(): void {", " return;", "}", ""].join(
|
||||
"\n",
|
||||
),
|
||||
);
|
||||
|
||||
// compat.ts — an obsolete deprecated compat wrapper with zero importers.
|
||||
await writeFile(
|
||||
join(root, "src", "compat.ts"),
|
||||
[
|
||||
"// @deprecated obsolete compatibility wrapper — scheduled for removal.",
|
||||
"export function legacyFormat(x: string): string {",
|
||||
" return x;",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
// orphan.ts — a completely unreferenced module (dead file).
|
||||
await writeFile(
|
||||
join(root, "src", "orphan.ts"),
|
||||
["export function orphan(): void {", " return;", "}", ""].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
describe("dead-code registry + help", () => {
|
||||
beforeEach(async () => {
|
||||
clearChecks();
|
||||
// Re-register after clearChecks (module-level registration runs on import).
|
||||
registerCheck(deadCodeCheck);
|
||||
});
|
||||
|
||||
it("registers a single dead-code check with a serialisable name", () => {
|
||||
expect(getCheck("dead-code")).toBeDefined();
|
||||
expect(getAllChecks().filter((c) => c.name === "dead-code")).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("appears in /pygienium-help with its description (auto discovery)", () => {
|
||||
const help = buildPygieniumHelpLines();
|
||||
expect(help.join("\n")).toContain("/pygienium-dead-code");
|
||||
expect(help.join("\n")).toContain(deadCodeCheck.description);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dead-code detection", () => {
|
||||
it("classifies unused export, dead file, compat shim, unused dep; preserves dynamic import", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-"));
|
||||
try {
|
||||
await writeFixture(root);
|
||||
const report = await detectDeadCode(root);
|
||||
|
||||
expect(report.items.length).toBeGreaterThan(0);
|
||||
|
||||
const unusedExport = report.items.find(
|
||||
(i) => i.category === "export" && i.name === "unusedHelper",
|
||||
);
|
||||
expect(unusedExport).toBeDefined();
|
||||
expect(unusedExport?.review).toBe(false);
|
||||
expect(unusedExport?.target).toBe("symbol");
|
||||
|
||||
const deadShim = report.items.find(
|
||||
(i) => i.category === "shim" && i.rel === "src/compat.ts",
|
||||
);
|
||||
expect(deadShim).toBeDefined();
|
||||
expect(deadShim?.review).toBe(false);
|
||||
expect(deadShim?.target).toBe("file");
|
||||
|
||||
const deadFile = report.items.find(
|
||||
(i) => i.category === "file" && i.rel === "src/orphan.ts",
|
||||
);
|
||||
expect(deadFile).toBeDefined();
|
||||
expect(deadFile?.review).toBe(false);
|
||||
|
||||
const unusedDep = report.items.find(
|
||||
(i) => i.category === "dep" && i.name === "leftoverpkg",
|
||||
);
|
||||
expect(unusedDep).toBeDefined();
|
||||
|
||||
// dynamic-import shim → review, always preserved
|
||||
const dynamic = report.items.find(
|
||||
(i) => i.rel === "src/routes/lazy-route.ts",
|
||||
);
|
||||
expect(dynamic).toBeDefined();
|
||||
expect(dynamic?.review).toBe(true);
|
||||
expect(dynamic?.target).toBe("file");
|
||||
|
||||
// used export + entry file are NOT flagged.
|
||||
expect(report.items.find((i) => i.name === "add")).toBeUndefined();
|
||||
expect(
|
||||
report.items.find((i) => i.rel === "src/index.ts"),
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("renderFindingsMd groups findings under the four category headings", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-"));
|
||||
try {
|
||||
await writeFixture(root);
|
||||
const report = await detectDeadCode(root);
|
||||
const md = renderFindingsMd(report);
|
||||
expect(md).toContain("## Unused exports");
|
||||
expect(md).toContain("## Dead files (zero importers)");
|
||||
expect(md).toContain("## Obsolete shims / migration helpers");
|
||||
expect(md).toContain("## Unused dependencies");
|
||||
expect(md).toMatch(/\[review\] src\/routes\/lazy-route\.ts/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("dead-code E2E (--fix)", () => {
|
||||
let cwd: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
clearChecks();
|
||||
registerCheck(deadCodeCheck);
|
||||
setAgentRunner(fakeAgentRunner);
|
||||
cwd = await mkdtemp(join(tmpdir(), "pygienium-dead-e2e-"));
|
||||
await writeFixture(cwd);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetAgentRunner();
|
||||
await rm(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("removes clearly-dead items, preserves dynamically-imported shim, and writes findings.md + changes.md", async () => {
|
||||
// Make the pre-existing compat.ts scan-detected before the run to also
|
||||
// prove the deterministic scan picks it up regardless of run order.
|
||||
await handleCheckCommand(deadCodeCheck, "--fix", stubCtx(cwd));
|
||||
|
||||
// --- Clearly-dead items are removed --------------------------------
|
||||
// Unused export: removed from util.ts, live export preserved.
|
||||
const util = await readFile(join(cwd, "src", "util.ts"), "utf8");
|
||||
expect(util).not.toContain("unusedHelper");
|
||||
expect(util).toContain("add");
|
||||
|
||||
// Obsolete compat shim: file deleted.
|
||||
expect(existsSync(join(cwd, "src", "compat.ts"))).toBe(false);
|
||||
|
||||
// Dead file: deleted.
|
||||
expect(existsSync(join(cwd, "src", "orphan.ts"))).toBe(false);
|
||||
|
||||
// Unused dependency: removed from package.json.
|
||||
const pkg = JSON.parse(
|
||||
await readFile(join(cwd, "package.json"), "utf8"),
|
||||
) as { dependencies: Record<string, string> };
|
||||
expect(pkg.dependencies.leftoverpkg).toBeUndefined();
|
||||
|
||||
// Dynamically-imported shim: preserved, untouched.
|
||||
expect(existsSync(join(cwd, "src", "routes", "lazy-route.ts"))).toBe(true);
|
||||
const lazy = await readFile(
|
||||
join(cwd, "src", "routes", "lazy-route.ts"),
|
||||
"utf8",
|
||||
);
|
||||
expect(lazy).toContain("registerRoute");
|
||||
|
||||
// findings.md exists and is categorized.
|
||||
expect(existsSync(findingsPath(cwd))).toBe(true);
|
||||
const findingsMd = await readFile(findingsPath(cwd), "utf8");
|
||||
expect(findingsMd).toContain("## Unused exports");
|
||||
expect(findingsMd).toContain("## Dead files (zero importers)");
|
||||
expect(findingsMd).toContain("## Obsolete shims / migration helpers");
|
||||
expect(findingsMd).toContain("## Unused dependencies");
|
||||
|
||||
// changes.md lists the removals and the preserved review item.
|
||||
expect(existsSync(changesPath(cwd))).toBe(true);
|
||||
const changesMd = await readFile(changesPath(cwd), "utf8");
|
||||
expect(changesMd).toContain("## Removed (auto)");
|
||||
expect(changesMd).toContain("src/compat.ts");
|
||||
expect(changesMd).toContain("unusedHelper");
|
||||
expect(changesMd).toContain("leftoverpkg");
|
||||
expect(changesMd).toContain("## Preserved for review (manual)");
|
||||
expect(changesMd).toContain("lazy-route.ts"); // dynamic import preserved
|
||||
});
|
||||
|
||||
it("records the run in run-state and marks the check complete", async () => {
|
||||
await handleCheckCommand(deadCodeCheck, "--fix", stubCtx(cwd));
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["dead-code"].status).toBe("complete");
|
||||
expect(state?.checks["dead-code"].findings).toBeDefined();
|
||||
expect(state?.checks["dead-code"].changes).toBeDefined();
|
||||
});
|
||||
|
||||
it("runs a dry scan (no --fix) and does not write changes.md or touch files", async () => {
|
||||
await handleCheckCommand(deadCodeCheck, "", stubCtx(cwd));
|
||||
|
||||
// Nothing removed without --fix.
|
||||
expect(existsSync(join(cwd, "src", "compat.ts"))).toBe(true);
|
||||
expect(existsSync(join(cwd, "src", "orphan.ts"))).toBe(true);
|
||||
const utils = await readFile(join(cwd, "src", "util.ts"), "utf8");
|
||||
expect(utils).toContain("unusedHelper");
|
||||
|
||||
// Findings recorded, but no changes yet.
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["dead-code"].status).toBe("complete");
|
||||
expect(state?.checks["dead-code"].changes).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dead-code shim auto-delete safety (entry points + prose)", () => {
|
||||
beforeEach(() => {
|
||||
clearChecks();
|
||||
registerCheck(deadCodeCheck);
|
||||
setAgentRunner(fakeAgentRunner);
|
||||
});
|
||||
afterEach(() => {
|
||||
resetAgentRunner();
|
||||
});
|
||||
|
||||
it("never auto-deletes an entry point whose prose mentions 'legacy'", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-entry-"));
|
||||
try {
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ name: "app", main: "src/index.ts" }) + "\n",
|
||||
);
|
||||
// Entry point, zero importers, prose contains 'legacy' — must survive.
|
||||
await writeFile(
|
||||
join(root, "src", "index.ts"),
|
||||
[
|
||||
"// handles legacy payloads",
|
||||
"export function main(): void {}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await handleCheckCommand(deadCodeCheck, "--fix", stubCtx(root));
|
||||
expect(existsSync(join(root, "src", "index.ts"))).toBe(true);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("never auto-deletes a *.test.ts whose description mentions 'deprecated'", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-test-"));
|
||||
try {
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "src", "foo.test.ts"),
|
||||
[
|
||||
"import { describe, it } from 'bun:test';",
|
||||
"describe('app', () => {",
|
||||
" it('still supports the deprecated API', () => {});",
|
||||
"});",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await handleCheckCommand(deadCodeCheck, "--fix", stubCtx(root));
|
||||
expect(existsSync(join(root, "src", "foo.test.ts"))).toBe(true);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags an entry-like file tagged @deprecated for review, not deletion", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-tag-"));
|
||||
try {
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ name: "app", main: "src/index.ts" }) + "\n",
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "src", "index.ts"),
|
||||
["/** @deprecated */", "export function main(): void {}", ""].join(
|
||||
"\n",
|
||||
),
|
||||
);
|
||||
|
||||
const report = await detectDeadCode(root);
|
||||
const entry = report.items.find(
|
||||
(i) => i.rel === "src/index.ts" && i.category === "shim",
|
||||
);
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry?.review).toBe(true);
|
||||
|
||||
await handleCheckCommand(deadCodeCheck, "--fix", stubCtx(root));
|
||||
expect(existsSync(join(root, "src", "index.ts"))).toBe(true);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("dead-code barrel re-export retention", () => {
|
||||
it("keeps modules reachable only through `export * from` / `export {…} from` barrels", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-barrel-"));
|
||||
try {
|
||||
// index.ts is entry-like; it aggregates barrelA, which aggregates
|
||||
// barrelB both by star and by name. Neither barrel may be treated as
|
||||
// a zero-importer dead file, and symbols only reachable through the
|
||||
// star re-export must stay behind a review flag.
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "src", "index.ts"),
|
||||
['export * from "./barrelA";', ""].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "src", "barrelA.ts"),
|
||||
[
|
||||
'export * from "./barrelB";',
|
||||
'export { namedB } from "./barrelB";',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "src", "barrelB.ts"),
|
||||
[
|
||||
"export const value = 1;",
|
||||
"export const namedB = 2;",
|
||||
"export const starOnly = 3;",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const report = await detectDeadCode(root);
|
||||
|
||||
// Neither barrel is a dead-file candidate.
|
||||
expect(
|
||||
report.items.find(
|
||||
(i) => i.category === "file" && i.rel === "src/barrelA.ts",
|
||||
),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
report.items.find(
|
||||
(i) => i.category === "file" && i.rel === "src/barrelB.ts",
|
||||
),
|
||||
).toBeUndefined();
|
||||
|
||||
// Symbols in the star/named re-export target stay `review` — the
|
||||
// deterministic fixer must not auto-delete them.
|
||||
const value = report.items.find(
|
||||
(i) =>
|
||||
i.category === "export" &&
|
||||
i.rel === "src/barrelB.ts" &&
|
||||
i.name === "value",
|
||||
);
|
||||
expect(value).toBeDefined();
|
||||
expect(value?.review).toBe(true);
|
||||
const starOnly = report.items.find(
|
||||
(i) =>
|
||||
i.category === "export" &&
|
||||
i.rel === "src/barrelB.ts" &&
|
||||
i.name === "starOnly",
|
||||
);
|
||||
expect(starOnly).toBeDefined();
|
||||
expect(starOnly?.review).toBe(true);
|
||||
|
||||
// The named re-export is referenced (by barrelA) so it is not dead.
|
||||
expect(
|
||||
report.items.find(
|
||||
(i) =>
|
||||
i.category === "export" &&
|
||||
i.rel === "src/barrelB.ts" &&
|
||||
i.name === "namedB",
|
||||
),
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("`--fix` never deletes a barrel-exported module", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-barrel-e2e-"));
|
||||
try {
|
||||
clearChecks();
|
||||
registerCheck(deadCodeCheck);
|
||||
setAgentRunner(fakeAgentRunner);
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "src", "index.ts"),
|
||||
['export * from "./barrelA";', ""].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "src", "barrelA.ts"),
|
||||
[
|
||||
'export * from "./barrelB";',
|
||||
'export { namedB } from "./barrelB";',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "src", "barrelB.ts"),
|
||||
[
|
||||
"export const value = 1;",
|
||||
"export const namedB = 2;",
|
||||
"export const starOnly = 3;",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await handleCheckCommand(deadCodeCheck, "--fix", stubCtx(root));
|
||||
|
||||
expect(existsSync(join(root, "src", "barrelA.ts"))).toBe(true);
|
||||
expect(existsSync(join(root, "src", "barrelB.ts"))).toBe(true);
|
||||
expect(existsSync(join(root, "src", "index.ts"))).toBe(true);
|
||||
} finally {
|
||||
resetAgentRunner();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("dead-code symbol removal (multi-statement bodies)", () => {
|
||||
it("removes arrow-block consts, object consts, and inline-closing functions whole", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-shapes-"));
|
||||
try {
|
||||
await mkdir(join(root, "src"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "src", "math.ts"),
|
||||
[
|
||||
"export const build = () => {",
|
||||
" const a = 1;",
|
||||
" return a + 2;",
|
||||
"};",
|
||||
"export function packed() {",
|
||||
' return "x"; }',
|
||||
'export const config = { retries: 3, label: "cfg" };',
|
||||
"export function keep(): string {",
|
||||
' return "keep";',
|
||||
"}",
|
||||
"export const keepVar = 9;",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
// Referenced exports keep math.ts alive and `keep`/`keepVar` used.
|
||||
await writeFile(
|
||||
join(root, "src", "app.ts"),
|
||||
[
|
||||
'import { keep, keepVar } from "./math";',
|
||||
"console.log(keep(), keepVar);",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const report = await detectDeadCode(root);
|
||||
const names = report.items
|
||||
.filter((i) => i.category === "export" && i.rel === "src/math.ts")
|
||||
.map((i) => i.name);
|
||||
expect(names).toContain("build");
|
||||
expect(names).toContain("packed");
|
||||
expect(names).toContain("config");
|
||||
expect(names).not.toContain("keep");
|
||||
expect(names).not.toContain("keepVar");
|
||||
|
||||
const { applied } = await applyDeadCodeFixes(report);
|
||||
expect(applied.map((i) => i.name)).toEqual(
|
||||
expect.arrayContaining(["build", "packed", "config"]),
|
||||
);
|
||||
|
||||
const out = await readFile(join(root, "src", "math.ts"), "utf8");
|
||||
expect(out).not.toContain("build");
|
||||
expect(out).not.toContain("packed");
|
||||
expect(out).not.toContain("config");
|
||||
expect(out).toContain("keep");
|
||||
expect(out).toContain("keepVar");
|
||||
// No leftover arrow body from the removed declaration.
|
||||
expect(out).not.toContain("return a + 2");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves a symbol it cannot safely remove instead of corrupting source", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "pygienium-dead-guard-"));
|
||||
try {
|
||||
const file = join(root, "weird.ts");
|
||||
await writeFile(
|
||||
file,
|
||||
[
|
||||
'export const rx = () => /[{;}]/.test("a;");',
|
||||
"export const keep = 1;",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const original = await readFile(file, "utf8");
|
||||
// Hand-built report forces an auto removal attempt on a shape the
|
||||
// scanner does not fully model (regex with braces/semicolons).
|
||||
const { applyDeadCodeFixes: apply } = await import(
|
||||
"../src/checks/dead-code.js"
|
||||
);
|
||||
const report = {
|
||||
target: root,
|
||||
scannedAt: new Date().toISOString(),
|
||||
items: [
|
||||
{
|
||||
category: "export" as const,
|
||||
path: file,
|
||||
rel: "weird.ts",
|
||||
name: "rx",
|
||||
line: 1,
|
||||
target: "symbol" as const,
|
||||
review: false,
|
||||
reason: "test",
|
||||
},
|
||||
],
|
||||
};
|
||||
const { applied } = await apply(report);
|
||||
const after = await readFile(file, "utf8");
|
||||
// Either the removal succeeded cleanly, or the file is untouched —
|
||||
// never a truncated/corrupt intermediate.
|
||||
if (applied.length === 0) {
|
||||
expect(after).toBe(original);
|
||||
} else {
|
||||
expect(after).not.toContain("rx");
|
||||
expect(after).toContain("keep");
|
||||
}
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user