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列表,也叫做令牌流(或者说一维数组)。
语法分析:也解析器。它会将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。
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
- BinaryExpression(二项式)对象,用来表示a + b
- ReturnStatement(Return域)对象,用来表示return a + b
- BlockStatement(块状域)对象,用来表示是{return a + b}
他的树的表现形式如下:
对ast对象不清楚可以查看ast对象文档
我们要理解的是: AST 中的节点是可以完整地描述它在模板中映射的节点信息 ast的生成过程总结如下:源码~词法分析(scanner)~token流~语法分析(parse)~AST
除了js,html也能映射成抽象语法树,最常见的是我们写的vue tempalte里,其实存放的是html的字符串,经过vue的一些列解析后生成了ast,直观的映射过程如下图
手写ast解析器:ast-parse github上比较完善的一个ast编译器项目:从一个具体的项目来看ast的词法解析和语法解析过程, the-super-tiny-compiler
值得一提的是,社区中有各种AST parser实现
- 早期有uglifyjs和esprima
- espree, 基于esprima,用于eslint,Introducing Espree, an Esprima alternative
- acorn,号称是相对于esprima性能更优, Acorn: yet another JavaScript parser
- babylon,出自acorn,用于babel
- babel-eslint,babel团队维护的,用于配合使用ESLint, GitHub - babel/babel-eslint: ESLint using Babel as the parser.
2. babel代码转译
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应用实例
- 将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();
};
- 替换函数变量名: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',
});
}
- 替换掉所有的老掉牙的匿名函数,把他们变成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