05月30, 2018

ast基础入门

说到ast,我相信大部分的人都不会陌生。它是Abstract Syntax Tree(抽象语法树)的简写,像webpackLint等很多的工具和库的核心都是通过它实现对代码的检查、分析等操作的。

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。

不同的parser解析js代码后得到的AST

alt

常用的JavaScript Parser

  • esprima
  • traceur
  • acorn
  • shift

一般用的比较多的,是esprima和acorn,像webpack用的是后者:

alt

babel用的是babylon(Acorn 的一份 fork):

alt

应用

我们平常在工作中,使用最多的可能就是用babel写一个插件啥的。

写babel插件,都有一个统一的格式:

//babel核心库,用来实现核心的转换引擎 
let babel = require('babel-core');
//可以实现类型判断,生成AST节点
let types = require('babel-types');
let code = `你需要处理的代码`; 
// 这个访问者可以对特定的类型的节点进行处理
let visitor = {
    /** your code here  **/
}

let yourCustomPlugin = { visitor }
//babel内部先先把代码转成AST,然后进行遍历,
let result = babel.transform(code, {
    plugins: [
        yourCustomPlugin
    ]
})
console.log(result.code);

我们只要实现visitor即可。

为一个function加一个注释

比如我们有一个code:

let code = `function sum() { var a = 1; var b = 2; }`;

alt

我们需要改这个(在它前面加注释),所以代码如下:

let visitor = {
    FunctionDeclaration(path) {
        path.insertBefore(types.identifier("/** 佛祖保佑 **/"))
    }
}

效果如下:

alt

这个代码参考是在这里:插入同级节点

函数内部第一行插入一段console

这个基本上也是读了上面的文档,能轻易写出来的。(然而我写了一下午,一开始有些蒙B)

函数内部是这个节点:

alt

let visitor = {
     BlockStatement(path) {
        path.node.body.unshift(types.identifier("console.log(123);"));
    }
}

我开始想通过t.blockStatement,然后path.placeWith。这个思路是没错的,但是它会死循环,就是这个BlockStatement方法会一直调用,除非外部做一个变量开关来控制。

转换箭头函数

比如我们的code是这样的:

let sum = (a,b)=>a+b

alt

只要替换这个节点即可。

let visitor = {
     ArrowFunctionExpression(path){
        let node = path.node;
        let expression = node.body;
        let params = node.params;
        let returnStatement = types.returnStatement(expression);
        let block = types.blockStatement([
            returnStatement
        ]);
        let func = types.functionExpression(null,params, block,false, false);
        path.replaceWith(func);
    }
}

当然这个考虑的比较简单,像这样的代码:

let sum = (a,b)=> { console.log(123);let c = 2; }

alt

但其实处理起来也容易,只要获取node.body.body,然后解构放到types.blockStatement方法里面即可。

难的是如果箭头函数里面有this,需要对this进行上下文处理。

我翻了一下babel-plugin-transform-es2015-arrow-functions的源码,它的代码是这样的:

alt

默认不传参的情况下,是走下面一个分支。path.arrowFunctionToShadowed()这个,我在插件开发中死活找不到,orz。。。

预计算处理

比如说我们的code:

const result = 1 + 2;

想在编译阶段就直接转成const result = 3

同理,先找到要处理的节点:

alt

let visitor = {
     BinaryExpression(path){
        let node = path.node;
        if(!isNaN(node.left.value) && !isNaN(node.right.value)){
            let result = eval(node.left.value + node.operator + node.right.value);
            path.replaceWith(types.numericLiteral(result));
        }
    }
}

然而上面只是处理了左右两个,当code变成:

const result = 1 + 2 + 3

上面的代码执行的结果就会变成:

alt

看结构图,会发现是这样的:

alt

因此我们需要在上面的代码if判断里面,再加入一个判断(重新执行一下该函数):

if(path.parentPath&& path.parentPath.node.type == 'BinaryExpression'){
    visitor.BinaryExpression.call(null,path.parentPath);
}

按需加载

比如我们要将:

import { flatten,concat } from "lodash"

转成:

import flatten from "lodash/flatten";
import concat from "lodash/flatten";

先找结构和对比:

alt

alt

所以我们只要去修改ImportDeclaration这个节点,然后将一条改为多条。

在这个插件中,需要我们从外部传入库名(如lodash,因为我们不能盲目地去对比上面的source名称)

//babel核心库,用来实现核心的转换引擎 
let babel = require('babel-core');
//可以实现类型判断,生成AST节点
let types = require('babel-types');
let code = `你需要处理的代码`; 
// 这个访问者可以对特定的类型的节点进行处理
let visitor = {
    ImportDeclaration(path, ref = { opts: {} }) {
        const specifiers = path.node.specifiers;
        const source = path.node.source;
        if (ref.opts.library == source.value && !types.isImportDefaultSpecifier(specifiers[0])) {
            const declarations = specifiers.map((specifier, index) => {
                return types.ImportDeclaration(
                    [types.importDefaultSpecifier(specifier.local)],
                    types.stringLiteral(`${source.value}/${specifier.local.name}`)
                )
            });
            path.replaceWithMultiple(declarations);
        }
    }
}

let yourCustomPlugin = { visitor }
//babel内部先先把代码转成AST,然后进行遍历,
let result = babel.transform(code, {
    plugins: [
        [yourCustomPlugin, {"library":"lodash"}]
    ]
})
console.log(result.code);

babel配置说明

简单demo:

{
    "presets":["env","stage-0","react"],
    "plugins":[
        ["extract", {"library":"lodash"}],
        ["transform-runtime", {}]
    ]
}

编译顺序为首先plugins从左往右,然后presets从右往左

相关资料

本文链接:www.my-fe.pub/post/ast-basic-getting-started.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。