操作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 表示

如何将自定义 AST 转换为 JS 代码

JavaScript的工作原理:解析抽象语法树(AST)+ 提升编译速度5个技巧

ast入门