使用ts-morph批量修改代码

发布于: 2020-09-15作者: 鱼肚最后更新: 2020-09-16

使用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』后会出现的问题,即某个函数中不是所有路径都有返回值。

为方便理解,假设有错误代码如下:

1
2
3
4
5
function foo () {
  if (Math.random() > 0.5) {
    return true
  }
}

它实际上等价于

1
2
3
4
5
6
function foo () {
  if (Math.random() > 0.5) {
    return true
  }
  return undefined // 无返回值即为隐式返回 undefined
}

这个问题类似于no-mixed-operators,直接修复是有安全隐患的,所以eslint官方没有提供修复选项。

现在我们确认当前所有逻辑正常,没有bug,决定要在所有存在隐式返回undefined的函数末尾加上一行

1
return undefined // auto-fixed-by-no-implicit-returns

那么就可以写这样一段ts代码来完成任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/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修复下缩进问题即可。

以上。

关注我:
分享文章:

0条评论