diff --git a/.changeset/spotty-snails-explain.md b/.changeset/spotty-snails-explain.md new file mode 100644 index 0000000..045cb65 --- /dev/null +++ b/.changeset/spotty-snails-explain.md @@ -0,0 +1,5 @@ +--- +"@effect/codemod": patch +--- + +Add codemods for swap-type-params in Effect,Exit,STM,Stream,Layer,Schema and add-tag-identifier diff --git a/package.json b/package.json index 4dc709c..fa44594 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "access": "public", "directory": "dist" }, + "packageManager": "pnpm@8.12.1", "description": "Code mod's for the Effect ecosystem", "engines": { "node": ">=16.17.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5397b37..e92f177 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true diff --git a/public/codemods/add-tag-identifier.ts b/public/codemods/add-tag-identifier.ts new file mode 100644 index 0000000..2a6847e --- /dev/null +++ b/public/codemods/add-tag-identifier.ts @@ -0,0 +1,49 @@ +import type cs from "jscodeshift" + +export default function transformer(file: cs.FileInfo, api: cs.API) { + const j = api.jscodeshift + const root = j(file.source) + + root.find(j.VariableDeclaration).forEach(ast => { + writeTagIdentifier(ast, j) + }) + + return root.toSource() +} + +const writeTagIdentifier = ( + ast: cs.ASTPath, + j: cs.API["jscodeshift"], +) => { + if (ast.value.declarations.length === 1) { + const declaration = ast.value.declarations[0] + if ( + declaration.type === "VariableDeclarator" + && declaration.init + && declaration.init.type === "CallExpression" + ) { + const init = declaration.init + const callee = init.callee + const isTag = (node: typeof callee): boolean => { + switch (node.type) { + case "Identifier": { + return node.name === "Tag" + } + case "MemberExpression": { + return isTag(node.property) + } + default: { + return false + } + } + } + if ( + isTag(callee) + && init.arguments.length === 0 + && declaration.id.type === "Identifier" + ) { + init.arguments.push(j.stringLiteral(`@services/${declaration.id.name}`)) + } + } + } +} diff --git a/public/codemods/swap-params.ts b/public/codemods/swap-params.ts deleted file mode 100644 index 863cee5..0000000 --- a/public/codemods/swap-params.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type k from "ast-types/gen/kinds.js" -import type cs from "jscodeshift" - -const enabled = { - swapEitherParams: true, - swapEffectParams: true, - writeTagIdentifier: true, -} - -const swapEitherParams = (ast: cs.ASTPath) => { - const name = ast.value.typeName - const isEither = (node: typeof name): boolean => { - switch (node.type) { - case "Identifier": { - if (node.name === "Either") { - return true - } - return false - } - case "JSXIdentifier": { - return false - } - case "TSQualifiedName": { - return isEither(node.right) - } - case "TSTypeParameter": { - return false - } - } - } - if ( - isEither(name) - && ast.value.typeParameters - && ast.value.typeParameters.params.length === 2 - ) { - const params = ast.value.typeParameters.params - const newParams = [params[1], params[0]] - popNever(newParams) - ast.value.typeParameters.params = newParams - } -} - -const swapEffectParams = (ast: cs.ASTPath) => { - const name = ast.value.typeName - const isEffect = (node: typeof name): boolean => { - switch (node.type) { - case "Identifier": { - return node.name === "Effect" - } - case "JSXIdentifier": { - return false - } - case "TSQualifiedName": { - return isEffect(node.right) - } - case "TSTypeParameter": { - return false - } - } - } - if ( - isEffect(name) - && ast.value.typeParameters - && ast.value.typeParameters.params.length === 3 - ) { - const params = ast.value.typeParameters.params - const newParams = [params[2], params[1], params[0]] - popNever(newParams) - popNever(newParams) - ast.value.typeParameters.params = newParams - } -} - -const popNever = (params: Array) => { - if ( - params.length > 0 && params[params.length - 1].type === "TSNeverKeyword" - ) { - params.pop() - } -} - -const writeTagIdentifier = ( - ast: cs.ASTPath, - j: cs.API["jscodeshift"], -) => { - if (ast.value.declarations.length === 1) { - const declaration = ast.value.declarations[0] - if ( - declaration.type === "VariableDeclarator" && declaration.init - && declaration.init.type === "CallExpression" - ) { - const init = declaration.init - const callee = init.callee - const isTag = (node: typeof callee): boolean => { - switch (node.type) { - case "Identifier": { - return node.name === "Tag" - } - case "MemberExpression": { - return isTag(node.property) - } - default: { - return false - } - } - } - if ( - isTag(callee) && init.arguments.length === 0 - && declaration.id.type === "Identifier" - ) { - init.arguments.push(j.stringLiteral(`@services/${declaration.id.name}`)) - } - } - } -} - -export default function transformer(file: cs.FileInfo, api: cs.API) { - const j = api.jscodeshift - const root = j(file.source) - - const forEveryTypeReference = ( - node: typeof root, - f: (ast: cs.ASTPath) => void, - ) => { - node.find(j.TSTypeReference).forEach(ast => { - f(ast) - }) - node.find(j.CallExpression).forEach(path => { - const typeParams = (path.value as any) - .typeParameters as cs.TSTypeParameterInstantiation - if (typeParams) { - j(typeParams).find(j.TSTypeReference).forEach(tref => { - f(tref) - }) - } - }) - } - - forEveryTypeReference(root, ast => { - if (enabled.swapEffectParams) { - swapEffectParams(ast) - } - if (enabled.swapEitherParams) { - swapEitherParams(ast) - } - }) - - root.find(j.VariableDeclaration).forEach(ast => { - if (enabled.writeTagIdentifier) { - writeTagIdentifier(ast, j) - } - }) - - return root.toSource() -} diff --git a/public/codemods/swap-type-params.ts b/public/codemods/swap-type-params.ts new file mode 100644 index 0000000..62d03f5 --- /dev/null +++ b/public/codemods/swap-type-params.ts @@ -0,0 +1,120 @@ +import type k from "ast-types/gen/kinds.js" +import type cs from "jscodeshift" +import type { Collection } from "jscodeshift/src/Collection" + +export default function transformer(file: cs.FileInfo, api: cs.API) { + const j = api.jscodeshift + const root = j(file.source) + + forEveryTypeReference(root, j, ast => { + swapParams(ast, "Effect", 3) + swapParams(ast, "Stream", 3) + swapParams(ast, "STM", 3) + swapParams(ast, "Layer", 3) + swapParams(ast, "Exit", 2) + swapSchema(ast, j) + }) + + return root.toSource() +} + +// +// utilities +// + +const swapParams = ( + ast: cs.ASTPath, + name: string, + size: number, +) => { + if (hasName(ast, name) && ast.value.typeParameters?.params.length === size) { + const params = ast.value.typeParameters.params + params.reverse() + for (let i = 0; i < size - 1; i++) { + popNever(params) + } + } +} + +const swapSchema = ( + ast: cs.ASTPath, + j: cs.API["jscodeshift"], +) => { + if (hasName(ast, "Schema") && ast.value.typeParameters?.params.length === 3) { + const params = ast.value.typeParameters.params + params.reverse() + popNever(params) + if ( + params.length === 2 + && j(params[0]).toSource() === j(params[1]).toSource() + ) { + params.pop() + } + } +} + +const popNever = (params: Array) => { + if ( + params.length > 0 + && params[params.length - 1].type === "TSNeverKeyword" + ) { + params.pop() + } +} + +const hasName = (reference: cs.ASTPath, name: string) => { + const initial = reference.value.typeName + const loop = (node: typeof initial): boolean => { + switch (node.type) { + case "Identifier": { + return node.name === name + } + case "JSXIdentifier": { + return false + } + case "TSQualifiedName": { + return loop(node.right) + } + case "TSTypeParameter": { + return false + } + } + } + return loop(initial) +} + +// +// this is needed to resolve a bug in jscodeshift that +// forgets to traverse type parameters in call expressions +// + +declare module "ast-types/gen/namedTypes" { + namespace namedTypes { + interface CallExpression extends TSHasOptionalTypeParameterInstantiation {} + } +} + +const forEveryTypeReference = ( + node: Collection, + j: cs.API["jscodeshift"], + f: (ast: cs.ASTPath) => void, +) => { + const visited = new Set() + node.find(j.TSTypeReference).forEach(ast => { + if (!visited.has(ast)) { + visited.add(ast) + f(ast) + } + }) + node.find(j.CallExpression).forEach(path => { + const typeParams = path.value.typeParameters + if (typeParams) { + j(typeParams).find(j.TSTypeReference).forEach(ast => { + if (!visited.has(ast)) { + visited.add(ast) + f(ast) + } + }) + } + }) +} diff --git a/scripts/copy-package-json.ts b/scripts/copy-package-json.ts index 4f17c8a..7bc5a0d 100644 --- a/scripts/copy-package-json.ts +++ b/scripts/copy-package-json.ts @@ -10,7 +10,9 @@ const read = pipe( name: json.name, version: json.version, description: json.description, - bin: "main.js", + bin: { + "effect-codemod": "main.js", + }, engines: json.engines, repository: json.repository, author: json.author, diff --git a/src/main.ts b/src/main.ts index 8beca55..e7c2531 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,7 +22,7 @@ const codemod = Args.choice( Args.withDescription("The code modification to run"), ) -const run = Command.make("codemod", { +const run = Command.make("effect-codemod", { codemod, paths: Args.text({ name: "paths" }).pipe( Args.repeated, diff --git a/tsconfig.json b/tsconfig.json index 87c8c75..597907b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,9 @@ "noEmit": true, "module": "commonjs", "moduleResolution": "node", - "lib": ["es6"], + "lib": [ + "es6" + ], "sourceMap": false, "strict": true, "noImplicitReturns": true, @@ -16,7 +18,13 @@ "forceConsistentCasingInFileNames": true, "stripInternal": true, "skipLibCheck": true, - "types": ["vitest/globals"], + "types": [ + "vitest/globals" + ], }, - "include": ["./src", "./public/codemods", "./test"], + "include": [ + "./src", + "./public/codemods", + "./test" + ], }