抽象语法树AST(Abstract Syntax Tree)

抽象语法树(Abstract Syntax Tree)

  • 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示
  • 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构

抽象语法树用途

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
    • IDE的错误提示、格式化、高亮、自动补全等等
  • 代码混淆压缩
    • UglifyJS2等
  • 优化变更代码,改变代码结构使达到想要的结构
    • 代码打包工具webpack、rollup等等
    • CommonJS、AMD、CMD、UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

AST过程

解析过程

AST整个解析过程分为两个步骤

  • 分词:将整个代码字符串分割成语法单元数组
  • 语法分析:建立分析语法单元之间的关系

语法单元

Javascript 代码中的语法单元主要包括以下这么几种

  • 关键字:constletvar
  • 标识符:可能是一个变量,也可能是 if、else 这些关键字,又或者是 true、false 这些常量
  • 运算符
  • 数字
  • 空格
  • 注释

词法分析

let jsx = `let element=<h1>hello</h1>`;

function lexical(code) {
    const tokens=[];
    for (let i=0;i<code.length;i++){
        let char=code.charAt(i);
        if (char == '=') {
            tokens.push({
                type: 'operator',
                value:char
            });
        }
        if (char=='<') {
            const token={
                type: 'JSXElement',
                value:char
            }
            tokens.push(token);
            let isClose = false;
            for (i++;i<code.length;i++){
                char=code.charAt(i);
                token.value+=char;
                if (char=='>') {
                    if (isClose) {
                        break;
                    } else {
                        isClose=true;
                    }
                }
            }
            continue;
        }
        if (/[a-zA-Z\$\_]/.test(char)) {
            const token={
                type: 'Identifier',
                value:char
            }
            tokens.push(token);
            for (i++;i<code.length;i++){
                char=code.charAt(i);
                if (/[a-zA-Z\$\_]/.test(char)) {
                    token.value+=char;
                } else {
                    i--;
                    break;
                }
            }
            continue;
        }

        if (/\s/.test(char)) {
            const token={
                type: 'whitespace',
                value:char
            }
            tokens.push(token);
            for (i++;i<code.length;i++){
                char=code.charAt[i];
                if (/\s/.test(char)) {
                    token.value+=char;
                } else {
                    i--;
                    break;
                }
            }
            continue;
        }
    }
    return  tokens;
}
let result=lexical(jsx);
console.log(result);
[
  { type: 'Identifier', value: 'let' },
  { type: 'whitespace', value: ' ' },
  { type: 'Identifier', value: 'element' },
  { type: 'operator', value: '=' },
  { type: 'JSXElement', value: '<h1>hello</h1>' }
]

语法分析

  • 语义分析则是将得到的词汇进行一个立体的组合,确定词语之间的关系
  • 简单来说语法分析是对语句和表达式识别,这是个递归过程
// babylon7 https://astexplorer.net/
// babylon7 https://astexplorer.net/
function parse(tokens) {
    const ast={
        type: 'Program',
        body: [],
        sourceType:'script'
    }
    let i=0;//标示当前位置
    let currentToken;//当前的符号
    while ((currentToken = tokens[i])) {
        if (currentToken.type == 'Identifier' && (currentToken.value == 'let'||currentToken.value == 'var')) {
            const VariableDeclaration={
                type: 'VariableDeclaration',
                declarations:[]
            }
            i+=2;
            currentToken=tokens[i];
            let VariableDeclarator = {
                type: 'VariableDeclarator',
                id: {
                    type: 'Identifier',
                    name:currentToken.value
                }
            };
            VariableDeclaration.declarations.push(VariableDeclarator);
            i+=2;
            currentToken=tokens[i];
            if (currentToken.type=='JSXElement') {
                let value=currentToken.value;
                let [,type,children]=value.match(/([^<]+?)>([^<]+)<\/\1>/);
                VariableDeclarator.init={
                    type: 'JSXElement',
                    openingElement:{
                       type:'JSXOpeningElement',
                       name:{
                           type:'JSXIdentifier',
                           name:'h1'
                       }
                    },
                    closingElement:{
                       type:'JSXClosingElement',
                       name:{
                           type:'JSXIdentifier',
                           name:'h1'
                       }
                    },
                    name: type,
                    children:[
                        {
                            type:'JSXText',
                            value:'hello'
                        }
                    ]
                }
            } else {
                VariableDeclarator.init={
                    type: 'Literal',
                    value:currentToken.value
                }
            }
            ast.body.push(VariableDeclaration);
        }
        i++;
    }
    return ast;
}

let tokens=[
    {type: 'Identifier',value: 'let'},
    {type: 'whitespace',value: ' '},
    {type: 'Identifier',value: 'element'},
    {type: 'operator',value: '='},
    {type: 'JSXElement',value: '<h1>hello</h1>'}
];
let result = parse(tokens);
console.log(result);
console.log(JSON.stringify(result));
{
    "type": "Program",
    "body": [{
        "type": "VariableDeclaration",
        "declarations": [{
            "type": "VariableDeclarator",
            "id": {
                "type": "Identifier",
                "name": "element"
            },
            "init": {
                "type": "JSXElement",
                "openingElement": {
                    "type": "JSXOpeningElement",
                    "name": {
                        "type": "JSXIdentifier",
                        "name": "h1"
                    }
                },
                "closingElement": {
                    "type": "JSXClosingElement",
                    "name": {
                        "type": "JSXIdentifier",
                        "name": "h1"
                    }
                },
                "name": "h1",
                "children": [{
                    "type": "JSXText",
                    "value": "hello"
                }]
            }
        }]
    }],
    "sourceType": "script"
}

抽象语法树定义

  • 这些工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

ast

JavaScript Parser

  • avaScript Parser,把js源码转化为抽象语法树的解析器。
  • 浏览器会把js源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。
  • 一般来说每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了详细SpiderMonkey AST format的详细说明,算是业界的标准。

常用的JavaScript Parser

  • esprima
  • traceur
  • acorn
  • babel parser
  • shift
  • estree

AST节点

  • estree
  • spec.md
  • astexplorer
  • AST节点
    • File 文件
    • Program 程序
    • Literal 字面量 NumericLiteral StringLiteral BooleanLiteral
    • Identifier 标识符
    • Statement 语句
    • Declaration 声明语句
    • Expression 表达式
    • Class 类

AST遍历

npm i esprima estraverse escodegen -S
let esprima = require('esprima');//把JS源代码转成AST语法树
let estraverse = require('estraverse');///遍历语法树,修改树上的节点
let escodegen = require('escodegen');//把AST语法树重新转换成代码
let code = `function ast(){}`;
let ast = esprima.parse(code);
let indent = 0;
const padding = ()=>" ".repeat(indent);
estraverse.traverse(ast,{
    enter(node){
        console.log(padding()+node.type+'进入');
        if(node.type === 'FunctionDeclaration'){
            node.id.name = 'newAst';
        }
        indent+=2;
    },
    leave(node){
        indent-=2;
        console.log(padding()+node.type+'离开');
    }
});
Program进入
  FunctionDeclaration进入
    Identifier进入
    Identifier离开
    BlockStatement进入
    BlockStatement离开
  FunctionDeclaration离开
Program离开

esprima

mkdir hsast
cd hsast

cnpm i esprima estraverse escodegen- S
let esprima = require('esprima');
var estraverse = require('estraverse');
var escodegen = require("escodegen");
let code = 'function ast(){}';
let ast=esprima.parse(code);
let indent=0;
function pad() {
    return ' '.repeat(indent);
}
estraverse.traverse(ast,{
    enter(node) {
        console.log(pad()+node.type);
        if(node.type == 'FunctionDeclaration'){
            node.id.name = 'ast_rename';
        }
        indent+=2;
     },
    leave(node) {
        indent-=2;
        console.log(pad()+node.type);

     }
 });
let generated = escodegen.generate(ast);
console.log(generated);
Program
  FunctionDeclaration
    Identifier
    Identifier
    BlockStatement
    BlockStatement
  FunctionDeclaration
Program

babel

  • Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行
  • 工作过程分为三个部分
    • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
    • Transform(转换) 对抽象语法树进行转换
    • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

babel 插件

Visitor

  • 访问者模式 Visitor 对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • Visitor 的对象定义了用于 AST 中获取具体节点的方法
  • Visitor 上挂载以节点 type 命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法

path

  • path
  • node 当前 AST 节点
  • parent 父 AST 节点
  • parentPath 父AST节点的路径
  • scope 作用域
  • get(key) 获取某个属性的 path
  • set(key, node) 设置某个属性
  • is类型(opts) 判断当前节点是否是某个类型
  • find(callback) 从当前节点一直向上找到根节点(包括自己)
  • findParent(callback)从当前节点一直向上找到根节点(不包括自己)
  • insertBefore(nodes) 在之前插入节点
  • insertAfter(nodes) 在之后插入节点
  • replaceWith(replacement) 用某个节点替换当前节点
  • replaceWithMultiple(nodes) 用多个节点替换当前节点
  • replaceWithSourceString(replacement) 把源代码转成AST节点再替换当前节点
  • remove() 删除当前节点
  • traverse(visitor, state) 遍历当前节点的子节点,第1个参数是节点,第2个参数是用来传递数据的状态
  • skip() 跳过当前节点子节点的遍历

scope

  • scope
  • scope.bindings 当前作用域内声明所有变量
  • scope.path 生成作用域的节点对应的路径
  • scope.references 所有的变量引用的路径
  • getAllBindings() 获取从当前作用域一直到根作用域的集合
  • getBinding(name) 从当前作用域到根使用域查找变量
  • getOwnBinding(name) 在当前作用域查找变量
  • parentHasBinding(name, noGlobals) 从当前父作用域到根使用域查找变量
  • removeBinding(name) 删除变量
  • hasBinding(name, noGlobals) 判断是否包含变量
  • moveBindingTo(name, scope) 把当前作用域的变量移动到其它作用域中
  • generateUid(name) 生成作用域中的唯一变量名,如果变量名被占用就在前面加下划线

转换箭头函数

转换前

const sum = (a,b)=>{
    console.log(this);
    return a+b;
}

转换后

var _this = this;

const sum = function (a, b) {
  console.log(_this);
  return a + b;
};
npm i @babel/core @babel/types -D

实现

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//let arrowFunctionPlugin = require('babel-plugin-transform-es2015-arrow-functions');
let arrowFunctionPlugin = {
    visitor: {
        //如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
        ArrowFunctionExpression(path) {
            let { node } = path;
            hoistFunctionEnvironment(path);
            node.type = 'FunctionExpression';
            let body = node.body;
            //如果函数体不是语句块
            if (!types.isBlockStatement(body)) {
                node.body = types.blockStatement([types.returnStatement(body)]);
            }
        }
    }
}
/**
 * 1.要在函数的外面声明一个_this变量,值是this
 * 2.在函数的内容,换this 变成_this
 * @param {*} path 
 */
function hoistFunctionEnvironment(path) {
    //1.确定我要用哪里的this 向上找不是箭头函数的函数或者根节点
    const thisEnv = path.findParent(parent => {
        return (parent.isFunction() && !path.isArrowFunctionExpression()) || parent.isProgram();
    });
    let thisBindings = '_this';
    let thisPaths = getThisPaths(path);
    if (thisPaths.length>0) {
        //在thisEnv这个节点的作用域中添加一个变量 变量名为_this, 值 为this var _this = this;
        if (!thisEnv.scope.hasBinding(thisBindings)) {
            thisEnv.scope.push({
                id: types.identifier(thisBindings),
                init: types.thisExpression()
            });
        }
    }
    thisPaths.forEach(thisPath => {
        //this=>_this
        thisPath.replaceWith(types.identifier(thisBindings));
    });
}
function getThisPaths(path){
    let thisPaths = [];
    path.traverse({
        ThisExpression(path) {
            thisPaths.push(path);
        }
    });
    return thisPaths;
}
let sourceCode = `
const sum = (a, b) => {
    console.log(this);
    const minus = (c,d)=>{
          console.log(this);
        return c-d;
    }
    return a + b;
}
`;
let targetSource = core.transform(sourceCode, {
    plugins: [arrowFunctionPlugin]
});

console.log(targetSource.code);

把类编译为 Function

es6

class Person {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

classast

es5

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function () {
  return this.name;
};

es5class1 es5class2

实现

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//let transformClassesPlugin = require('@babel/plugin-transform-classes');
let transformClassesPlugin = {
    visitor: {
        //如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
        //path代表路径,node代表路径上的节点
        ClassDeclaration(path) {
            let node = path.node;
            let id = node.id;//Identifier name:Person
            let methods = node.body.body;//Array<MethodDefinition>
            let nodes = [];
            methods.forEach(method => {
                if (method.kind === 'constructor') {
                    let constructorFunction = types.functionDeclaration(
                        id,
                        method.params,
                        method.body
                    );
                    nodes.push(constructorFunction);
                } else {
                    let memberExpression = types.memberExpression(
                        types.memberExpression(
                            id, types.identifier('prototype')
                        ), method.key
                    )
                    let functionExpression = types.functionExpression(
                        null,
                        method.params,
                        method.body
                    )
                    let assignmentExpression = types.assignmentExpression(
                        '=',
                        memberExpression,
                        functionExpression
                    );
                    nodes.push(assignmentExpression);
                }
            })
            if (nodes.length === 1) {
                //单节点用replaceWith
                //path代表路径,用nodes[0]这个新节点替换旧path上现有老节点node ClassDeclaration
                path.replaceWith(nodes[0]);
            } else {
                //多节点用replaceWithMultiple
                path.replaceWithMultiple(nodes);
            }
        }
    }
}
let sourceCode = `
class Person{
    constructor(name){
        this.name = name;
    }
    sayName(){
        console.log(this.name);
    }
}
`;
let targetSource = core.transform(sourceCode, {
    plugins: [transformClassesPlugin]
});

console.log(targetSource.code);

实现日志插件

logger.js

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
const types = require("@babel/types");
const path = require('path');
const visitor = {
    CallExpression(nodePath, state) {
        const { node } = nodePath;
        if (types.isMemberExpression(node.callee)) {
            if (node.callee.object.name === 'console') {
                if (['log', 'info', 'warn', 'error', 'debug'].includes(node.callee.property.name)) {
                    const { line, column } = node.loc.start;
                    const relativeFileName = path.relative(__dirname, state.file.opts.filename).replace(/\\/g, '/');
                    node.arguments.unshift(types.stringLiteral(`${relativeFileName} ${line}:${column}`));
                }
            }
        }
    }
}
module.exports = function () {
    return {
        visitor
    }
}
/* {
    loc: {
        start: { line: 1, column: 1 }
    }
} */

自动日志插件

use.js

const { transformSync } = require('@babel/core');
const autoLoggerPlugin = require('./auto-logger-plugin');
const sourceCode = `
function sum(a,b){return a+b;}
const multiply = function(a,b){return a*b;};
const minus = (a,b)=>a-b
class Calculator{divide(a,b){return a/b}}
`
const { code } = transformSync(sourceCode, {
  plugins: [autoLoggerPlugin({ libName: 'logger' })]
});
console.log(code);

auto-logger-plugin

const importModule = require('@babel/helper-module-imports');
const template = require('@babel/template');
const types = require('@babel/types');
const autoLoggerPlugin = (options) => {
    return {
        visitor: {
            Program: {
                enter(path, state) {
                    let loggerId;
                    path.traverse({
                        ImportDeclaration(path) {
                            const libName = path.get('source').node.value;
                            if (libName === options.libName) {
                                const specifierPath = path.get('specifiers.0');
                                //import logger from 'logger'
                                //import { logger } from 'logger';
                                //import * as logger from 'logger';
                                if (specifierPath.isImportDefaultSpecifier()
                                    || specifierPath.isImportSpecifier()
                                    || specifierPath.isImportNamespaceSpecifier()) {
                                    loggerId = specifierPath.local.name;
                                }
                                path.stop();
                            }
                        }
                    });
                    if (!loggerId) {
                        loggerId = importModule.addDefault(path, 'logger', {
                            nameHint: path.scope.generateUid('logger')
                        }).name;
                    }
                    //state.loggerNode = types.expressionStatement(types.callExpression(types.identifier(loggerId), []));
                    //state.loggerNode = template.statement(`${loggerId}();`)();
                    state.loggerNode = template.statement(`LOGGER();`)({
                        LOGGER: loggerId
                    });
                }
            },
            'FunctionExpression|FunctionDeclaration|ArrowFunctionExpression|ClassMethod'(path, state) {
                const { node } = path
                if (types.isBlockStatement(node.body)) {
                    node.body.body.unshift(state.loggerNode);
                } else {
                    const newNode = types.blockStatement([
                        state.loggerNode,
                        types.expressionStatement(node.body)
                    ]);
                    path.get('body').replaceWith(newNode);
                }
            }
        }
    }
};
module.exports = autoLoggerPlugin;

eslint

use.js

const { transformSync } = require('@babel/core');
const eslintPlugin = require('./eslintPlugin');
const sourceCode = `
var a = 1;
console.log(a);
var b = 2;
`;
const { code } = transformSync(sourceCode, {
  plugins: [eslintPlugin({ fix: true })]
});
console.log(code);

eslintPlugin.js

eslintPlugin.js

//no-console 禁用 console
const eslintPlugin = ({ fix }) => {
  return {
    pre(file) {
      file.set('errors', []);
    },
    visitor: {
      CallExpression(path, state) {
        const errors = state.file.get('errors');
        const { node } = path
        if (node.callee.object && node.callee.object.name === 'console') {
          //const tmp = Error.stackTraceLimit;//可以修改堆栈信息的深度,默认为10
          //Error.stackTraceLimit = 0;
          errors.push(path.buildCodeFrameError(`代码中不能出现console语句`, Error));
          //Error.stackTraceLimit = tmp;
          if (fix) {
            path.parentPath.remove();
          }
        }
      }
    },
    post(file) {
      console.log(...file.get('errors'));
    }
  }
};
module.exports = eslintPlugin;

uglify

use.js

const { transformSync } = require('@babel/core');
const uglifyPlugin = require('./uglifyPlugin');
const sourceCode = `
function getAge(){
  var age = 12;
  console.log(age);
  var name = '';
  console.log(name);
}
`;
const { code } = transformSync(sourceCode, {
  plugins: [uglifyPlugin()]
});
console.log(code);

uglifyPlugin.js

uglifyPlugin.js

const uglifyPlugin = () => {
  return {
    visitor: {
      Scopable(path) {
        Object.entries(path.scope.bindings).forEach(([key, binding]) => {
          const newName = path.scope.generateUid();
          binding.path.scope.rename(key, newName)
        });
      }
    }
  }
};
module.exports = uglifyPlugin;

webpack中使用babel插件

实现按需加载

import { flatten, concat } from "lodash";

treeshakingleft

转换为

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

treeshakingright

webpack 配置

npm i webpack webpack-cli babel-plugin-import -D
const path = require("path");
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve("dist"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options:{
                   plugins:[
                     [
                       path.resolve(__dirname,'plugins/babel-plugin-import.js'),
                       {
                         libraryName:'lodash'
                       }
                     ]
                   ]
                }
        },
      },
    ],
  },
};

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

babel 插件

plugins\babel-plugin-import.js

//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");

const visitor = {
    ImportDeclaration(path, state) {
        const { node } = path;//获取节点
        const { specifiers } = node;//获取批量导入声明数组
        const { libraryName, libraryDirectory = 'lib' } = state.opts;//获取选项中的支持的库的名称
        //如果当前的节点的模块名称是我们需要的库的名称
        if (node.source.value === libraryName
            //并且导入不是默认导入才会进来
            && !types.isImportDefaultSpecifier(specifiers[0])) {
            //遍历批量导入声明数组
            const declarations = specifiers.map(specifier => {
                //返回一个importDeclaration节点
                return types.importDeclaration(
                    //导入声明importDefaultSpecifier flatten
                    [types.importDefaultSpecifier(specifier.local)],
                    //导入模块source lodash/flatten
                    types.stringLiteral(libraryDirectory ? `${libraryName}/${libraryDirectory}/${specifier.imported.name}` : `${libraryName}/${specifier.imported.name}`)
                );
            })
            path.replaceWithMultiple(declarations);//替换当前节点
        }
    }
}


module.exports = function () {
    return {
        visitor
    }
}

参考

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值