1、AST语法树简介
当编写代码分析工具、代码美化工具、网站逆向分析等场景时,通常需要使用AST语法树技术。
比如项目开发过程中常遇到的场景:某个公共函数名需要更改,但被很多文件多处代码调用,手工修改非常容易漏改、改错等,而且还花时间。又如在网站逆向分析时,由于使用了混淆技术,代码难以阅读。 这时使用AST技术可以轻松完成这些任务。
对于重要项目,使用AST技术来开发自动化代码质量检查工具,也是提高项目质量的重要手段。
1.1 什么是AST
AST(Abstract Syntax Tree)抽象语法树,是源代码语法结构的一种抽象表示。常用于编译器、代码分析工具等。
如何将代码转为AST呢?
构建 AST 树的步骤
生成AST树之前,通常需要先将源代码转为 Parser Tree。
如1个表达式,2*7 + 3 , 先转为 Parser Tree,如下,其根节点,由 1个操作符 + 与2个 term 组成。左边的term 是 factor * factor 构成。 右边的term 为1个factor.。这个Parser Tree表示了完整的语法规则,但如果语句复杂,会造成树的层次复杂。
AST tree是Parse Tree的简化,其特点:
用操作符做为根节点。整个树由根节点,内部节点,叶节点组成
叶节点是由操作数组成,内部节点的子节点都是操作数(或称叶节点)。
内部节点不展示语法规则,因此是抽象的。
1.2 流行的AST工具库
当前有许多工具支持源代码转AST 树,以及逆向转回。
在线工具
https://astexplorer.net/
Babel/parser插件方案
babel/parser是JS社区最常用的解析器。但Babel方案节点多、API也很多,因此学习成本高, 另外编程代码工作量较大,可读性差。
jscodeshift
相比于Babel而言,jscodeshift的优势是匹配节点更简便一些,链式操作用起来更加顺手。还提供了命令行工具。
本文主要介绍,用jscodeshift 来进行AST解析。
2、jscodeshift库介绍
jscodeshift 是一个源代码转换工具,它基于 AST(抽象语法树)进行操作,使得开发者能够自定义转换规则来处理 JavaScript 代码。
当你使用 jscodeshift 解析 JavaScript 源文件后,会得到一个表示源文件结构的 AST。理解jscodeshift的AST树结构是最基础的知识点。
AST 树的每个节点都代表了源代码中的一部分。节点是object类型,由许多“属性–值”对组成。 比如 type, value, id, 子节点等。
在 AST 中,主要的概念和名词包括:
节点(Node):AST 的基本单元,节点通常包含类型、值、子节点等属性。
{
type: ‘Literal’,
value: ‘foo’,
raw: ‘“foo”’
}
类型(Type):节点的类型,比如 VariableDeclarator(变量声明符)、CallExpression(函数调用表达式)等。
值(Value):对于某些类型的节点,比如字面量节点(字符串、数字等),会有一个值属性,表示该节点的具体值。
子节点(Children or Subtree):节点通常会有子节点,这些子节点代表了该节点在语法树下的更具体的结构。例如,一个 CallExpression 节点可能有一个子节点是函数名,另一个子节点是参数列表。
jscodeshift 主要数据结构
1)NodePath对象
: 是对Node的封装,
2) Collection对象
是1个Array,元素为NodePath,
3、使用jscodeshift修改源代码步骤
3.1 安装 jscodeshift
全局安装
npm i -g jscodeshift
本地安装
npm install --save-dev jscodeshift
为测试目的,安装 lodash库
npm install --save-dev lodash
3.2 准备1个源文件
如我们准备1个源文件 example.js , 包含1个add()函数, 及调用。 后面我们通过AST技术来修改函数名以及调用名。
const array = require('lodash/array')
var x = 1;
var a1 = array.initial([1,2,3,4,5])
console.log(a1)
// define a function
function add(a,b){
return a + b;
}
x = add( 10,20)
3.3 使用AST 查找与修改函数名与调用名
对于初学者来说,可以先用在线工具可视化方式查看AST节点,确定要查找及替换节点名称。
访问地址:https://astexplorer.net/
3.3.1 先用在线工具查看 AST 结果
打开 https://astexplorer.net/ 网页,将example.js 代码拷贝过来后,会自动生成 AST数。
可以看到function 的节点 type 为FunctionDeclaration,
x = add( 10,20)
对应节点为 ExpressionStatement, 我们要查找的调用表达式为为右侧子节点,打开会看到, 子节点的type 为 CallExpression
3.3.2 编写代码
在example.js 同1目录下,新建1个文件ast.js,添加如下内容:
const path = require('path')
const fs = require('fs')
const jc = require('jscodeshift') //引入jscodeshift
// 读取源代码文件
const fpath = path.join(__dirname, 'example.js')
const root = jc(fs.readFileSync(fpath, 'utf8'))
let oldFuncName = "add" //设置原函数名称
//查找及修改函数名称
let newFuncName = "newAdd"
root.find(jc.FunctionDeclaration)
.forEach( (path) => {
//如果函数名为oldFuncName, 则修改为新名称
if (path.value.id.name == oldFuncName) {
path.value.id.name = newFuncName
console.log("change name: ", path.value.id.name)
}
})
// 修改函数调用名称
root.find(jc.CallExpression).forEach((path)=>{
if(path.value.callee.name == oldFuncName){
path.value.callee.name = newFuncName
console.log("change name of CallExpress: ", path.value.callee.name)
}
})
// 输出修改后的代码
console.log(root.toSource())
代码解释:
const root = jc(fs.readFileSync(fpath, 'utf8'))
将源码转为AST 树结构对象root.find(jc.FunctionDeclaration)
在AST树中查找 type = 'FunctionDeclaration’ 的节点,可能有多个。 用forEach()方法遍历。find()
方法的查找结果为NodePath 集合, NodePath.value中保存 Node对象。 不同类型节点有不同的属性。- FunctionDeclaration 节点名称为 path.value.id.name,
- CallExpression 调用函数名为 path.value.callee.name
oot.toSource()
将 AST 树还原为源代码。
3.3.3 测试代码
在代码所在目录下
node ast.js
输出结果如下,可以看到,函数名与调用名均已改为 newAdd
change name: newAdd
change name: newAdd
const array = require('lodash/array')
var x = 1;
var a1 = array.initial([1,2,3,4,5])
console.log(a1)
// define a function
function newAdd(a,b){
return a + b;
}
x = newAdd( 10,20)
怎么样,使用 AST 来修改代码也不复杂 ,而且效率远高于手工。可以按上述思路,修改变量名,添加、删除语句等操作,对自己的代码进行修改或美化。