aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/iku/package.json7
-rw-r--r--packages/iku/src/checker.test.ts283
-rw-r--r--pnpm-lock.yaml3
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: