diff --git a/src/enum/enum.test.ts b/src/enum/enum.test.ts index 0af8d8a..82b1d67 100644 --- a/src/enum/enum.test.ts +++ b/src/enum/enum.test.ts @@ -1,681 +1,100 @@ +import { executeTest } from "../utils/test.ts"; import { enumCodemod } from "./enum.ts"; -import { spawn } from "node:child_process"; -import { assertEquals } from "jsr:@std/assert"; -/** - * Checks a TypeScript file using tsc. - * @example - * await checkTsFile("src/index.ts"); - */ -async function checkTsFile(filePath: string): Promise { - await new Promise((resolve, reject) => { - const tsc = spawn("deno", ["check", filePath]); - - tsc.stderr.on("data", (data) => { - console.error(data.toString()); - }); - - tsc.on("close", (code: number) => { - if (code === 0) { - console.log("Typecheck succeeded 🎉"); - resolve(undefined); - } else { - reject(new Error("Typecheck failed")); - } - }); - }); -} - -/** - * Test utilities for codemod testing with proper cleanup - * @example - * const utils = new TestUtils(); - * await utils.createTestFile("test.ts", "enum Color { Red }"); - */ -class TestUtils { - private tempFiles = new Set(); - - async createTestFile(filePath: string, content: string): Promise { - await Deno.writeTextFile(filePath, content); - this.tempFiles.add(filePath); - } - - async readTestFile(filePath: string): Promise { - return await Deno.readTextFile(filePath); - } - - async runAndReadCodemod(filePath: string): Promise { - enumCodemod(filePath); - return await this.readTestFile(filePath); - } - - async cleanup(): Promise { - for (const file of this.tempFiles) { - try { - await Deno.remove(file); - } catch { - // Ignore if file doesn't exist - } - } - this.tempFiles.clear(); - } -} +const testFn = executeTest(enumCodemod); Deno.test("converts string enum to object", async () => { - const utils = new TestUtils(); - const testFile = "string_enum_test.ts"; - - try { - const originalContent = `enum Color { - Red = "red", - Blue = "blue", - Green = "green" -} - -function getColorName(color: Color): string { - return color === Color.Red ? "Red color" : "Other color"; -} - -const userColor: Color = Color.Blue;`; - - const expectedOutput = `const Color = { - Red: "red", - Blue: "blue", - Green: "green", -} as const; -type ColorType = typeof Color[keyof typeof Color]; - -function getColorName(color: ColorType): string { - return color === Color.Red ? "Red color" : "Other color"; -} - -const userColor: ColorType = Color.Blue;`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/string_enum_test.ts", + inFile: "src/enum/spec/string-enum.in.ts", + outFile: "src/enum/spec/string-enum.out.ts", + }); }); Deno.test("converts numeric enum to object", async () => { - const utils = new TestUtils(); - const testFile = "numeric_enum_test.ts"; - - try { - const originalContent = `enum Status { - Pending = 0, - Active = 1, - Inactive = 2 -} - -interface User { - name: string; - status: Status; -} - -function createUser(name: string): User { - return { - name, - status: Status.Pending - }; -}`; - - const expectedOutput = `const Status = { - Pending: 0, - Active: 1, - Inactive: 2, -} as const; -type StatusType = typeof Status[keyof typeof Status]; - -interface User { - name: string; - status: StatusType; -} - -function createUser(name: string): User { - return { - name, - status: Status.Pending - }; -}`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/numeric_enum_test.ts", + inFile: "src/enum/spec/numeric-enum.in.ts", + outFile: "src/enum/spec/numeric-enum.out.ts", + }); }); Deno.test("converts auto-incrementing enum to object", async () => { - const utils = new TestUtils(); - const testFile = "auto_enum_test.ts"; - - try { - const originalContent = `enum Direction { - North, - South, - East, - West -} - -const compass: Direction[] = [ - Direction.North, - Direction.South, - Direction.East, - Direction.West -];`; - - const expectedOutput = `const Direction = { - North: "North", - South: "South", - East: "East", - West: "West", -} as const; -type DirectionType = typeof Direction[keyof typeof Direction]; - -const compass: DirectionType[] = [ - Direction.North, - Direction.South, - Direction.East, - Direction.West -];`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/auto_enum_test.ts", + inFile: "src/enum/spec/auto-enum.in.ts", + outFile: "src/enum/spec/auto-enum.out.ts", + }); }); Deno.test("converts mixed value enum to object", async () => { - const utils = new TestUtils(); - const testFile = "mixed_enum_test.ts"; - - try { - const originalContent = `enum MixedEnum { - First = 1, - Second = "second", - Third = 3, - Fourth = "fourth" -} - -function handleMixed(value: MixedEnum): string { - switch (value) { - case MixedEnum.First: - return "Number one"; - case MixedEnum.Second: - return "String second"; - case MixedEnum.Third: - return "Number three"; - case MixedEnum.Fourth: - return "String fourth"; - default: - return "Unknown"; - } -}`; - - const expectedOutput = `const MixedEnum = { - First: 1, - Second: "second", - Third: 3, - Fourth: "fourth", -} as const; -type MixedEnumType = typeof MixedEnum[keyof typeof MixedEnum]; - -function handleMixed(value: MixedEnumType): string { - switch (value) { - case MixedEnum.First: - return "Number one"; - case MixedEnum.Second: - return "String second"; - case MixedEnum.Third: - return "Number three"; - case MixedEnum.Fourth: - return "String fourth"; - default: - return "Unknown"; - } -}`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/mixed_enum_test.ts", + inFile: "src/enum/spec/mixed-enum.in.ts", + outFile: "src/enum/spec/mixed-enum.out.ts", + }); }); Deno.test("converts multiple enums in single file", async () => { - const utils = new TestUtils(); - const testFile = "multiple_enums_test.ts"; - - try { - const originalContent = `enum Color { - Red = "red", - Blue = "blue" -} - -enum Status { - Active = 1, - Inactive = 0 -} - -enum Priority { - Low, - Medium, - High -} - -interface Task { - color: Color; - status: Status; - priority: Priority; -} - -function createTask(): Task { - return { - color: Color.Red, - status: Status.Active, - priority: Priority.High - }; -} - -class TaskManager { - private tasks: Task[] = []; - - addTask(color: Color = Color.Blue): void { - this.tasks.push({ - color, - status: Status.Inactive, - priority: Priority.Medium - }); - } -}`; - - const expectedOutput = `const Color = { - Red: "red", - Blue: "blue", -} as const; -type ColorType = typeof Color[keyof typeof Color]; - -const Status = { - Active: 1, - Inactive: 0, -} as const; -type StatusType = typeof Status[keyof typeof Status]; - -const Priority = { - Low: "Low", - Medium: "Medium", - High: "High", -} as const; -type PriorityType = typeof Priority[keyof typeof Priority]; - -interface Task { - color: ColorType; - status: StatusType; - priority: PriorityType; -} - -function createTask(): Task { - return { - color: Color.Red, - status: Status.Active, - priority: Priority.High - }; -} - -class TaskManager { - private tasks: Task[] = []; - - addTask(color: ColorType = Color.Blue): void { - this.tasks.push({ - color, - status: Status.Inactive, - priority: Priority.Medium - }); - } -}`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/multiple_enums_test.ts", + inFile: "src/enum/spec/multiple-enums.in.ts", + outFile: "src/enum/spec/multiple-enums.out.ts", + }); }); Deno.test("preserves enum usage in complex scenarios", async () => { - const utils = new TestUtils(); - const testFile = "complex_usage_test.ts"; - - try { - const originalContent = `enum ApiEndpoint { - Users = "/api/users", - Posts = "/api/posts", - Comments = "/api/comments" -} - -enum HttpMethod { - GET = "GET", - POST = "POST", - PUT = "PUT", - DELETE = "DELETE" -} - -enum ResponseStatus { - Success = 200, - NotFound = 404, - ServerError = 500 -} - -interface ApiRequest { - endpoint: ApiEndpoint; - method: HttpMethod; -} - -interface ApiResponse { - status: ResponseStatus; - data?: T; - error?: string; -} - -class ApiClient { - private baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - - async makeRequest( - endpoint: ApiEndpoint, - method: HttpMethod = HttpMethod.GET - ): Promise> { - const url = \`\${this.baseUrl}\${endpoint}\`; - - try { - const response = await fetch(url, { method }); - - if (response.ok) { - const data = await response.json(); - return { - status: ResponseStatus.Success, - data - }; - } else { - return { - status: response.status === 404 ? ResponseStatus.NotFound : ResponseStatus.ServerError, - error: \`Request failed with status \${response.status}\` - }; - } - } catch (error: any) { - return { - status: ResponseStatus.ServerError, - error: error.message - }; - } - } -} - -// Usage examples -const client = new ApiClient("https://api.example.com"); -const usersRequest: ApiRequest = { - endpoint: ApiEndpoint.Users, - method: HttpMethod.GET -};`; - - const expectedOutput = `const ApiEndpoint = { - Users: "/api/users", - Posts: "/api/posts", - Comments: "/api/comments", -} as const; -type ApiEndpointType = typeof ApiEndpoint[keyof typeof ApiEndpoint]; - -const HttpMethod = { - GET: "GET", - POST: "POST", - PUT: "PUT", - DELETE: "DELETE", -} as const; -type HttpMethodType = typeof HttpMethod[keyof typeof HttpMethod]; - -const ResponseStatus = { - Success: 200, - NotFound: 404, - ServerError: 500, -} as const; -type ResponseStatusType = typeof ResponseStatus[keyof typeof ResponseStatus]; - -interface ApiRequest { - endpoint: ApiEndpointType; - method: HttpMethodType; -} - -interface ApiResponse { - status: ResponseStatusType; - data?: T; - error?: string; -} - -class ApiClient { - private baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - - async makeRequest( - endpoint: ApiEndpointType, - method: HttpMethodType = HttpMethod.GET - ): Promise> { - const url = \`\${this.baseUrl}\${endpoint}\`; - - try { - const response = await fetch(url, { method }); - - if (response.ok) { - const data = await response.json(); - return { - status: ResponseStatus.Success, - data - }; - } else { - return { - status: response.status === 404 ? ResponseStatus.NotFound : ResponseStatus.ServerError, - error: \`Request failed with status \${response.status}\` - }; - } - } catch (error: any) { - return { - status: ResponseStatus.ServerError, - error: error.message - }; - } - } -} - -// Usage examples -const client = new ApiClient("https://api.example.com"); -const usersRequest: ApiRequest = { - endpoint: ApiEndpoint.Users, - method: HttpMethod.GET -};`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/complex_usage_test.ts", + inFile: "src/enum/spec/complex-usage.in.ts", + outFile: "src/enum/spec/complex-usage.out.ts", + }); }); Deno.test("handles const enum conversion", async () => { - const utils = new TestUtils(); - const testFile = "const_enum_test.ts"; - - try { - const originalContent = `const enum LogLevel { - Debug = 0, - Info = 1, - Warn = 2, - Error = 3 -} - -function log(level: LogLevel, message: string): void { - if (level >= LogLevel.Info) { - console.log(\`[\${level}] \${message}\`); - } -} - -log(LogLevel.Error, "Something went wrong");`; - - const expectedOutput = `const LogLevel = { - Debug: 0, - Info: 1, - Warn: 2, - Error: 3, -} as const; -type LogLevelType = typeof LogLevel[keyof typeof LogLevel]; - -function log(level: LogLevelType, message: string): void { - if (level >= LogLevel.Info) { - console.log(\`[\${level}] \${message}\`); - } -} - -log(LogLevel.Error, "Something went wrong");`; - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/const_enum_test.ts", + inFile: "src/enum/spec/const-enum.in.ts", + outFile: "src/enum/spec/const-enum.out.ts", + }); }); Deno.test("preserves comments", async () => { - const utils = new TestUtils(); - const testFile = "comments_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/enum/spec/comments.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/enum/spec/comments.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/comments_test.ts", + inFile: "src/enum/spec/comments.in.ts", + outFile: "src/enum/spec/comments.out.ts", + }); }); Deno.test("comprehensive enum transformation - full sample", async () => { - const utils = new TestUtils(); - const testFile = "comprehensive_sample.ts"; - - try { - const sampleContent = await Deno.readTextFile( - "src/enum/spec/comprehensive.in.ts", - ); - - await utils.createTestFile(testFile, sampleContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - await Deno.writeTextFile("tmp/out.ts", result); - assertEquals( - result, - await Deno.readTextFile("src/enum/spec/comprehensive.out.ts"), - ); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/comprehensive_sample.ts", + inFile: "src/enum/spec/comprehensive.in.ts", + outFile: "src/enum/spec/comprehensive.out.ts", + }); }); -Deno.test( - "enum usage in destructuring and object patterns", - async () => { - const utils = new TestUtils(); - const testFile = "destructuring_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/enum/spec/object-patterns.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/enum/spec/object-patterns.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } - }, -); - -Deno.test( - "enum usage in template literals and conditionals", - async () => { - const utils = new TestUtils(); - const testFile = "templates_conditionals_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/enum/spec/template-literal.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/enum/spec/template-literal.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } - }, -); - -Deno.test( - "enum usage with arrays, maps and complex data structures", - async () => { - const utils = new TestUtils(); - const testFile = "data_structures_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/enum/spec/data-structures.in.ts", - ); +Deno.test("enum usage in destructuring and object patterns", async () => { + await testFn({ + testFile: "tmp/destructuring_test.ts", + inFile: "src/enum/spec/object-patterns.in.ts", + outFile: "src/enum/spec/object-patterns.out.ts", + }); +}); - const expectedOutput = await Deno.readTextFile( - "src/enum/spec/data-structures.out.ts", - ); +Deno.test("enum usage in template literals and conditionals", async () => { + await testFn({ + testFile: "tmp/templates_conditionals_test.ts", + inFile: "src/enum/spec/template-literal.in.ts", + outFile: "src/enum/spec/template-literal.out.ts", + }); +}); - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } - }, -); +Deno.test("enum usage with arrays, maps and complex data structures", async () => { + await testFn({ + testFile: "tmp/data_structures_test.ts", + inFile: "src/enum/spec/data-structures.in.ts", + outFile: "src/enum/spec/data-structures.out.ts", + }); +}); diff --git a/src/enum/spec/auto-enum.in.ts b/src/enum/spec/auto-enum.in.ts new file mode 100644 index 0000000..2451104 --- /dev/null +++ b/src/enum/spec/auto-enum.in.ts @@ -0,0 +1,13 @@ +enum Direction { + North, + South, + East, + West, +} + +const compass: Direction[] = [ + Direction.North, + Direction.South, + Direction.East, + Direction.West, +]; diff --git a/src/enum/spec/auto-enum.out.ts b/src/enum/spec/auto-enum.out.ts new file mode 100644 index 0000000..c742519 --- /dev/null +++ b/src/enum/spec/auto-enum.out.ts @@ -0,0 +1,14 @@ +const Direction = { + North: "North", + South: "South", + East: "East", + West: "West", +} as const; +type DirectionType = typeof Direction[keyof typeof Direction]; + +const compass: DirectionType[] = [ + Direction.North, + Direction.South, + Direction.East, + Direction.West, +]; diff --git a/src/enum/spec/complex-usage.in.ts b/src/enum/spec/complex-usage.in.ts new file mode 100644 index 0000000..f76ce81 --- /dev/null +++ b/src/enum/spec/complex-usage.in.ts @@ -0,0 +1,75 @@ +enum ApiEndpoint { + Users = "/api/users", + Posts = "/api/posts", + Comments = "/api/comments", +} + +enum HttpMethod { + GET = "GET", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", +} + +enum ResponseStatus { + Success = 200, + NotFound = 404, + ServerError = 500, +} + +interface ApiRequest { + endpoint: ApiEndpoint; + method: HttpMethod; +} + +interface ApiResponse { + status: ResponseStatus; + data?: T; + error?: string; +} + +class ApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + async makeRequest( + endpoint: ApiEndpoint, + method: HttpMethod = HttpMethod.GET, + ): Promise> { + const url = `${this.baseUrl}${endpoint}`; + + try { + const response = await fetch(url, { method }); + + if (response.ok) { + const data = await response.json(); + return { + status: ResponseStatus.Success, + data, + }; + } else { + return { + status: response.status === 404 + ? ResponseStatus.NotFound + : ResponseStatus.ServerError, + error: `Request failed with status ${response.status}`, + }; + } + } catch (error: any) { + return { + status: ResponseStatus.ServerError, + error: error.message, + }; + } + } +} + +// Usage examples +const client = new ApiClient("https://api.example.com"); +const usersRequest: ApiRequest = { + endpoint: ApiEndpoint.Users, + method: HttpMethod.GET, +}; diff --git a/src/enum/spec/complex-usage.out.ts b/src/enum/spec/complex-usage.out.ts new file mode 100644 index 0000000..27f6714 --- /dev/null +++ b/src/enum/spec/complex-usage.out.ts @@ -0,0 +1,78 @@ +const ApiEndpoint = { + Users: "/api/users", + Posts: "/api/posts", + Comments: "/api/comments", +} as const; +type ApiEndpointType = typeof ApiEndpoint[keyof typeof ApiEndpoint]; + +const HttpMethod = { + GET: "GET", + POST: "POST", + PUT: "PUT", + DELETE: "DELETE", +} as const; +type HttpMethodType = typeof HttpMethod[keyof typeof HttpMethod]; + +const ResponseStatus = { + Success: 200, + NotFound: 404, + ServerError: 500, +} as const; +type ResponseStatusType = typeof ResponseStatus[keyof typeof ResponseStatus]; + +interface ApiRequest { + endpoint: ApiEndpointType; + method: HttpMethodType; +} + +interface ApiResponse { + status: ResponseStatusType; + data?: T; + error?: string; +} + +class ApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + async makeRequest( + endpoint: ApiEndpointType, + method: HttpMethodType = HttpMethod.GET, + ): Promise> { + const url = `${this.baseUrl}${endpoint}`; + + try { + const response = await fetch(url, { method }); + + if (response.ok) { + const data = await response.json(); + return { + status: ResponseStatus.Success, + data, + }; + } else { + return { + status: response.status === 404 + ? ResponseStatus.NotFound + : ResponseStatus.ServerError, + error: `Request failed with status ${response.status}`, + }; + } + } catch (error: any) { + return { + status: ResponseStatus.ServerError, + error: error.message, + }; + } + } +} + +// Usage examples +const client = new ApiClient("https://api.example.com"); +const usersRequest: ApiRequest = { + endpoint: ApiEndpoint.Users, + method: HttpMethod.GET, +}; diff --git a/src/enum/spec/const-enum.in.ts b/src/enum/spec/const-enum.in.ts new file mode 100644 index 0000000..caa36d7 --- /dev/null +++ b/src/enum/spec/const-enum.in.ts @@ -0,0 +1,14 @@ +const enum LogLevel { + Debug = 0, + Info = 1, + Warn = 2, + Error = 3, +} + +function log(level: LogLevel, message: string): void { + if (level >= LogLevel.Info) { + console.log(`[${level}] ${message}`); + } +} + +log(LogLevel.Error, "Something went wrong"); diff --git a/src/enum/spec/const-enum.out.ts b/src/enum/spec/const-enum.out.ts new file mode 100644 index 0000000..be894ea --- /dev/null +++ b/src/enum/spec/const-enum.out.ts @@ -0,0 +1,15 @@ +const LogLevel = { + Debug: 0, + Info: 1, + Warn: 2, + Error: 3, +} as const; +type LogLevelType = typeof LogLevel[keyof typeof LogLevel]; + +function log(level: LogLevelType, message: string): void { + if (level >= LogLevel.Info) { + console.log(`[${level}] ${message}`); + } +} + +log(LogLevel.Error, "Something went wrong"); diff --git a/src/enum/spec/mixed-enum.in.ts b/src/enum/spec/mixed-enum.in.ts new file mode 100644 index 0000000..bd59e63 --- /dev/null +++ b/src/enum/spec/mixed-enum.in.ts @@ -0,0 +1,21 @@ +enum MixedEnum { + First = 1, + Second = "second", + Third = 3, + Fourth = "fourth", +} + +function handleMixed(value: MixedEnum): string { + switch (value) { + case MixedEnum.First: + return "Number one"; + case MixedEnum.Second: + return "String second"; + case MixedEnum.Third: + return "Number three"; + case MixedEnum.Fourth: + return "String fourth"; + default: + return "Unknown"; + } +} diff --git a/src/enum/spec/mixed-enum.out.ts b/src/enum/spec/mixed-enum.out.ts new file mode 100644 index 0000000..a486d96 --- /dev/null +++ b/src/enum/spec/mixed-enum.out.ts @@ -0,0 +1,22 @@ +const MixedEnum = { + First: 1, + Second: "second", + Third: 3, + Fourth: "fourth", +} as const; +type MixedEnumType = typeof MixedEnum[keyof typeof MixedEnum]; + +function handleMixed(value: MixedEnumType): string { + switch (value) { + case MixedEnum.First: + return "Number one"; + case MixedEnum.Second: + return "String second"; + case MixedEnum.Third: + return "Number three"; + case MixedEnum.Fourth: + return "String fourth"; + default: + return "Unknown"; + } +} diff --git a/src/enum/spec/multiple-enums.in.ts b/src/enum/spec/multiple-enums.in.ts new file mode 100644 index 0000000..bce66e1 --- /dev/null +++ b/src/enum/spec/multiple-enums.in.ts @@ -0,0 +1,41 @@ +enum Color { + Red = "red", + Blue = "blue", +} + +enum Status { + Active = 1, + Inactive = 0, +} + +enum Priority { + Low, + Medium, + High, +} + +interface Task { + color: Color; + status: Status; + priority: Priority; +} + +function createTask(): Task { + return { + color: Color.Red, + status: Status.Active, + priority: Priority.High, + }; +} + +class TaskManager { + private tasks: Task[] = []; + + addTask(color: Color = Color.Blue): void { + this.tasks.push({ + color, + status: Status.Inactive, + priority: Priority.Medium, + }); + } +} diff --git a/src/enum/spec/multiple-enums.out.ts b/src/enum/spec/multiple-enums.out.ts new file mode 100644 index 0000000..70e494d --- /dev/null +++ b/src/enum/spec/multiple-enums.out.ts @@ -0,0 +1,44 @@ +const Color = { + Red: "red", + Blue: "blue", +} as const; +type ColorType = typeof Color[keyof typeof Color]; + +const Status = { + Active: 1, + Inactive: 0, +} as const; +type StatusType = typeof Status[keyof typeof Status]; + +const Priority = { + Low: "Low", + Medium: "Medium", + High: "High", +} as const; +type PriorityType = typeof Priority[keyof typeof Priority]; + +interface Task { + color: ColorType; + status: StatusType; + priority: PriorityType; +} + +function createTask(): Task { + return { + color: Color.Red, + status: Status.Active, + priority: Priority.High, + }; +} + +class TaskManager { + private tasks: Task[] = []; + + addTask(color: ColorType = Color.Blue): void { + this.tasks.push({ + color, + status: Status.Inactive, + priority: Priority.Medium, + }); + } +} diff --git a/src/enum/spec/numeric-enum.in.ts b/src/enum/spec/numeric-enum.in.ts new file mode 100644 index 0000000..722aace --- /dev/null +++ b/src/enum/spec/numeric-enum.in.ts @@ -0,0 +1,17 @@ +enum Status { + Pending = 0, + Active = 1, + Inactive = 2, +} + +interface User { + name: string; + status: Status; +} + +function createUser(name: string): User { + return { + name, + status: Status.Pending, + }; +} diff --git a/src/enum/spec/numeric-enum.out.ts b/src/enum/spec/numeric-enum.out.ts new file mode 100644 index 0000000..5cb04c9 --- /dev/null +++ b/src/enum/spec/numeric-enum.out.ts @@ -0,0 +1,18 @@ +const Status = { + Pending: 0, + Active: 1, + Inactive: 2, +} as const; +type StatusType = typeof Status[keyof typeof Status]; + +interface User { + name: string; + status: StatusType; +} + +function createUser(name: string): User { + return { + name, + status: Status.Pending, + }; +} diff --git a/src/enum/spec/string-enum.in.ts b/src/enum/spec/string-enum.in.ts new file mode 100644 index 0000000..6ee0934 --- /dev/null +++ b/src/enum/spec/string-enum.in.ts @@ -0,0 +1,11 @@ +enum Color { + Red = "red", + Blue = "blue", + Green = "green", +} + +function getColorName(color: Color): string { + return color === Color.Red ? "Red color" : "Other color"; +} + +const userColor: Color = Color.Blue; diff --git a/src/enum/spec/string-enum.out.ts b/src/enum/spec/string-enum.out.ts new file mode 100644 index 0000000..b5e2b87 --- /dev/null +++ b/src/enum/spec/string-enum.out.ts @@ -0,0 +1,12 @@ +const Color = { + Red: "red", + Blue: "blue", + Green: "green", +} as const; +type ColorType = typeof Color[keyof typeof Color]; + +function getColorName(color: ColorType): string { + return color === Color.Red ? "Red color" : "Other color"; +} + +const userColor: ColorType = Color.Blue; diff --git a/src/parameter-properties/parameter-properties.test.ts b/src/parameter-properties/parameter-properties.test.ts index 3b7b24d..c777025 100644 --- a/src/parameter-properties/parameter-properties.test.ts +++ b/src/parameter-properties/parameter-properties.test.ts @@ -1,335 +1,100 @@ +import { executeTest } from "../utils/test.ts"; import { parameterPropertiesCodemod } from "./parameter-properties.ts"; -import { spawn } from "node:child_process"; -import { assertEquals } from "jsr:@std/assert"; -/** - * Checks a TypeScript file using tsc. - * @example - * await checkTsFile("src/index.ts"); - */ -async function checkTsFile(filePath: string): Promise { - await new Promise((resolve, reject) => { - const tsc = spawn("deno", ["check", filePath]); - - tsc.stderr.on("data", (data) => { - console.error(data.toString()); - }); - - tsc.on("close", (code: number) => { - if (code === 0) { - console.log("Typecheck succeeded 🎉"); - resolve(undefined); - } else { - reject(new Error("Typecheck failed")); - } - }); - }); -} - -async function denoFmt(filePath: string): Promise { - await new Promise((resolve, reject) => { - const tsc = spawn("deno", ["fmt", filePath]); - - tsc.stderr.on("data", (data) => { - console.error(data.toString()); - }); - - tsc.on("close", (code: number) => { - if (code === 0) { - console.log("Typecheck succeeded 🎉"); - resolve(undefined); - } else { - reject(new Error("Typecheck failed")); - } - }); - }); -} - -/** - * Test utilities for codemod testing with proper cleanup - * @example - * const utils = new TestUtils(); - * await utils.createTestFile("test.ts", "class Test { constructor(public name: string) {} }"); - */ -class TestUtils { - private tempFiles = new Set(); - - async createTestFile(filePath: string, content: string): Promise { - await Deno.writeTextFile(filePath, content); - this.tempFiles.add(filePath); - } - - async readTestFile(filePath: string): Promise { - return await Deno.readTextFile(filePath); - } - - async runAndReadCodemod(filePath: string): Promise { - parameterPropertiesCodemod(filePath); - await denoFmt(filePath); - return await this.readTestFile(filePath); - } - - async cleanup(): Promise { - if (Deno.env.get("DEBUG")) return; - for (const file of this.tempFiles) { - try { - await Deno.remove(file); - } catch { - // Ignore if file doesn't exist - } - } - this.tempFiles.clear(); - } -} +const testFn = executeTest(parameterPropertiesCodemod); Deno.test("converts public parameter properties", async () => { - const utils = new TestUtils(); - const testFile = "tmp/public_params_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/public-params.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/public-params.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/public_params_test.ts", + inFile: "src/parameter-properties/spec/public-params.in.ts", + outFile: "src/parameter-properties/spec/public-params.out.ts", + }); }); Deno.test("converts private parameter properties", async () => { - const utils = new TestUtils(); - const testFile = "tmp/private_params_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/private-params.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/private-params.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/private_params_test.ts", + inFile: "src/parameter-properties/spec/private-params.in.ts", + outFile: "src/parameter-properties/spec/private-params.out.ts", + }); }); Deno.test("converts readonly parameter properties", async () => { - const utils = new TestUtils(); - const testFile = "tmp/readonly_params_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/readonly-params.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/readonly-params.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/readonly_params_test.ts", + inFile: "src/parameter-properties/spec/readonly-params.in.ts", + outFile: "src/parameter-properties/spec/readonly-params.out.ts", + }); }); Deno.test("converts mixed access modifiers", async () => { - const utils = new TestUtils(); - const testFile = "tmp/mixed_modifiers_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/mixed-modifiers.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/mixed-modifiers.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/mixed_modifiers_test.ts", + inFile: "src/parameter-properties/spec/mixed-modifiers.in.ts", + outFile: "src/parameter-properties/spec/mixed-modifiers.out.ts", + }); }); Deno.test("handles parameter properties with initializers", async () => { - const utils = new TestUtils(); - const testFile = "tmp/initializers_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/initializers.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/initializers.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/initializers_test.ts", + inFile: "src/parameter-properties/spec/initializers.in.ts", + outFile: "src/parameter-properties/spec/initializers.out.ts", + }); }); Deno.test("handles complex types in parameter properties", async () => { - const utils = new TestUtils(); - const testFile = "tmp/complex_types_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/complex-types.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/complex-types.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/complex_types_test.ts", + inFile: "src/parameter-properties/spec/complex-types.in.ts", + outFile: "src/parameter-properties/spec/complex-types.out.ts", + }); }); Deno.test("handles multiple classes in single file", async () => { - const utils = new TestUtils(); - const testFile = "tmp/multiple_classes_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/multiple-classes.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/multiple-classes.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/multiple_classes_test.ts", + inFile: "src/parameter-properties/spec/multiple-classes.in.ts", + outFile: "src/parameter-properties/spec/multiple-classes.out.ts", + }); }); Deno.test("preserves existing class properties", async () => { - const utils = new TestUtils(); - const testFile = "tmp/existing_properties_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/existing-properties.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/existing-properties.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/existing_properties_test.ts", + inFile: "src/parameter-properties/spec/existing-properties.in.ts", + outFile: "src/parameter-properties/spec/existing-properties.out.ts", + }); }); Deno.test("handles classes without parameter properties", async () => { - const utils = new TestUtils(); - const testFile = "tmp/no_params_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/no-params.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/no-params.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/no_params_test.ts", + inFile: "src/parameter-properties/spec/no-params.in.ts", + outFile: "src/parameter-properties/spec/no-params.out.ts", + }); }); Deno.test("preserves comments", async () => { - const utils = new TestUtils(); - const testFile = "tmp/comments_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/comments.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/comments.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/comments_test.ts", + inFile: "src/parameter-properties/spec/comments.in.ts", + outFile: "src/parameter-properties/spec/comments.out.ts", + }); }); Deno.test("handles generic classes", async () => { - const utils = new TestUtils(); - const testFile = "tmp/generic_classes_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/generic-classes.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/generic-classes.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/generic_classes_test.ts", + inFile: "src/parameter-properties/spec/generic-classes.in.ts", + outFile: "src/parameter-properties/spec/generic-classes.out.ts", + }); }); Deno.test("handles abstract classes", async () => { - const utils = new TestUtils(); - const testFile = "tmp/abstract_classes_test.ts"; - - try { - const originalContent = await Deno.readTextFile( - "src/parameter-properties/spec/abstract-classes.in.ts", - ); - const expectedOutput = await Deno.readTextFile( - "src/parameter-properties/spec/abstract-classes.out.ts", - ); - - await utils.createTestFile(testFile, originalContent); - const result = await utils.runAndReadCodemod(testFile); - await checkTsFile(testFile); - assertEquals(result, expectedOutput); - } finally { - await utils.cleanup(); - } + await testFn({ + testFile: "tmp/abstract_classes_test.ts", + inFile: "src/parameter-properties/spec/abstract-classes.in.ts", + outFile: "src/parameter-properties/spec/abstract-classes.out.ts", + }); }); diff --git a/src/utils/test.ts b/src/utils/test.ts new file mode 100644 index 0000000..b1a0695 --- /dev/null +++ b/src/utils/test.ts @@ -0,0 +1,113 @@ +import { assertEquals } from "@std/assert/equals"; +import { spawn } from "node:child_process"; + +/** + * Checks a TypeScript file using tsc. + * @example + * await checkTsFile("src/index.ts"); + */ +async function checkTsFile(filePath: string): Promise { + await new Promise((resolve, reject) => { + const tsc = spawn("deno", ["check", filePath]); + + tsc.stderr.on("data", (data) => { + console.error(data.toString()); + }); + + tsc.on("close", (code: number) => { + if (code === 0) { + console.log("Typecheck succeeded 🎉"); + resolve(undefined); + } else { + reject(new Error("Typecheck failed")); + } + }); + }); +} + +async function denoFmt(filePath: string): Promise { + await new Promise((resolve, reject) => { + const tsc = spawn("deno", ["fmt", filePath]); + + tsc.stderr.on("data", (data) => { + console.error(data.toString()); + }); + + tsc.on("close", (code: number) => { + if (code === 0) { + console.log("Typecheck succeeded 🎉"); + resolve(undefined); + } else { + reject(new Error("Typecheck failed")); + } + }); + }); +} + +/** + * Test utilities for codemod testing with proper cleanup + * @example + * const utils = new TestUtils(); + * await utils.createTestFile("test.ts", "class Test { constructor(public name: string) {} }"); + */ +class TestUtils { + private tempFiles = new Set(); + private codemod: (filePath: string) => void; + + constructor(codemod: (filePath: string) => void) { + this.codemod = codemod; + } + + async createTestFile(filePath: string, content: string): Promise { + await Deno.writeTextFile(filePath, content); + this.tempFiles.add(filePath); + } + + async runAndReadCodemod(filePath: string): Promise { + this.codemod(filePath); + await denoFmt(filePath); + return Deno.readTextFile(filePath); + } + + async cleanup(): Promise { + if (Deno.env.get("DEBUG")) return; + for (const file of this.tempFiles) { + try { + await Deno.remove(file); + } catch { + // Ignore if file doesn't exist + } + } + this.tempFiles.clear(); + } +} + +export const executeTest = (codemod: (filePath: string) => void) => { + const utils = new TestUtils(codemod); + + return async ( + config: { + testFile: string; + inFile: string; + outFile: string; + }, + ) => { + const { testFile, inFile, outFile } = config; + + try { + const originalContent = await Deno.readTextFile( + inFile, + ); + const expectedOutput = await Deno.readTextFile( + outFile, + ); + + await utils.createTestFile(testFile, originalContent); + const result = await utils.runAndReadCodemod(testFile); + await checkTsFile(testFile); + assertEquals(result, expectedOutput); + } finally { + await utils.cleanup(); + } + }; +};