操作JavaScript的AST
Posted Jtag特工
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作JavaScript的AST相关的知识,希望对你有一定的参考价值。
操作javascript的AST
前面我们学习了eslint和stylelint的规则的写法,当大家实际去写的时候,一定会遇到很多细节的问题,比如解析的代码是有错误的,或者是属性值不足以分析出问题来之类的。我们还需要更多的工具来帮助我们简化规则开发的流程。比如说容错度更高的解析器,或者获取更丰富属性的工具。
我们知道,Eslint主要是基于AST层次进行操作的。
我们知道,eslint支持更换解析器,那么,它就需要一套标准。eslint使用的这套标准叫做estree规范。estree规范的指导委员会的三名成员,恰好来自eslint, acorn和babel.
Estree的基础格式,可以从ES5规范中查看到,从ES6规范开始,每个版本都增加新的规范。比如ES2016增加对"**"运算符的支持。
acorn解析器是支持plugin机制的,于是eslint所用的espree解析器和babel的解析器就都在acorn上进行扩展。
Acorn解析器
acorn的默认用法非常简单,直接来段代码字符串parse一下就出来AST结构了:
let acorn = require("acorn");
console.log(acorn.parse("for(let i=0;i<10;i+=1)console.log(i);", ecmaVersion: 2020));
输出如下:
Node
type: 'Program',
start: 0,
end: 39,
body: [
Node
type: 'ForStatement',
start: 0,
end: 39,
init: [Node],
test: [Node],
update: [Node],
body: [Node]
],
sourceType: 'script'
遍历语法树
解析好了语法树节点之后,我们就可以遍历树了。acorn-walk包为我们提供了遍历的能力。
Acorn-walk提供了几种粒度的遍历方式,比如我们用simple函数遍历所有的Literal值:
const acorn = require("acorn")
const walk = require("acorn-walk")
const code = 'for(let i=0;i<10;i+=1)console.log(i);';
walk.simple(acorn.parse(code, ecmaVersion:2020),
Literal(node)
console.log(`Found a literal: $node.value`);
);
输出如下:
Found a literal: 0
Found a literal: 10
Found a literal: 1
当然,更经常使用的是full函数:
const acorn = require("acorn")
const walk = require("acorn-walk")
const code = 'for(let i=0;i<10;i+=1)console.log(i);';
const ast1 = acorn.parse(code, ecmaVersion:2020);
walk.full(ast1, function(node)
console.log(node.type);
);
输出如下:
Identifier
Literal
VariableDeclarator
VariableDeclaration
Identifier
Literal
BinaryExpression
Identifier
Literal
AssignmentExpression
Identifier
MemberExpression
Identifier
CallExpression
ExpressionStatement
BlockStatement
ForStatement
Program
我们可以看到,最后是树根Program.
高容错版本 acorn-loose
Acorn正常使用起来没有什么问题,但是还有一点可以做得更好,就是容错的情况。
我们看一个出错的例子:
let acorn = require("acorn");
console.log(acorn.parse("let a = 1 );", ecmaVersion: 2020));
Acorn就不干了,报错:
SyntaxError: Unexpected token (1:10)
at Parser.pp$4.raise (acorn/node_modules/acorn/dist/acorn.js:3434:15)
at Parser.pp$9.unexpected (acorn/node_modules/acorn/dist/acorn.js:749:10)
at Parser.pp$9.semicolon (acorn/node_modules/acorn/dist/acorn.js:726:68)
at Parser.pp$8.parseVarStatement (acorn/node_modules/acorn/dist/acorn.js:1157:10)
at Parser.pp$8.parseStatement (acorn/node_modules/acorn/dist/acorn.js:904:19)
at Parser.pp$8.parseTopLevel (acorn/node_modules/acorn/dist/acorn.js:806:23)
at Parser.parse (acorn/node_modules/acorn/dist/acorn.js:579:17)
at Function.parse (acorn/node_modules/acorn/dist/acorn.js:629:37)
at Object.parse (acorn/node_modules/acorn/dist/acorn.js:5546:19)
at Object.<anonymous> (acorn/normal.js:3:19)
pos: 10,
loc: Position line: 1, column: 10 ,
raisedAt: 11
下面我们换成高容错版本的acorn-loose:
let acornLoose = require("acorn-loose");
console.log(acornLoose.parse("let a = 1 );", ecmaVersion: 2020 ));
Acorn-loose会将我们多写的半个括号识别成一个空语句:
Node
type: 'Program',
start: 0,
end: 12,
body: [
Node
type: 'VariableDeclaration',
start: 0,
end: 9,
kind: 'let',
declarations: [Array]
,
Node type: 'EmptyStatement', start: 11, end: 12
],
sourceType: 'script'
Espree解析器
espree既然是扩展acorn,基本用法当然是兼容的:
const espree = require("espree");
const code = "for(let i=0;i<10;i+=1)console.log(i);";
const ast = espree.parse(code, ecmaVersion: 2020 );
console.log(ast);
生成的格式当然也是estree, 跟acorn一样:
Node
type: 'Program',
start: 0,
end: 39,
body: [
Node
type: 'ForStatement',
start: 0,
end: 39,
init: [Node],
test: [Node],
update: [Node],
body: [Node]
],
sourceType: 'script'
如果看AST还不行,我们还可以直接看分词的效果:
const tokens = espree.tokenize(code, ecmaVersion: 2020 );
console.log(tokens);
结果如下:
[
Token type: 'Keyword', value: 'for', start: 0, end: 3 ,
Token type: 'Punctuator', value: '(', start: 3, end: 4 ,
Token type: 'Keyword', value: 'let', start: 4, end: 7 ,
Token type: 'Identifier', value: 'i', start: 8, end: 9 ,
Token type: 'Punctuator', value: '=', start: 9, end: 10 ,
Token type: 'Numeric', value: '0', start: 10, end: 11 ,
Token type: 'Punctuator', value: ';', start: 11, end: 12 ,
Token type: 'Identifier', value: 'i', start: 12, end: 13 ,
Token type: 'Punctuator', value: '<', start: 13, end: 14 ,
Token type: 'Numeric', value: '10', start: 14, end: 16 ,
Token type: 'Punctuator', value: ';', start: 16, end: 17 ,
Token type: 'Identifier', value: 'i', start: 17, end: 18 ,
Token type: 'Punctuator', value: '+=', start: 18, end: 20 ,
Token type: 'Numeric', value: '1', start: 20, end: 21 ,
Token type: 'Punctuator', value: ')', start: 21, end: 22 ,
Token type: 'Punctuator', value: '', start: 22, end: 23 ,
Token type: 'Identifier', value: 'console', start: 23, end: 30 ,
Token type: 'Punctuator', value: '.', start: 30, end: 31 ,
Token type: 'Identifier', value: 'log', start: 31, end: 34 ,
Token type: 'Punctuator', value: '(', start: 34, end: 35 ,
Token type: 'Identifier', value: 'i', start: 35, end: 36 ,
Token type: 'Punctuator', value: ')', start: 36, end: 37 ,
Token type: 'Punctuator', value: ';', start: 37, end: 38 ,
Token type: 'Punctuator', value: '', start: 38, end: 39
]
从结果可以看到,词法分析之后的结果是token,而语法分析之后的已经是语句的结果了。
关于为什么eslint为什么在acorn之上还要封装一个espree,是因为最早eslint依赖于esprima,二者之间有不兼容的地方,eslint需要更多的信息来分析代码。
Babel基础操作
最后出场的是虽然不是eslint默认,但是双向支持都不错的重型武器babel.
Babel解析器
大杀器babel也可以配置成只要ast的模式:
const code2 = 'function greet(input) return input ?? "Hello world";';
const babel = require("@babel/core");
result = babel.transformSync(code2, ast: true );
console.log(result.ast);
输出结果是这样的:
Node
type: 'File',
start: 0,
end: 54,
loc: SourceLocation
start: Position line: 1, column: 0 ,
end: Position line: 1, column: 54 ,
filename: undefined,
identifierName: undefined
,
errors: [],
program: Node
type: 'Program',
start: 0,
end: 54,
loc: SourceLocation
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
,
sourceType: 'module',
interpreter: null,
body: [ [Node] ],
directives: [],
leadingComments: undefined,
innerComments: undefined,
trailingComments: undefined
,
comments: [],
leadingComments: undefined,
innerComments: undefined,
trailingComments: undefined
我们还可以用babel.parseSync方法只去读取AST:
result2 = babel.parseSync(code2);
console.log(result2);
甚至我们可以只用parser包:
const babelParser = require('@babel/parser');
console.log(babelParser.parse(code2, ));
Babel的遍历器
Acorn有专门的遍历器包,Babel当然也不甘示弱,提供了@babel/traverse包来辅助遍历抽象语法树。
我们来看个代码节点路径的例子:
const code4 = 'let a = 2 ** 8;'
const ast4 = babelParser.parse(code4, )
const traverse2 = require("@babel/traverse");
traverse2.default(ast4,
enter(path)
console.log(path.type);
);
输出如下,是从Program自顶向下的路径:
Program
VariableDeclaration
VariableDeclarator
Identifier
BinaryExpression
NumericLiteral
NumericLiteral
类型判断
遍历之后,我们需要大量的工具函数去进行类型判断。Babel给我们提供了一个巨大的工具类库@babel/types.
比如,我们想判断一个AST节点是不是标识符,就可以调用isIdentifier函数去判断下,我们看个例子:
const code6 = 'if (a==2) a+=1;';
const t = require('@babel/types');
const ast6 = babelParser.parse(code6, )
traverse2.default(ast6,
enter(path)
if (t.isIdentifier(path.node))
console.log(path.node);
);
输出如下:
Node
type: 'Identifier',
start: 4,
end: 5,
loc: SourceLocation
start: Position line: 1, column: 4 ,
end: Position line: 1, column: 5 ,
filename: undefined,
identifierName: 'a'
,
name: 'a'
Node
type: 'Identifier',
start: 11,
end: 12,
loc: SourceLocation
start: Position line: 1, column: 11 ,
end: Position line: 1, column: 12 ,
filename: undefined,
identifierName: 'a'
,
name: 'a'
现在,我们要判断有没有表达式使用了"=="运算符,就可以这样写:
const code8 = 'if (a==2) a+=1;';
const ast8 = babelParser.parse(code6, )
traverse2.default(ast8,
enter(path)
if (t.isBinaryExpression(path.node))
if(path.node.operator==="==")
console.log(path.node);
);
isBinaryExpression也支持参数,我们可以把运算符的条件加上:
traverse2.default(ast8,
enter(path)
if (t.isBinaryExpression(path.node,operator:"=="))
console.log(path.node);
);
构造AST节点
光能判断类型还不算什么。@babel/type库的更主要的作用是可以用来生成AST Node。
比如我们要生成一个二元表达式,就可以用binaryExpression函数来生成:
let node7 = t.binaryExpression("==",t.identifier("a"),t.numericLiteral(0));
console.log(node7);
注意,标识符和字面量都不能生接给值,而是要用自己类型的构造函数来生成哈。
运行结果如下:
type: 'BinaryExpression',
operator: '==',
left: type: 'Identifier', name: 'a' ,
right: type: 'NumericLiteral', value: 0
要把运算符"“改成”=",直接替换掉就好:
node7.operator="===";
console.log(node7);
输出结果如下:
type: 'BinaryExpression',
operator: '===',
left: type: 'Identifier', name: 'a' ,
right: type: 'NumericLiteral', value: 0
我们把上面的逻辑串一下,将"“运算符替换成”="运算符的代码如下:
const code8 = 'if (a==2) a+=1;';
const ast8 = babelParser.parse(code6, )
traverse2.default(ast8,
enter(path)
if (t.isBinaryExpression(path.node,operator:"=="))
path.node.operator = "===";
);
AST生成代码
下面我们的高光时刻来了,直接生成代码。babel为我们准备了"@babel/generator"包:
const generate = require("@babel/generator") ;
let c2 = generate.default(ast8,);
console.log(c2.code);
生成的代码如下:
if (a === 2)
a += 1;
;
代码模板
我们要生成的代码都通过AST表达式来写有时候有点反人性,这时候我们可以尝试下代码模板。
我们来看个例子:
const babelTemplate = require("@babel/template");
const requireTemplate = babelTemplate.default(`
const IMPORT_NAME = require(SOURCE);
`);
const ast9 = requireTemplate(
IMPORT_NAME: t.identifier("babelTemplate"),
SOURCE: t.stringLiteral("@babel/template")
);
console.log(ast9);
请注意,通过代码模板生成的直接就是AST哈,做的可不是模板字符串替换,替换的可是标识符和文本字面常量。
输出结果如下:
type: 'VariableDeclaration',
kind: 'const',
declarations: [
type: 'VariableDeclarator',
id: [Object],
init: [Object],
loc: undefined
],
loc: undefined
想要转成源代码,还需要调用generate包:
console.log(generate.default(ast9).code);
输出如下:
const babelTemplate = require("@babel/template");
另外,需要注意的是,我们的代码模板生成的是抽象语法树,不是具体语法树,比如我们在代码模板里写了注释,最后生成回代码里可就没有了:
const forTemplate = babelTemplate.default(`
for(let i=0;i<END;i+=1)
console.log(i); // output loop variable
`);
const ast10 = forTemplate(
END: t.numericLiteral(10)
);
console.log(generate.default(ast10).code);
生成的代码如下:
for (let i = 0; i < 10; i += 1)
console.log(i);
Babel高级操作
Babel转码器
既然有了babel,我们只用其parser有点浪费了,我们可以在我们的代码中使用babel来作为转码器:
const code2 = 'function greet(input) return input ?? "Hello world";';
const babel = require("@babel/core");
let result = babel.transformSync(code2,
targets: ">0.5%",
presets: ["@babel/preset-env"]);
console.log(result.code);
记得安装@babel/core和@babel/preset-env。
结果如下:
"use strict";
function greet(input)
return input !== null && input !== void 0 ? input : "Hello world";
我们再来个ES 6 Class转换的例子:
const code3 = `
//Test Class Function
class Test
constructor()
this.x = 2;
`;
const babel = require("@babel/core");
let result = babel.transformSync(code3,
presets: ["@babel/preset-env"]
);
console.log(result.code);
除了presets: ["@babel/preset-env"]需要指定外,其它用缺省的参数就好。
生成的代码如下:
"use strict";
function _classCallCheck(instance, Constructor) if (!(instance instanceof Constructor)) throw new TypeError("Cannot call a class as a function");
//Test Class Function
var Test = function Test()
_classCallCheck(this, Test);
this.x = 2;
;
在eslint规则中,如果源代码没有转码器,我们就可以利用babel直接转码生成autofix.
AST节点的替换
前面我们只修改了二元表达式中的运算符,不过这样的情况在实际中很少见。实际情况中我们经常要修改一大段表达式。这时候我们可以用replaceWith函数将旧的AST节点换成新的AST节点。
还以将"“换成”="为例,这次我们改成直接生成一个新的binaryExpression来替换原有的,表达式中的左右节点都不变:
const babel = require("@babel/core");
const babelParser = require('@babel/parser');
const t = require('@babel/types');
const traverse = require("@babel/traverse");
const generate = require("@babel/generator");
const code8 = 'if (a==2) a+=1; if (a!=0) a=0';
const ast8 操作JavaScript的AST
逆向进阶,利用 AST 技术还原 JavaScript 混淆代码
就像我们在 babelTypes 中一样,将 JavaScript 代码转换为 AST 表示