diff options
| -rw-r--r-- | packages/iku/package.json | 7 | ||||
| -rw-r--r-- | packages/iku/src/checker.test.ts | 283 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 3 |
3 files changed, 291 insertions, 2 deletions
diff --git a/packages/iku/package.json b/packages/iku/package.json index a3455ff..e468e89 100644 --- a/packages/iku/package.json +++ b/packages/iku/package.json @@ -8,7 +8,9 @@ }, "scripts": { "build": "tsc", - "check": "node --import tsx ./src/cli.ts" + "check": "node --import tsx ./src/cli.ts", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "typescript": "^5.8.3" @@ -16,6 +18,7 @@ "devDependencies": { "@imemio/typescript-config": "workspace:*", "@types/node": "^22.15.21", - "tsx": "^4.20.3" + "tsx": "^4.20.3", + "vitest": "^3.0.5" } } diff --git a/packages/iku/src/checker.test.ts b/packages/iku/src/checker.test.ts new file mode 100644 index 0000000..58a861c --- /dev/null +++ b/packages/iku/src/checker.test.ts @@ -0,0 +1,283 @@ +import { describe, it, expect } from "vitest"; +import { writeFileSync, unlinkSync, mkdtempSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +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("expression -> if"); + }); + }); + 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 between scoped blocks of same type"); + }); + 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 between scoped blocks of same type"); + }); + }); + 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); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 523cb23..41fd53d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: tsx: specifier: ^4.20.3 version: 4.21.0 + vitest: + specifier: ^3.0.5 packages/mcp: dependencies: |