阅读 154

AST抽象语法树

ast,即_Abstract Syntax Tree_抽象语法树(通常被简写成AST),在计算机科学中,抽象语法树其实是源代码的抽象语法结构的树状表现形式

AST 抽象语法树,似乎我们平时并不会接触到。然而我们打开package.json会发现这个其实很多依赖其实都是通过ast来完成的。在我们的项目当中,CSS 预处理,JSX 亦或是 TypeScript 的处理,Javascript 转译,代码压缩,Webpack, Vue-Cli,babel,这些插件经常都能被用到

使用场景

  • JS 反编译,语法解析:
  • Babel 编译 ES6 语法
  • 代码高亮:
  • 关键字匹配
  • 作用域判断
  • 代码压缩

1. 拆解ast

ast拆解器,在线玩转AST

举个栗子:

function add(a, b) {
    return a + b
}
复制代码

解析过程:

词法分析:也叫做扫描scanner。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识tokens。同时,它会移除空白符,注释,等。最后,整个代码将被分割进一个tokens列表,也叫做令牌流(或者说一维数组)。 image.png

语法分析:也解析器。它会将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。

FunctionDeclaration,这是一个函数声明

  • 一个id,就是它的名字,即add,一个Identifier

    • { name: 'add' type: 'identifier' ... }
  • 两个params,就是它的参数,即[a, b],两个Identifier组成的数组

    • [ { name: 'a' type: 'identifier' ... }, { name: 'b' type: 'identifier' ... } ]
  • 一块body,也就是大括号内的一堆东西

    • BlockStatement(块状域)对象,用来表示是{return a + b}
      • ReturnStatement(Return域)对象,用来表示return a + b
        • BinaryExpression(二项式)对象,用来表示a + b
          • operator 即+
          • left 里面装的,是Identifier对象 a
          • right 里面装的,是Identifer对象 b

他的树的表现形式如下: image.png 对ast对象不清楚可以查看ast对象文档

我们要理解的是: AST 中的节点是可以完整地描述它在模板中映射的节点信息 ast的生成过程总结如下:源码~词法分析(scanner)~token流~语法分析(parse)~AST

除了js,html也能映射成抽象语法树,最常见的是我们写的vue tempalte里,其实存放的是html的字符串,经过vue的一些列解析后生成了ast,直观的映射过程如下图 image.png

手写ast解析器:ast-parse github上比较完善的一个ast编译器项目:从一个具体的项目来看ast的词法解析和语法解析过程, the-super-tiny-compiler

值得一提的是,社区中有各种AST parser实现

2. babel代码转译

image.png

babel是一个javascript编译器。宏观来说,它分3个阶段运行代码:解析(parsing),转译(transforming),生成(generation)。

  • babylon: Babel 的解析器。
  • babel-traverse:Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
  • babel-generator:Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

从一个简单的示例来看babel代码转译的过程:

import * as babylon from 'babylon'
import traverse from 'babel-traverse'
import generate from 'babel-generator'

const code = ` const abc = 5;`
const ast = babylon.parse(code);

traverse(ast, {
    enter(path) {
        if(path.node.type === 'Identifier') {
            path.node.name = path.node.name.split("").reverse().join("")
        }
    }
})

const newCode = generate(ast);

console.log(newCode) // 打印出 const cba = 5
复制代码

相关api可查看babel文档-API 如果想要学习怎么创建一个babel-plugin,可以查看编写你的第一个babel插件

当我们开发babel-plugin的时候,我们只需要描述转化你AST的节点“visitors”就可以了。

//your-plugin.js 

export default function ({ types: t }) {
    return {
        visitor: {
            Identifier(path) {
                const name = path.node.name;
                path.node.name = name.split("").reserve().join("");
            }
        }
    }
}
复制代码

示例:将箭头函数转化为函数表达式

let babel = require('babel-core');//babel核心库
let types = require('babel-types');
let code = `codes.map(code=>{return code.toUpperCase()})`;//转换语句

let visitor = {
    ArrowFunctionExpression(path) {//定义需要转换的节点
        let params = path.node.params
        let blockStatement = path.node.body
        let func = types.functionExpression(null, params, blockStatement, false, false)
        path.replaceWith(func) //
    }
}

let arrayPlugin = { visitor }
let result = babel.transform(code, {
    plugins: [
        arrayPlugin
    ]
})
console.log(result.code)
复制代码

示例:构建一个静态的语法分析器,分析函数的调用和声明是否符合规范

var fs = require('fs'),
esprima = require('esprima');
estraverse = require('estraverse')
    
// 1. 执行主要的代码分析工作
function analyzeCode(code) {
    var ast = esprima.parse(code);  

    var functionsStats = {}; // 1. 存放函数的调用和声明的统计信息 

    var addStatsEntry = function (funcName) { // 2. 存放统计信息  
        if (!functionsStats[funcName]) {  
            functionsStats[funcName] = { calls: 0, declarations: 0 };  
        }  
    };  

    // 3. 遍历AST  
    estraverse.traverse(ast, {
        enter: function (node) { 
            if (node.type === 'FunctionDeclaration') {  
                addStatsEntry(node.id.name); // 4. 如果发现了函数声明,增加一次函数声明  
                functionsStats[node.id.name].declarations++;  
            } else if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {  
                addStatsEntry(node.callee.name);  
                functionsStats[node.callee.name].calls++; // 5. 如果发现了函数调用,增加一次函数调用
            }  
        }  
    });  

    // 处理结果
    processResults(functionsStats);
}

function processResults(results) {
    for (var name in results) {
        if (results.hasOwnProperty(name)) { 
            var stats = results[name];  
            if (stats.declarations === 0) {
                console.log('Function', name, 'undeclared');  
            } else if (stats.declarations > 1) {
                console.log('Function', name, 'decalred multiple times');  
            } else if (stats.calls === 0) {
                console.log('Function', name, 'declared but not called');  
            }  
        }  
    }  
}  
复制代码

更多babel插件应用实例: class转es5

3. 自动代码重构工具

jscodeshift 是facebook出的一款围绕recast的增强过的可以遍历更改js 里面ast节点,并且重新生成js代码的工具。 基于ast的代码重构工具。

安装jscodeshift

npm i jscodeshift -g
复制代码

github上已经有一些现成的codemod迁移的脚本,不必要再手动造轮子,极大的提升效率。 js-codemod:迁移一般 JavaScript 代码(比如 ES5 -> ES2015) 的工具 react-codemod:迁移 React 相关项目的工具,无痛升级旧版React

jscodeshift应用实例

  1. 将get方法改为post方法:
module.exports=function(fileInfo, api, options) { 
    const jsContext = fileInfo.source; 
    const j = api.jscodeshift; 
    return j(jsContext).find(j.Identifier, {name: 'get'}).forEach(path=> { 
        j(path).replaceWith(j.identifier('post')); 
    }).toSource(); 
};
复制代码
  1. 替换函数变量名:ab -> testab
export default function transformer(file, api) {
    const j = api.jscodeshift;
    const source = j(file.source)
    source
        .find(j.FunctionDeclaration)
        .filter(path => path.value.id.name == 'ab')
        .replaceWith(path => {
            return j.functionDeclaration(
                    j.identifier('testab'), 
                    [], 
                    path.value.body
                )
        })
    return source.toSource({
        quote: 'single',
    });
}
复制代码
  1. 替换掉所有的老掉牙的匿名函数,把他们变成Lambda表达式(箭头函数)

js-codemod中内置了arrow-function.js,可以用来做这件事。

比如替换下文件中的

foo().then(function(data) {
	console.log(data)
})

// 替换成
// foo().then(data => {
//   console.log(data)
// })
复制代码

执行下面的命令:

git clone https://github.com/cpojer/js-codemod
jscodeshift -t js-codemod/transforms/arrow-function.js transform/fileA.js
复制代码
文章分类
前端
文章标签