logo

鱼肚的博客

Don't Repeat Yourself

使用ts-morph批量修改代码

重构代码时的痛点

在重构项目、推行代码规范等过程中,经常会出现批量修改代码的需求,将代码从一种形式转换成另一种形式。

举例来说:

  • 删除某个全局变量,改为在相应代码文件中按需引入
  • 如果一个函数末尾没有返回值,为其添加默认的返回值undefined
  • if (a > 1 || b < 2 && c === 3)修改为if (a > 1 || (b < 2 && c === 3))

这种任务虽然描述起来很简单,但是较难使用正则表达式进行批量替换,或替换时容易出错。

而如果手动修改,则会比较枯燥,且浪费大量的时间。甚至在疲劳状态下还容易出一些错。

ESLint Fix

ESLint 的 --fix 功能是一个很棒的功能,它能够自动改正很多不合理的写法,比如代码格式化和一些修复方式比较明显的问题。

但是它的覆盖范围还不够全。

关于eslint fix未覆盖到的方面,主要有如下的两种原因:

  1. 修复规则不明确,或未实现。如删除全局变量后应该从哪个文件中引入,是不确定的。eslint的配置中一般也不会写太复杂的规则。
  2. 修复有安全隐患。如将 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修复下缩进问题即可。

以上。