在重构项目、推行代码规范等过程中,经常会出现批量修改代码的需求,将代码从一种形式转换成另一种形式。
举例来说:
- 删除某个全局变量,改为在相应代码文件中按需引入
- 如果一个函数末尾没有返回值,为其添加默认的返回值undefined
- 将
if (a > 1 || b < 2 && c === 3)
修改为if (a > 1 || (b < 2 && c === 3))
这种任务虽然描述起来很简单,但是较难使用正则表达式进行批量替换,或替换时容易出错。
而如果手动修改,则会比较枯燥,且浪费大量的时间。甚至在疲劳状态下还容易出一些错。
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是一个针对 Typescrpit/Javascript的AST处理库,可用于浏览、修改TS/JS的AST。
关于ts-morph的详细文档,参见其官网:https://ts-morph.com/。
下面用一个实际的例子,说明ts-morph的用法:
这个例子中的脚本,会遍历所有函数,解决typescript中启用『noImplicitReturns』后会出现的问题,即某个函数中不是所有路径都有返回值。
为方便理解,假设有错误代码如下:
function foo () {
if (Math.random() > 0.5) {
return true
}
}
它实际上等价于
function foo () {
if (Math.random() > 0.5) {
return true
}
return undefined // 无返回值即为隐式返回 undefined
}
这个问题类似于 no-mixed-operators
,直接修复是有安全隐患的,所以eslint官方没有提供修复选项。
现在我们确认当前所有逻辑正常,没有bug,决定要在所有存在隐式返回undefined的函数末尾加上一行
return undefined // auto-fixed-by-no-implicit-returns
那么就可以写这样一段ts代码来完成任务:
#!/usr/bin/env ts-node
// fix-no-implicit-returns.ts
import { getProject } from '../utils'
import { Project, ArrowFunction, FunctionDeclaration, FunctionExpression, MethodDeclaration } from 'ts-morph'
const ERR_CODE = 7030
;
(async () => {
const project = new Project({
tsConfigFilePath: '<替换成tsconfig.json文件路径>'
})
const oldCompilerOptions = project.getCompilerOptions()
project.compilerOptions.set({
...oldCompilerOptions,
noImplicitReturns: true // 在内存中打开 noImplicitReturns 选项,使得所有 no-implicit-returns 的情况都会有一个TS错误,错误代号为 TS7030
})
// 找到所有TS7030的错误
const diagnostics = project.getPreEmitDiagnostics().filter(item => item.getCode() === ERR_CODE)
// 先遍历,再修改。遍历时将待修改项缓存进toModify
const toModify: Array<FunctionExpression | MethodDeclaration | ArrowFunction | FunctionDeclaration> = []
for (const item of diagnostics) {
const file = item.getSourceFile()
if (file !== undefined) {
const start = item.getStart() ?? 0
const length = item.getLength() ?? 0
if (start > 0 && length > 0) {
// 先根据 start 位置定位到具体的Node,再获取Parent得到所属 Function 的Node
const node = file.getDescendantAtPos(start)?.getParent()
if (
(node instanceof FunctionExpression) ||
(node instanceof MethodDeclaration) ||
(node instanceof ArrowFunction) ||
(node instanceof FunctionDeclaration)) {
toModify.push(node)
}
}
}
}
// 调用 addStatements 在函数的末尾添加一行
toModify.forEach(node => node.addStatements('return undefined // auto-fixed-by-no-implicit-returns'))
await project.save()
})().catch(console.error)
使用ts-morph处理过后,no-implicit-returns 即 TS7030 的问题就都修复了。
因为addStatements
的时候不容易很好地兼容代码格式,可能会有一些缩进问题,在运行完修复脚本之后,再使用eslint fix修复下缩进问题即可。
以上。