diff options
Diffstat (limited to 'adapter_ecmascript_test.go')
| -rw-r--r-- | adapter_ecmascript_test.go | 462 |
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) + } + } +} |