GitHub上5k+ star!用ast-grep解决代码重构难题

铲屎官ast-grep

ast-grep 简称sg,是一个基于抽象语法树(AST)的代码搜索、代码check和代码重构工具。让你可以在YAML文件里面像写jQuery选择器一样写规则,进行代码的check和重构。

sg提供了cli和vscode插件两种方式使用

sg在官网上提供playground,你可以在playground上很方便地演示、测试和分享你的demo

AST相较于正则表达式

正则表达式只能针对字符串进行模式匹配,而AST提供了对代码的结构化展示,可以更准确地理解、处理代码的各个部分,AST提供了一种更符合代码语法的方式对代码进行操作,在操作代码上AST也比正则表达式更加安全可靠。

看下面的例子,当我们想把console.error改成console.log时,正则表达式和AST都可以实现,但是正则表达式会把注释里的console.error也替换掉,我们并不需要去修改注释的内容,而AST则只对代码处理,不处理注释的内容。从这个例子可以看出,AST和正则表达式的一大区别。

js 复制代码
// console.error(123)
console.error(456)

第二个例子,当我们想重写const { b, c } = a时,正则表达式还要处理换行空白符的场景,而AST是具有代码的语义信息的,const { b, c } = a是否换行,对AST的解析结果是没有影响的。

js 复制代码
// 一般写法
const { b, c } = a

// 有的人喜欢这么写,换行了
const {
  b,
  c,
} = a

在简单场景下,正则表达式是一个快速方便的工具,而AST需要一定的学习成本,编写、运用AST来重构代码需要适配各种场景case,写起来挺繁琐的,代码可读性会比较差。比方说,你想匹配commonjs的require语句,就会遇到各种场景case:

  • node.type === 'CallExpression' && node.callee.name === 'require'
    • node.arguments[0].type === 'TemplateLiteral'
      • node.arguments[0].quasis[0].value.raw.indexOf('@/')
      • node.arguments[0].quasis[0].value.raw.indexOf('.')
    • node.arguments[0].type === 'BinaryExpression'
    • node.arguments[0]..type === 'StringLiteral'
      • node.arguments[0].quasis[0].value.raw.indexOf('@/')
      • node.arguments[0].quasis[0].value.raw.indexOf('.')
    • ...

上面的场景,那么多if else,代码写起来又多又乱的。ast-grep通过配置化的方式,让你可以低代码操作AST,而且是类似jQuery写css选择器那样子去写你的配置

一个重写console.error的例子,很简单,几行配置就搞定了,而且配置的意思也很容易理解

yaml 复制代码
id: fix-console
language: JavaScript
rule:
  pattern: console.error($A)
fix:
  console.log($A)

项目场景实际应用

背景

我们有一个nodejs项目,项目里引用模块是通过类似require(ROOT+'/controllers/hci.js')这种方式引用的,ROOT是在启动文件里通过global.ROOT = process.cwd()注入全局的。这种方案有几个缺点:

第一个缺点是,ROOT是代码运行时注入的,无法识别出来引用的模块路径,模块导出的代码缺失了代码提示功能,开发体验不好,影响开发效率。

第二个缺点是,由于ROOT是一个全局变量,代码写起来就五花八门:

js 复制代码
require(ROOT+'/controllers/vt.js')

require(ROOT+"/controllers/vt.js") // 用的双引号

require(ROOT+ '/controllers/vt.js') // 多了空格

require(ROOT +
'/controllers/vt.js') // 换行了

require(`${ROOT}/controllers/vt.js`)

require(ROOT + `/controllers/vt.js`) // 用的模板字符串相加

第三个缺点是,当我们需要在项目配置其它的路径别名时,都需要像ROOT一样写入global.ROOT = process.cwd(),这种方案的可扩展性是不好的。

后来,我们就通过tsconfig-paths这个npm包做了方案改进,tsconfig-paths的能力就是通过配置tsconfig.json或者jsconfig.json文件中的路径映射,来实现自定义的路径解析。在项目里的jsconfig.json编写配置:

js 复制代码
{
  "compilerOptions": { 
    "baseUrl": ".",
    "paths": {
        "@/*": ["src/*"]
    } 
  }
}

然后在项目启动入口添加require('tsconfig-paths/register')即可,这样就会在运行时动态重定向路径的导入。从jsconfig.json读取,可以获得代码提示能力。

但是新的难题又来了,项目里有那么多的require(ROOT+'/xx')代码,如何重构掉呢?不用担心,ast-grep一扫解君愁

ast-grep一扫解君愁

初始化

先安装好ast-grep,安装方式参考官方文档,命令行通过sg --version可以验证是否安装成功

然后可以编写sg的配置了,在编写配置的时候,我们先梳理出存在哪些场景case,配置是要覆盖到所有场景case的。

场景case梳理

梳理出来的场景case如下:

javascript 复制代码
require(ROOT+'/controllers/vt.js')

require(ROOT+"/controllers/vt.js") // 用的双引号

require(ROOT+ '/controllers/vt.js') // 多了空格

require(ROOT +
'/controllers/vt.js') // 换行了

require(`${ROOT}/controllers/vt.js`)

require(ROOT + `/controllers/vt.js`) // 用的模板字符串相加

playground简单验证

然后就可以在sg官网提供的playground上进行简单编写和验证你的配置

playground 链接

项目里扫描check

要注意的是,如果是模板字符串需要保留为模板字符串,不能简单替换为单引号,因为它们的语义不是对等的,比如把require(${ROOT}/${lang}/tool.js)替换为require('@/${lang}/tool.js')就获取不到lang,是个bug了。

接下来就可以在你的项目里创建sgconfig.ymlrules/a.ymlrules/b.ymlrules/c.yml

yaml 复制代码
# sgconfig.yml
ruleDirs:
 - rules/a.yml
 - rules/b.yml
 - rules/c.yml
yaml 复制代码
id: a
language: JavaScript
rule:
  pattern: require($A)
constraints:
  A:
    regex: '^`\$\{ROOT\}.+`$' # 对A做了限制,必须是模板字符串,且前面是ROOT
transform:
  # B是通过对A进行字符串裁切获得的
  B:
    substring:
      source: $A
      startChar: 8
      endChar: -1
fix:
  require(`@$B`)
yaml 复制代码
id: b
language: JavaScript
rule:
  pattern: require($A + $B)
constraints:
  A:
    regex: '^ROOT$' # A必须是ROOT
  B:
    regex: ^['"].+['"]$ # B必须是单引号或双引号包裹的字符串
transform:
  C:
    substring:
      source: $B
      startChar: 1
      endChar: -1
fix:
  require('@$C') # 双引号也会被重构成单引号
yaml 复制代码
id: c
language: JavaScript
rule:
  pattern: require($A + $B)
constraints:
  A:
    regex: '^ROOT$'
  B:
    regex: '^`.+`$' # B是模板字符串
transform:
  C:
    substring:
      source: $B
      startChar: 1
      endChar: -1
fix:
  require(`@$C`) # 模板字符串得保留

配置写好之后,就执行sg scan看下效果

sg scan并不会更改你的代码,你可以从cli的输出中查看更改后的效果,检查一下是否有问题,没有问题的话,就可以执行sg scan -U进行代码更改了。

执行更改

从git上看,是重构了1353个文件

我们来看一下耗时,

  • real time:这是指从命令开始执行到结束实际花费的时间。这包括了在命令执行期间所经历的所有延迟和等待时间。
  • user time:这是指在 CPU 上运行用户态程序所花费的时间。换句话说,它是您的程序真正执行所花费的时间。如果一个程序启动了另一个程序,那么被启动的程序所花费的 CPU 时间也会被包括在内。
  • sys time:这是指在 CPU 上运行内核程序(内核态)所花费的时间。通常,这代表了程序调用操作系统内核所花费的时间

可以看到,扫描几十万行代码的项目,更改1300+文件只需要不到15秒的时间,又快又准!

提交合并请求

1300+文件的改动太大了,我们需要分目录进行提交,相信有一些人或者身边的人遇到过本地git、vscode崩溃挂掉导致辛辛苦苦写的代码丢失。分目录提交的好处有:

  • 我们分多次提交,避免在单次提交里出现大量文件,本地git、vscode处理不过来挂掉
  • 多次提交可以减少在单次提交里出现大量文件的代码冲突
  • 虽然代码重构好了,但保险起见还是要人工review,分目录多次提交,可以按目录来分工,让团队成员进行Code Review

相关链接

相关推荐
m0_748255262 小时前
前端安全——敏感信息泄露
前端·安全
鑫~阳4 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin4 小时前
CSS|14 z-index
前端·css
2401_882727576 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder6 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂6 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand6 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL6 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿6 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫7 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js