import { mkdtempSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { checkFile } from "./checker.js"; function createTempFile(content: string): string { const directory = mkdtempSync(join(tmpdir(), "iku-test-")); const filePath = join(directory, "test.ts"); writeFileSync(filePath, content); return filePath; } function cleanupTempFile(filePath: string): void { try { unlinkSync(filePath); } catch {} } describe("checkFile", () => { describe("imports", () => { it("passes when imports have no blank lines between them", () => { const content = `import { foo } from "foo"; import { bar } from "bar"; import { baz } from "baz"; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); it("fails when imports have blank lines between them", () => { const content = `import { foo } from "foo"; import { bar } from "bar"; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("Blank line between imports"); }); }); describe("same statement types", () => { it("passes when same statement types are grouped without blank lines", () => { const content = `const a = 1; const b = 2; const c = 3; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); it("fails when same statement types have unnecessary blank lines", () => { const content = `const a = 1; const b = 2; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain( "Unnecessary blank line between same statement types", ); }); it("passes for consecutive expression statements without blank lines", () => { const content = `console.log("a"); console.log("b"); console.log("c"); `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); }); describe("different statement types", () => { it("passes when different statement types have blank lines between them", () => { const content = `const a = 1; console.log(a); return a; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); it("fails when different statement types have no blank line between them", () => { const content = `const a = 1; console.log(a); `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain( "Missing blank line between different statement types", ); }); it("requires blank line between variable declaration and return", () => { const content = `function test() { const result = 42; return result; } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("variableDeclaration -> return"); }); it("requires blank line between expression and if statement", () => { const content = `function test() { console.log("start"); if (true) { return; } } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("Missing blank line around scoped block"); }); }); describe("scoped blocks", () => { it("passes when scoped if blocks have blank lines between them", () => { const content = `function test(a: number, b: number) { if (a > 0) { return a; } if (b > 0) { return b; } return 0; } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); it("fails when scoped if blocks have no blank line between them", () => { const content = `function test(a: number, b: number) { if (a > 0) { return a; } if (b > 0) { return b; } } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("Missing blank line around scoped block"); }); it("passes when scoped for loops have blank lines between them", () => { const content = `function test(items: number[]) { for (const item of items) { console.log(item); } for (let i = 0; i < 10; i++) { console.log(i); } } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); it("fails when scoped for loops have no blank line between them", () => { const content = `function test(items: number[]) { for (const item of items) { console.log(item); } for (let i = 0; i < 10; i++) { console.log(i); } } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("Missing blank line around scoped block"); }); }); describe("nested blocks", () => { it("checks formatting inside nested function bodies", () => { const content = `function outer() { function inner() { const a = 1; console.log(a); } } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("variableDeclaration -> expression"); }); it("checks formatting inside arrow function bodies", () => { const content = `const fn = () => { const a = 1; return a; }; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(1); expect(errors[0]).toContain("variableDeclaration -> return"); }); }); describe("mixed scenarios", () => { it("handles complex file with multiple statement types correctly", () => { const content = `import { foo } from "foo"; import { bar } from "bar"; const a = 1; const b = 2; console.log(a); console.log(b); if (a > b) { return a; } if (b > a) { return b; } `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(0); }); it("detects multiple errors in a single file", () => { const content = `import { foo } from "foo"; import { bar } from "bar"; const a = 1; const b = 2; console.log(a); `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors).toHaveLength(3); }); }); describe("comments", () => { it("detects single-line comments by default", () => { const content = `const a = 1; // this is a comment const b = 2; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors.some((error) => error.includes("Comment detected"))).toBe( true, ); }); it("detects multi-line comments by default", () => { const content = `const a = 1; /* this is a multi-line comment */ const b = 2; `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors.some((error) => error.includes("Comment detected"))).toBe( true, ); }); it("detects trailing comments by default", () => { const content = `const a = 1; // trailing comment `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors.some((error) => error.includes("Comment detected"))).toBe( true, ); }); it("allows comments when noComments is false", () => { const content = `const a = 1; // this is a comment const b = 2; `; const filePath = createTempFile(content); const errors = checkFile(filePath, { noComments: false }); cleanupTempFile(filePath); expect(errors.some((error) => error.includes("Comment detected"))).toBe( false, ); }); it("detects JSDoc comments by default", () => { const content = `/** * This is a JSDoc comment */ function test() {} `; const filePath = createTempFile(content); const errors = checkFile(filePath); cleanupTempFile(filePath); expect(errors.some((error) => error.includes("Comment detected"))).toBe( true, ); }); }); });