aboutsummaryrefslogtreecommitdiff
path: root/adapter_ecmascript_test.go
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-05 08:09:00 +0000
committerFuwn <[email protected]>2026-02-05 08:09:00 +0000
commite240a3468d0fa2afe427430679b3651e8a79b870 (patch)
tree958ee121d80fa414d4513906975f875d53a1c2fe /adapter_ecmascript_test.go
parentrefactor(formatter): Replace rewrite logic with engine (diff)
downloadiku-e240a3468d0fa2afe427430679b3651e8a79b870.tar.xz
iku-e240a3468d0fa2afe427430679b3651e8a79b870.zip
feat(adapter): Add EcmaScript adapter for JS/TS/JSX/TSX formatting
Diffstat (limited to 'adapter_ecmascript_test.go')
-rw-r--r--adapter_ecmascript_test.go462
1 files changed, 462 insertions, 0 deletions
diff --git a/adapter_ecmascript_test.go b/adapter_ecmascript_test.go
new file mode 100644
index 0000000..ee739ac
--- /dev/null
+++ b/adapter_ecmascript_test.go
@@ -0,0 +1,462 @@
+package main
+
+import (
+ "github.com/Fuwn/iku/engine"
+ "testing"
+)
+
+type ecmaScriptTestCase struct {
+ name string
+ source string
+ expected string
+}
+
+var ecmaScriptTestCases = []ecmaScriptTestCase{
+ {
+ name: "blank lines around if block",
+ source: `const x = 1;
+if (x > 0) {
+ doSomething();
+}
+const y = 2;
+`,
+ expected: `const x = 1;
+
+if (x > 0) {
+ doSomething();
+}
+
+const y = 2;
+`,
+ },
+ {
+ name: "blank lines around for loop",
+ source: `const items = [1, 2, 3];
+for (const item of items) {
+ process(item);
+}
+const result = done();
+`,
+ expected: `const items = [1, 2, 3];
+
+for (const item of items) {
+ process(item);
+}
+
+const result = done();
+`,
+ },
+ {
+ name: "blank lines between different top-level types",
+ source: `import { foo } from "bar";
+const x = 1;
+function main() {
+ return x;
+}
+class Foo {
+ bar() {}
+}
+`,
+ expected: `import { foo } from "bar";
+
+const x = 1;
+
+function main() {
+ return x;
+}
+
+class Foo {
+ bar() {}
+}
+`,
+ },
+ {
+ name: "no blank between same type",
+ source: `const x = 1;
+const y = 2;
+const z = 3;
+`,
+ expected: `const x = 1;
+const y = 2;
+const z = 3;
+`,
+ },
+ {
+ name: "consecutive imports stay together",
+ source: `import { a } from "a";
+import { b } from "b";
+import { c } from "c";
+`,
+ expected: `import { a } from "a";
+import { b } from "b";
+import { c } from "c";
+`,
+ },
+ {
+ name: "switch with case clauses",
+ source: `const x = getValue();
+switch (x) {
+case "a":
+ handleA();
+ break;
+case "b":
+ handleB();
+ break;
+}
+cleanup();
+`,
+ expected: `const x = getValue();
+
+switch (x) {
+case "a":
+ handleA();
+ break;
+case "b":
+ handleB();
+ break;
+}
+
+cleanup();
+`,
+ },
+ {
+ name: "try catch finally",
+ source: `setup();
+try {
+ riskyOperation();
+} catch (error) {
+ handleError(error);
+} finally {
+ cleanup();
+}
+done();
+`,
+ expected: `setup();
+
+try {
+ riskyOperation();
+} catch (error) {
+ handleError(error);
+} finally {
+ cleanup();
+}
+
+done();
+`,
+ },
+ {
+ name: "consecutive scoped blocks",
+ source: `if (a) {
+ doA();
+}
+if (b) {
+ doB();
+}
+`,
+ expected: `if (a) {
+ doA();
+}
+
+if (b) {
+ doB();
+}
+`,
+ },
+ {
+ name: "export prefixes",
+ source: `export const x = 1;
+export function foo() {
+ return x;
+}
+export class Bar {
+ baz() {}
+}
+`,
+ expected: `export const x = 1;
+
+export function foo() {
+ return x;
+}
+
+export class Bar {
+ baz() {}
+}
+`,
+ },
+ {
+ name: "async function",
+ source: `const data = prepare();
+async function fetchData() {
+ return await fetch(url);
+}
+const result = process();
+`,
+ expected: `const data = prepare();
+
+async function fetchData() {
+ return await fetch(url);
+}
+
+const result = process();
+`,
+ },
+ {
+ name: "typescript interface and type",
+ source: `type ID = string;
+interface User {
+ name: string;
+ id: ID;
+}
+const defaultUser: User = { name: "", id: "" };
+`,
+ expected: `type ID = string;
+
+interface User {
+ name: string;
+ id: ID;
+}
+
+const defaultUser: User = { name: "", id: "" };
+`,
+ },
+ {
+ name: "multi-line function call preserved",
+ source: `const result = someFunction(
+ longArgument,
+ otherArgument,
+);
+const next = 1;
+`,
+ expected: `const result = someFunction(
+ longArgument,
+ otherArgument,
+);
+const next = 1;
+`,
+ },
+ {
+ name: "method chaining preserved",
+ source: `const result = someArray
+ .filter(x => x > 0)
+ .map(x => x * 2);
+const next = 1;
+`,
+ expected: `const result = someArray
+ .filter(x => x > 0)
+ .map(x => x * 2);
+const next = 1;
+`,
+ },
+ {
+ name: "block comment passthrough",
+ source: `/*
+ * This is a block comment
+ */
+const x = 1;
+`,
+ expected: `/*
+ * This is a block comment
+ */
+const x = 1;
+`,
+ },
+ {
+ name: "collapses extra blank lines",
+ source: `const x = 1;
+
+
+const y = 2;
+`,
+ expected: `const x = 1;
+const y = 2;
+`,
+ },
+ {
+ name: "while loop",
+ source: `let count = 0;
+while (count < 10) {
+ count++;
+}
+const done = true;
+`,
+ expected: `let count = 0;
+
+while (count < 10) {
+ count++;
+}
+
+const done = true;
+`,
+ },
+ {
+ name: "nested scopes",
+ source: `function main() {
+ const x = 1;
+ if (x > 0) {
+ for (let i = 0; i < x; i++) {
+ process(i);
+ }
+ cleanup();
+ }
+ return x;
+}
+`,
+ expected: `function main() {
+ const x = 1;
+
+ if (x > 0) {
+ for (let i = 0; i < x; i++) {
+ process(i);
+ }
+
+ cleanup();
+ }
+
+ return x;
+}
+`,
+ },
+ {
+ name: "template literal passthrough",
+ source: "const x = `\nhello\n\nworld\n`;\nconst y = 1;\n",
+ expected: "const x = `\nhello\n\nworld\n`;\nconst y = 1;\n",
+ },
+ {
+ name: "jsx expressions",
+ source: `function Component() {
+ const data = useMemo();
+ if (!data) {
+ return null;
+ }
+ return (
+ <div>
+ <span>{data}</span>
+ </div>
+ );
+}
+`,
+ expected: `function Component() {
+ const data = useMemo();
+
+ if (!data) {
+ return null;
+ }
+
+ return (
+ <div>
+ <span>{data}</span>
+ </div>
+ );
+}
+`,
+ },
+ {
+ name: "expression after scoped block",
+ source: `function main() {
+ if (x) {
+ return;
+ }
+ doSomething();
+}
+`,
+ expected: `function main() {
+ if (x) {
+ return;
+ }
+
+ doSomething();
+}
+`,
+ },
+ {
+ name: "enum declaration",
+ source: `type Color = string;
+enum Direction {
+ Up,
+ Down,
+}
+const x = Direction.Up;
+`,
+ expected: `type Color = string;
+
+enum Direction {
+ Up,
+ Down,
+}
+
+const x = Direction.Up;
+`,
+ },
+}
+
+func TestEcmaScriptAdapter(t *testing.T) {
+ for _, testCase := range ecmaScriptTestCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ adapter := &EcmaScriptAdapter{}
+ _, events, err := adapter.Analyze([]byte(testCase.source))
+
+ if err != nil {
+ t.Fatalf("adapter error: %v", err)
+ }
+
+ formattingEngine := &engine.Engine{CommentMode: engine.CommentsFollow}
+ result := formattingEngine.FormatToString(events)
+
+ if result != testCase.expected {
+ t.Errorf("mismatch\ngot:\n%s\nwant:\n%s", result, testCase.expected)
+ }
+ })
+ }
+}
+
+func TestClassifyEcmaScriptStatement(t *testing.T) {
+ cases := []struct {
+ input string
+ expectedType string
+ expectedScope bool
+ }{
+ {"function foo() {", "function", true},
+ {"async function foo() {", "function", true},
+ {"export function foo() {", "function", true},
+ {"export default function() {", "function", true},
+ {"class Foo {", "class", true},
+ {"export class Foo {", "class", true},
+ {"if (x) {", "if", true},
+ {"else if (y) {", "if", true},
+ {"else {", "if", true},
+ {"for (const x of items) {", "for", true},
+ {"while (true) {", "while", true},
+ {"do {", "do", true},
+ {"switch (x) {", "switch", true},
+ {"try {", "try", true},
+ {"interface Foo {", "interface", true},
+ {"enum Direction {", "enum", true},
+ {"namespace Foo {", "namespace", true},
+ {"const x = 1;", "const", false},
+ {"let x = 1;", "let", false},
+ {"var x = 1;", "var", false},
+ {"import { foo } from 'bar';", "import", false},
+ {"type Foo = string;", "type", false},
+ {"return x;", "return", false},
+ {"return;", "return", false},
+ {"throw new Error();", "throw", false},
+ {"await fetch(url);", "await", false},
+ {"yield value;", "yield", false},
+ {"export const x = 1;", "const", false},
+ {"export default class Foo {", "class", true},
+ {"declare const x: number;", "const", false},
+ {"declare function foo(): void;", "function", true},
+ {"foo();", "", false},
+ {"x = 1;", "", false},
+ {"", "", false},
+ }
+
+ for _, testCase := range cases {
+ statementType, isScoped := classifyEcmaScriptStatement(testCase.input)
+
+ if statementType != testCase.expectedType || isScoped != testCase.expectedScope {
+ t.Errorf("classifyEcmaScriptStatement(%q) = (%q, %v), want (%q, %v)",
+ testCase.input, statementType, isScoped, testCase.expectedType, testCase.expectedScope)
+ }
+ }
+}