背景

AST 是非常有用的

上午一直在搬砖,下午听了小组一个老哥做的AST分享,觉得做的很好。

为了加深印象,就写了篇总结,顺便分享给大家,希望能给朋友们一些帮助和启发

可能小伙伴要问,AST真有这么厉害?

口说无凭, 且看几个具体的案例

且不说:

  1. Vue => React
  2. React => Vue

的代码转换方法,

我们就看一个可以无痛升级旧版React的工具:

react-codemod

代码地址:https://github.com/reactjs/react-codemod

这个工具,功能十分强大,使用起来也很方便,只需要运行一行命令:

npx react-codemod <transform> <path> [...options]

这些功能的实现,无不借助了AST

下面我们就进入今天的内容。

正文

本文的主要内容包括:

  • 1 理论: AST 基本概念
  • 2 实践: 使用 AST 实现一个代码转换工具, 把 var转换成let
  • 3 实践: 使用 AST 实现一个Eslint 插件, 禁用 console
  • 4 实践: 使用 AST 实现一个Babel插件, 过滤 Debugger

1. AST 基本概念

AST 是什么?

AST is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.

在计算机科学中,抽象语法抽象语法树其实是源代码的抽象语法结构的树状表现形式

常用的浏览器就是通过将js代码转化为抽象语法树来进行下一步的分析等其他操作。

所以将js转化为抽象语法树更利于程序的分析

AST 能做什么

  • 代码语法的检查
  • 代码风格的检查
  • 代码的格式化
  • 代码的高亮
  • 代码错误提示
  • 代码自动补全
  • 等等。

AST 三板斧

  • 生成AST
  • 遍历和更新AST
  • 将AST重新生成源码

为了便于理解, 我们看一个具体的例子。

这里顺便给大家介绍一个十分有用的网站:

https://astexplorer.net/

譬如:

java AST 语法树 图解_Babel

声明, 变量, 类型等各种信息一应俱全。

而且这里也提供了各种插件模版供你选择:

java AST 语法树 图解_Babel_02

十分的方便。

我们就根据这个例子, 我们做个小的实践

2. 实践: 使用AST实现一个代码转换工具, 把var转换成let

比如, 现在要重构一个老项目, 你要把项目里的var 全部替换成let, 你会怎么做?

手动替换? 或者借助工具一键替换?

现在就教你一招:一键替换大法

首先还是要介绍一把大杀器:

jscodeshift

它是一个 Javscript Codemod 工具,官方对 Codemod 的解释是:

Codemod is a tool/library to assist you with large-scale codebase refactors that can be partially automated but still require human oversight and occasional intervention.

jscodeshift 也是基于 esprima 的,其通过 path 可以很容易的在 AST 上遍历 node

现在我们就开始替换项目中的var.

首先,到 https://astexplorer.net 里面编写代码.

模板我们选:jscodeshift



java AST 语法树 图解_语法树_03

官方自带的例子, 把变量名字反转:

java AST 语法树 图解_Babel_04

我们现在要改变量, 这个工具很贴心的一点是可以高亮实时对照,

java AST 语法树 图解_模版_05

现在, 找到kind === var 的对象, 替换成let:

得到如下代码:

export default function transformer(file, api) {
const j = api.jscodeshift;

return j(file.source)
    .find(j.VariableDeclaration, { kind: 'var'})
    .forEach(path => {
const letStatement = j.variableDeclaration('let', path.node.declarations)
      j(path).replaceWith(letStatement)
    })
    .toSource();
}

这样也可以:

path.node.kind = 'let'; // 传入的实际是一个引用

实际效果:

java AST 语法树 图解_Babel_06

大功告成!

假如我项目里有几个文件也需要相同的操作:

简单安装:

sudo npm install -g jscodeshift

执行:

jscodeshift -t transform.js ./src/demo.js --dry --print



java AST 语法树 图解_java AST 语法树 图解_07

这里用了--dry 和 --print

--dry 加上之后,不会立刻把新生成的代码覆盖源文件

--print 是打印出来看看

在实际项目里, 你需要在独立的分支里操作,新生成代码之后, 需要你再检查检查, review没有问题之后才能合并。

3. 使用AST实现一个Eslint 插件, 禁用console

和上面的类似, 我们也可以做一个eslint 插件, 功能也很简单: 检查到使用console的时候就报错

期望达到的效果:

// Do not use console methods (at 1:9)
console.log('haha')
// --------^

我们这次选择 babel-eslint 模版。

代码实现:

const disallowMethods = ["log", "info", "warn", "error", "dir"];
export default function(context) {
return {
    Identifier(node) {
const isConsoleMethod =
        disallowMethods.includes(node.name) &&
        node.parent.type === "MemberExpression" &&
        node.parent.object.name === "console";

if (!isConsoleMethod) return;

      context.report({
        node,
message: "Do not use console methods"
      });
    }
  };
}

实际效果:

简单有效。

不过你要是非要玩什么骚操作,比如自定义一个log, 那就没得搞了。

最后, 你可以把这段代码封装成一个完整的插件:

教你如何编写 Eslint 插件:

你可以自行实践。

4. 使用AST实现一个Babel插件, 过滤debugger

最后一个是过滤源代码中的debugger, Transform 我们选择babelv7

这个插件,我们期望达到的效果是:

var a = 1
debugger
function test() {
  debugger
   a++
}
debugger

到:

var a = 1;

function test() {
  a++;
}

这也是一个十分有用的功能。

代码实现:

export default function (babel) {const {types: t
    } = babel;return {name: "ast-transform",
        visitor: {
            DebuggerStatement(path) {
                path.remove()
            }
        }
    };
}

实际效果:

java AST 语法树 图解_Babel_08