AST

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-07-03

『转载』

什么是AST?

抽象语法树即:Abstract Syntax Tree。简称AST。webpack和Lint等很多的工具和库的核心都是通过Abstract Syntax Tree抽象语法树这个概念来实现对代码的检查、分析等操作的。通过了解抽象语法树这个概念,你也可以随手编写类似的工具。

AST 的构成

抽象语法树生成的过程为:代码 => 词法分析 => 语法分析 => AST,如下图,我们看看一个简单函数声明的构成

通过 https://astexplorer.net/ 可以在线解析出很多程序语言的 ast。

function square(n) {
  return n * n;
} 

代码解析后的AST 对象如下:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

js ast解析规范

js引擎在执行js文件时,都会先将js代码转换成抽象语法树(AST)。一位Mozilla工程师在FireFox中公开了这个将代码转成AST的解析器Api,也就是Parser_API,后来被人整理到github项目estree,慢慢的成了业界的规范。

下面介绍几个AST里的几个基本元素:

1、Node objects

interface Node {
  type: string;  //type 字段是一个字符串,代表AST变量类型,你可以使用这个字段去决定一个节点要实现的接口.
  loc: SourceLocation | null;  // loc 字段代表节点的源位置信息 
}
// 代码位置信息
interface SourceLocation {
  source: string | null;
  start: Position;
  end: Position;
}
// 位置信息
interface Position {
  line: uint32 >= 1;
  column: uint32 >= 0;
}

2、Programs(程序语句)

//  A complete program source tree.
interface Program <: Node {
  type: "Program";
  body: [ Statement ];
}

3、Identifier(标识符,我们写代码时自定义的名称,如变量名、函数名、属性名)

//  A complete program source tree.
interface Identifier <: Node, Expression, Pattern {
  type: "Identifier";
  name: string;
}

4、Literal(字面量,“hello”、 true、 null、 100、 /\d/)

interface Literal <: Node, Expression {
  type: "Literal";
  value: string | boolean | null | number | RegExp;
}

5、Functions(函数)

interface Function <: Node {
  id: Identifier | null;
  params: [ Pattern ];
  defaults: [ Expression ];
  rest: Identifier | null;
  body: BlockStatement | Expression;
  generator: boolean;
  expression: boolean;
}

6、Statements(语句,子类有很多,空语句,if、switch 语句等)

interface Statement <: Node { }

// 一个空语句,也就是,一个孤立的分号
interface EmptyStatement <: Statement {
  type: "EmptyStatement";
}
// 一个语句块,也就是由大括号包围的语句序列.
interface BlockStatement <: Statement {
  type: "BlockStatement";
  body: [ Statement ];
}
// 表达式语句,也就是,仅有一个表达式组成的语句.
interface ExpressionStatement <: Statement {
  type: "ExpressionStatement";
  expression: Expression;
} 

7、Declarations(声明)

// 声明,子类主要有变量申明、函数声明。
interface Declaration <: Statement { }

8、Expressions(表达式)

// 表达式,子类很多,有二元表达式( n*n)
// 函数表达式(var fun = function(){})、数组表达式(var arr = [])
// 对象表达式(var obj = {})、赋值表达式( a=1)等等

// this 表达式
interface ThisExpression <: Expression {
  type: "ThisExpression";
}
// 数组表达式
interface ArrayExpression <: Expression {
  type: "ArrayExpression";
  elements: [ Expression | null ];
}
//对象表达式
interface ObjectExpression <: Expression {
  type: "ObjectExpression";
  properties: [
    {
      key: Literal | Identifier,
      value: Expression,
      kind: "init" | "get" | "set" 
    }
  ];
}

9、Patterns(模式)

模式,主要在 ES6 的解构赋值中有意义,let {name} = user,其中{name}部分为 ObjectPattern,在 ES5 中,可以理解为和 Identifier 差不多的东西。

解析&简单实践

业界已经有很多成熟的解析器,可以将js代码转换成符合规范的AST:

  • Esprima,比较经典,出现的比较早
  • Acorn,fork自Esprima,代码更精简。webpack使用acorn进行模块解析
  • UglifyJS2,主要用于代码压缩
  • babylon,babel解析器,fork自Acorn,目前最新版本是babylon7,对应npm包@babel/parser
  • Espree,eslint默认的解析器,由于遵循同一套规范,也可以使用babel的解析器替代
  • flow、shift等等

我们选用 babel 的解析器来实践一下:

babel 主要有四个包来处理ast ,@babel/parser用来解析代码为AST,@babel/traverse用来转换代码时遍历代码,@babel/generator转换AST为code ,@babel/types是上述AST 的类型定义及一些工具函数,类似于lodash,方便我们操作节点的工具函数

1、用 ast 逆向构造一个构造代码块 const add = (a ,b)=>{return a + b;}

const parse = require('@babel/parser').parse;     //解析代码块
const traverse = require('@babel/traverse').default;  //遍历代码块
const generate = require('@babel/generator').default; // 生成代码块
const t = require('@babel/types');  // 工具函数

console.log(
  generate(
    t.variableDeclaration('const',[   //变量声明
      t.variableDeclarator(//变量定义
        t.identifier('add'),//标识符定义
        t.arrowFunctionExpression( //剪头函数表达式
          [],
          t.blockStatement(// 语句块 {}
            [
              t.returnStatement(// return 语句块
                t.binaryExpression( //二元表达式
                  "+",
                  t.identifier('a'),
                  t.identifier('b')
                )
              ),
            ]
          )
        )
      )
    ]) 
  ).code
)

2、去除代码中的 console.logdebugger 语句

const parse = require('@babel/parser').parse;     //解析代码块
const traverse = require('@babel/traverse').default;  //遍历代码块
const generate = require('@babel/generator').default; // 生成代码块
const t = require('@babel/types');  // 工具函数

const code = `

function add (a,b) {
  // 嗯,去除 console.log 和 debugger
  console.log('xxxx');
  debugger;
  return a + b;
}
`

const ast = parse(code);

traverse(ast, {

  // debugger语句
  DebuggerStatement(path){
    path.remove();
  },
  // 调用表达式
  CallExpression(path){
    const {callee}  = path.node;
    if(callee.type = 'MemberExpression' && callee.object.name === 'console' && callee.property.name ==='log'){
      path.remove();
    }
  }
});

console.log(generate(ast,{comments:false}).code);

3、导出文件的所有函数

const parse = require('@babel/parser').parse;     //解析代码块
const traverse = require('@babel/traverse').default;  //遍历代码块
const generate = require('@babel/generator').default; // 生成代码块
const t = require('@babel/types');  // 工具函数

const code = `
function add(a, b) {
  return a + b;
}
function sub(a, b) {
    return a - b;
}
function commonDivision(a, b) {
  while(b !== 0){
    if(a > b){
      a = sub(a, b);
    }else{
      b = sub(b, a)
    }
  }
  return a;
}`;

// 完全遍历替换代码中的function
let functionIds = [];
const ast = parse(code);
traverse(ast,{
  FunctionDeclaration(path){
    const node = path.node;
    const funcName = node.id;
    const params = node.params;
    const body = node.body;
    functionIds.push(funcName.name);
    const rep = t.expressionStatement(
      t.assignmentExpression( //赋值表达式
        '=',
        t.memberExpression( //成员表达式
          t.identifier('exports'),
          funcName
        ),
        t.arrowFunctionExpression(params,body)  //箭头函数表达式
      )
    )
    path.replaceWith(rep);
  }
})
// 遍历调用改为 exports.xxxx
traverse(ast,{
  CallExpression(path){
    const node = path.node;
    if(functionIds.includes(node.callee.name)){
      node.callee = t.memberExpression(t.identifier('exports'),node.callee);
    }
  }
})
console.log(generate(ast).code);

4、编写 一个babel插件,删除文件中的 debugger

// babel 配置
// babel.config.js
const removePlugin = require('./babel-plugin-debugger-remove-plugin');
// 预设
const presets = [
  [
    "@babel/env",
    {
      targets: {
        edge: "17",
        firefox: "60",
        chrome: "67",
        safari: "11.1",
      },
      useBuiltIns: "usage",
    },
  ],
];
// 添加 自定义插件
const plugins = [
  [removePlugin,{isOpen:true}]
];
module.exports = { presets, plugins };

// babel-plugin-debugger-remove-plugin.js
// babel debugger去除插件
module.exports =  function({ types: t }) {
  return {
    visitor: {
      DebuggerStatement(path, state= {opts:{isOpen:true}}) {
        console.log(state.opts.isOpen);
        if(state.opts.isOpen){
          path.remove();
        }
      }
    }
  };
};

结语

几个例子下来我们可以了解到,ast可以让我们更加了解javascript.也可以开发一些工具来优化转换代码。平常开发过程中,很少接触AST,但包括Vue/react,等的模板转换都以AST为基础。以上抛砖引玉介绍一下简单例子,希望同学们在研究AST过程中可以产出更多基于AST的优秀工具、项目。

拓展资料