使用ts-morph批量修改代码
重构代码时的痛点
在重构项目、推行代码规范等过程中,经常会出现批量修改代码的需求,将代码从一种形式转换成另一种形式。
举例来说:
- 删除某个全局变量,改为在相应代码文件中按需引入
- 如果一个函数末尾没有返回值,为其添加默认的返回值undefined
- 将
if (a > 1 || b < 2 && c === 3)
修改为if (a > 1 || (b < 2 && c === 3))
这种任务虽然描述起来很简单,但是较难使用正则表达式进行批量替换,或替换时容易出错。
而如果手动修改,则会比较枯燥,且浪费大量的时间。甚至在疲劳状态下还容易出一些错。
ESLint Fix
ESLint 的 --fix 功能是一个很棒的功能,它能够自动改正很多不合理的写法,比如代码格式化和一些修复方式比较明显的问题。
但是它的覆盖范围还不够全。
关于eslint fix未覆盖到的方面,主要有如下的两种原因:
- 修复规则不明确,或未实现。如删除全局变量后应该从哪个文件中引入,是不确定的。eslint的配置中一般也不会写太复杂的规则。
- 修复有安全隐患。如将
if (a > 1 || b < 2 && c === 3)
修改为`if (a > 1 || (b < 2 && c === 3))这个过程,是不太安全的。虽然转换保留了原语义,但是考虑到原代码可能是有bug的,本来通过eslint还能够找到可疑的代码,转换后就丧失这个能力了。
关于有安全隐患的修复方式,有一部分人也会选择自己实现一个『不安全』的eslint plugin。比如上面说的no-mixed-operators
,官方选择了不自动修复,有另一个插件支持自动修复:https://github.com/kevin940726/eslint-plugin-no-mixed-operators。
如果对eslint比较熟悉,可以使用eslint plugin完成自己的自定义需求。
如果不想依赖于eslint,则可以考虑使用ts-morph自己实现一个脚本处理复杂任务。
使用ts-morph完成复杂的需求
ts-morph是一个针对 Typescrpit/Javascript的AST处理库,可用于浏览、修改TS/JS的AST。
关于ts-morph的详细文档,参见其官网:https://ts-morph.com/。
下面用一个实际的例子,说明ts-morph的用法:
这个例子中的脚本,会遍历所有函数,解决typescript中启用『noImplicitReturns』后会出现的问题,即某个函数中不是所有路径都有返回值。
为方便理解,假设有错误代码如下:
1function foo () { 2 if (Math.random() > 0.5) { 3 return true 4 } 5}
它实际上等价于
1function foo () { 2 if (Math.random() > 0.5) { 3 return true 4 } 5 return undefined // 无返回值即为隐式返回 undefined 6}
这个问题类似于 no-mixed-operators
,直接修复是有安全隐患的,所以eslint官方没有提供修复选项。
现在我们确认当前所有逻辑正常,没有bug,决定要在所有存在隐式返回undefined的函数末尾加上一行
1return undefined // auto-fixed-by-no-implicit-returns
那么就可以写这样一段ts代码来完成任务:
1#!/usr/bin/env ts-node 2 3// fix-no-implicit-returns.ts 4import { getProject } from '../utils' 5import { Project, ArrowFunction, FunctionDeclaration, FunctionExpression, MethodDeclaration } from 'ts-morph' 6const ERR_CODE = 7030 7 8; 9(async () => { 10 const project = new Project({ 11 tsConfigFilePath: '<替换成tsconfig.json文件路径>' 12 }) 13 const oldCompilerOptions = project.getCompilerOptions() 14 project.compilerOptions.set({ 15 ...oldCompilerOptions, 16 noImplicitReturns: true // 在内存中打开 noImplicitReturns 选项,使得所有 no-implicit-returns 的情况都会有一个TS错误,错误代号为 TS7030 17 }) 18 19 // 找到所有TS7030的错误 20 const diagnostics = project.getPreEmitDiagnostics().filter(item => item.getCode() === ERR_CODE) 21 22 // 先遍历,再修改。遍历时将待修改项缓存进toModify 23 const toModify: Array<FunctionExpression | MethodDeclaration | ArrowFunction | FunctionDeclaration> = [] 24 for (const item of diagnostics) { 25 const file = item.getSourceFile() 26 if (file !== undefined) { 27 const start = item.getStart() ?? 0 28 const length = item.getLength() ?? 0 29 if (start > 0 && length > 0) { 30 // 先根据 start 位置定位到具体的Node,再获取Parent得到所属 Function 的Node 31 const node = file.getDescendantAtPos(start)?.getParent() 32 if ( 33 (node instanceof FunctionExpression) || 34 (node instanceof MethodDeclaration) || 35 (node instanceof ArrowFunction) || 36 (node instanceof FunctionDeclaration)) { 37 toModify.push(node) 38 } 39 } 40 } 41 } 42 43 // 调用 addStatements 在函数的末尾添加一行 44 toModify.forEach(node => node.addStatements('return undefined // auto-fixed-by-no-implicit-returns')) 45 46 await project.save() 47})().catch(console.error) 48
使用ts-morph处理过后,no-implicit-returns 即 TS7030 的问题就都修复了。
因为addStatements
的时候不容易很好地兼容代码格式,可能会有一些缩进问题,在运行完修复脚本之后,再使用eslint fix修复下缩进问题即可。
以上。