AST-web端javascript逆向杀器之常用API

来了,前面一篇文章的相关api,常用的使用方法和参数来了。

 

本文转自:点我

 

@babel/parse

解析函数

babelParser.parse(code, [options])

将提供的代码作为一个完整的ECMAScript程序进行解析

babelParser.parseExpression(code, [options])

用于解析单个Expression,性能比parse()要高

options 函数参数

  • allowImportExportEverywhere
    默认情况下,import 和 export 声明语句只能出现在程序的最顶层
    把这个设置为true,可以使得语句在任何地方都可以声明

  • allowAwaitOutsideFunction
    默认情况下,仅在 异步函数内部 或 启用topLevelAwait插件时 在模块的顶层内允许使用await
    把这个设置为true,可以使得语句在任何地方都可以声明

  • allowReturnOutsideFunction
    默认情况下,如果在顶层中使用return语句会引起错误
    把这个设置为true,就不会报错

  • allowSuperOutsideMethod
    默认情况下,在类和对象方法之外不允许使用super
    把这个设置为true就可以声明

  • allowUndeclaredExports
    默认情况下,export一个在当前作用域下未声明的内容会报错
    把这个设置为true就可以防止解析器过早地抛出未声明的错误

  • createParenthesizedExpressions
    默认情况下,parser会在expression节点设置extra.parenthesized
    把这个设置为true,则会设置ParenthesizedExpressionAST节点

  • errorRecovery
    默认情况下,如果Babel发现一些 不正常的代码 就会抛出错误
    把这个设置为true,则会在保存解析错误的同时继续解析代码,错误的记录将被保存在 最终生成的AST的errors属性中
    注意,那些严重的错误依然会终止解析

  • plugins
    记录希望启动的插件的数组

  • sourceType
    代码的解析方式,你可以填入"script"(默认),"module" 或 "unambiguous"
    如果设置为”unambiguous”,那么系统会根据ES6语法中的importsexport来判断是"module"还是"script"

  • sourceFilename
    将输出的AST节点与其源文件名相关联
    在你处理多个文件时,这个功能会很有用

  • startLine
    默认情况下,第一行代码就是line 1。你可以传入一个数字,作为起始行数
    这个功能在你整合其他插件的时候会很有用

  • strictMode
    默认情况下,只有在声明了”use strict”条件下,ECMAScript代码才会被严格解析
    将此选项设置为true则始终以严格模式解析文件

  • ranges
    添加ranges属性到每一个节点中

    ranges: [node.start, node.end]

  • tokens
    将所有已经解析的tokens保存到File节点的tokens属性中

输出 Output

Babel parser是根据 Babel AST format 创建AST的
Babel AST format是基于 ESTree 规范 建立的

ESTree 代码生成对应节点文档
Babel parser 代码生成对应节点文档

Babel parserESTree的不同之处

@babel/generator

官方文档:https://babeljs.io/docs/en/babel-generator

generate(ast, options, code);

函数用于根据ast生成代码,可以传入一些参数

options 参数

name 参数名type 类型default 默认值description 描述
auxiliaryCommentAfter string   Optional 在输出文件内容末尾添加的注释块文字
auxiliaryCommentBefore string   Optional 在输出文件内容头部添加的注释块文字
comments boolean true 输出内容是否包含注释
compact boolean or 'auto' opts.minified 是否不添加空格来让代码看起来紧密
concise boolean false 是否减少空格来让代码看起来紧凑一些
只是减少空格,而不是不添加
decoratorsBeforeExport boolean   是否在导出之前print一下装饰器
filename string   Used in warning messages
jsescOption object   Use jsesc to process literals. jsesc is applied to numbers only if jsescOption.numbers (added in v7.9.0) is present. You can customize jsesc by passing options to it.
jsonCompatibleStrings boolean false Set to true to run jsesc with “json”: true to print “\u00A9” vs. “©”;
minified boolean false Should the output be minified
是否压缩代码
retainFunctionParens boolean false Retain parens around function expressions (could be used to change engine parsing behavior)
retainLines boolean false 尝试在输出代码中使用与源代码中相同的行号(用于追踪堆栈)
shouldPrintComment function opts.comments Function that takes a comment (as a string) and returns true if the comment should be included in the output.
By default, comments are included if opts.comments is true or if opts.minified is false and the comment contains @preserve or @license

@babel/traverse

index

 

NodePath基础属性

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f(){
    var b = 123;
    a = ['a', 'b'];
}`;

const visitor = {
    BlockStatement(path)
    {
        console.log('当前路径 源码:\n', path.toString());
        console.log('当前路径 节点:\n', path.node)
        console.log('当前路径 父级节点:\n', path.parent);
        console.log('当前路径 父级路径:\n', path.parentPath)
        console.log('当前路径 类型:\n', path.type)

        console.log('当前路径 contexts:\n', path.contexts);
        console.log('当前路径 hub:\n', path.hub);
        console.log('当前路径 state:\n', path.state);
        console.log('当前路径 opts:\n', path.opts)
        console.log('当前路径 skipKeys:\n', path.skipKeys)
        console.log('当前路径 container:\n', path.container)
        console.log('当前路径 key:\n', path.key)
        console.log('当前路径 scope:\n', path.scope)
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  

你会发现其中有不少值都是没有定义的,这是因为很多值都是懒加载的
而且会给与专门的方法进行获取,并不是这样直接获取的

ancestry

父级/祖先相关

 

NodePath.findParent(callback)

@return NodePath | None
逐级递归寻找父级节点的Path,并将Path作为参数传入的判断函数进行判断
当判断函数返回true, 则Path.findParent(callback)返回对应Path
当判断函数返回false, 则递归继续寻找父级, 进行判断。若已无父级,则返回null

例: 寻找当前Path的父级函数节点

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());
        
        // 寻找父级
        function to_parent_function_path(x){  // 进行判断是否是函数声明节点的判断函数
            if(x.isFunctionDeclaration()){return true}else{return false}
        }
        //      将判断函数传入,进行递归寻找父级path
        the_path = path.findParent(to_parent_function_path)
        console.log('to_parent_function_path 最终路径源码:\n', the_path.toString())

        //      递归后如果没有发现符合要求的父级
        function to_null(x){return false}
        the_path = path.findParent(to_null)
        console.log('to_null 最终路径:\n', the_path)
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  

得到的输出结果

 

 

当前路径源码:
 a = b + 1
to_parent_function_path 最终路径源码:
 function f() {
  var b = 123;
  a = b + 1;
}
to_null 最终路径:
 null

  

 

 

NodePath.find(callback)

@return NodePath | None
此函数与 Path.findParent 基本相同, 但这个判断包含对 当前Path 的判断
它会先对 当前Path 进行一次判断. 如果自身符合条件,那就返回 当前Path,然后才递归调用父级进行判断

例子:当前或父级Path

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());
        
        function to_path(x){  
            if(x.isAssignmentExpression()){return true}else{return false}
        }
        the_path = path.find(to_path)
        console.log('to_path最终路径源码:\n', the_path.toString())

        // 寻找父级
        function to_parent_function_path(x){  // 进行判断是否是函数声明节点的判断函数
            if(x.isFunctionDeclaration()){return true}else{return false}
        }
        the_path = path.find(to_parent_function_path)
        console.log('to_parent_function_path最终路径源码:\n', the_path.toString())
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  得到的输出结果

 

当前路径源码:
 a = b + 1
to_path最终路径源码:
 a = b + 1
to_parent_function_path最终路径源码:
 function f() {
  var b = 123;
  a = b + 1;
}

  

NodePath.getFunctionParent()

@return NodePath | None
得到当前节点的第一个 父级/祖先 函数声明节点的Path

此方法通过调用 Path.findParent(callback) 传入内置的判断函数,来得到对应的结果

例: 寻找 父级/祖先 函数声明节点的Path

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());

        the_path = path.getFunctionParent()
        console.log('最终路径源码:\n', the_path.toString())
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  

得到对应结果

当前路径源码:
 a = b + 1
最终路径源码:
 function f() {
  var b = 123;
  a = b + 1;
}

  

NodePath.getStatementParent()

@return NodePath
返回第一个 父级/祖先 声明节点的Path
声明节点所包含的节点类型见:Github文档
若找不到目标,会报错

例:返回第一个 父级/祖先 声明节点的 Path

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f2(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    BinaryExpression(path){
        console.log('当前路径源码:\n', path.toString());
        the_path = path.getStatementParent()
        console.log('最终路径源码:\n', the_path.toString())
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  得到的结果

当前路径源码:
 b + 1
最终路径源码:
 a = b + 1;

  

NodePath.getAncestry()

@return Array
返回所有 父级/祖先 的Path

例:得到当前Path的所有 父级/祖先 的Path

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f2(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());
        the_paths = path.getAncestry()

        console.log('返回类型:\n', the_paths instanceof Array)
        console.log('结果路径源码:\n', the_paths.join('\n\n'))
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  

 

结果:

当前路径源码:
 a = b + 1
返回类型:
 true
结果路径源码:
 a = b + 1

a = b + 1;

{
  var b = 123;
  a = b + 1;
}

function f2() {
  var b = 123;
  a = b + 1;
}

function f2() {
  var b = 123;
  a = b + 1;
}

  

NodePath.isDescendant(path)

@return bool
判断当前 Path 是否是指定 Path 的后代

此方法通过调用 Path.findParent() 来进行判断,得到结果

例:辈分判断

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f2(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());
        console.log('儿子是爸爸的后代:', path.isDescendant(path.parentPath))
        console.log('儿子是爷爷的后代:', path.isDescendant(path.parentPath.parentPath))
        console.log('儿子是孙子的后代:', path.isDescendant(path.get('left')))
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  结果:

当前路径源码:
 a = b + 1
儿子是爸爸的后代: true
儿子是爷爷的后代: true
儿子是孙子的后代: false

  

NodePath.isAncestor(path)

@return bool
判断当前 Path 是否是指定 Path 的后代

此方法是调用 传入的path的Path.isDescendant() 来进行判断的

例:判断是否是后代

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f2(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());
        console.log('儿子是爸爸的祖先:', path.isAncestor(path.parentPath))
        console.log('儿子是爷爷的祖先:', path.isAncestor(path.parentPath.parentPath))
        console.log('儿子是孙子的祖先:', path.isAncestor(path.get('left')))
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  结果:

当前路径源码:
 a = b + 1
儿子是爸爸的祖先: false
儿子是爷爷的祖先: false
儿子是孙子的祖先: true

  

NodePath.inType(**NodeType_str)

@return bool
判断当前Path对应节点,或其 父/祖先 节点 是否包含特定类型的节点
可以一次性传入多个类型,只要有一个符合就会返回 true, 否则返回 false

例: 是否包含特定类型的节点

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f2(){
    var b = 123;
    a = b + 1;
}`;

const visitor = {
    AssignmentExpression(path){
        console.log('当前路径源码:\n', path.toString());
        _is = path.inType('FunctionDeclaration')
        console.log('父级或自身包含函数声明节点:', _is);
        _is = path.inType('WithStatement', 'DebuggerStatement')
        console.log('父级或自身包含 with 或 debugger:', path.inType(_is));
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  结果:

当前路径源码:
 a = b + 1
父级或自身包含函数声明节点: true
父级或自身包含 with 或 debugger: false

  

NodePath.getDeepestCommonAncestorFrom(paths, filter)

@return NodePath | 自定义
获取传入的Path对应节点的 最大深度共同祖先节点的Path

  • 当 paths不存在length属性时,报错
  • 当 paths 长度为0时,返回 null
  • 当 paths 长度为1时,返回唯一的Path
  • 当 paths 大于1如果并不存在共同的祖先节点,报错
    • 计算 最大深度共同祖先节点 的Path并返回
    • 当传入一个filter函数,那么返回结果会作为参数进行回调。返回结果变为filter(最大深度共同祖先节点Path:NodePath, 深度:int, 所有path的祖先信息:list);

例:最大深度的共同祖先节点

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f(){
    function f3(){
        function f1(){return 1;}
        function f2(){return 2;}
        return 3;
    }
}`;


let paths = []

const visitor = {
    ReturnStatement(path){
        console.log('路径源码:\n', path.toString());
        paths.push(path)
        if (paths.length > 1){
            _is = path.getDeepestCommonAncestorFrom(paths)
            console.log('最大深度的共同祖先节点 源代码:', _is.toString());
        }
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  

结果:

路径源码:
 return 1;
路径源码:
 return 2;
最大深度的共同祖先节点 源代码: {
  function f1() {
    return 1;
  }

  function f2() {
    return 2;
  }

  return 3;
}
路径源码:
 return 3;
最大深度的共同祖先节点 源代码: {
  function f1() {
    return 1;
  }

  function f2() {
    return 2;
  }

  return 3;
}

  

NodePath.getEarliestCommonAncestorFrom(paths)

@return NodePath
获取paths中最早出现的共同祖先
方法会遍历计算,共同祖先一旦出现, 则返回,不再继续计算所有的path

此方法是调用 getDeepestCommonAncestorFrom(paths, filter) 方法,传入固定的filter函数来实现

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

var jscode = `
function f(){
    function f3(){
        function f1(){return 1;}
        function f2(){return 2;}
        return 3;
    }
}`;


let paths = []

const visitor = {
    ReturnStatement(path){
        console.log('路径源码:\n', path.toString());
        paths.push(path)
        if (paths.length > 1){
            _is = path.getEarliestCommonAncestorFrom(paths)
            console.log('最早的共同祖先节点 源代码:', _is.toString());
        }
    }
}

let ast = parser.parse(jscode);
traverse(ast, visitor);

  

family

主要用于获取同级/前后 NodePath

 

NodePath.getSibling(key)

@return NodePath

通过父级,获取同级节点的 NodePath 或 其它内容

  • 如果传入数字,则尝试获取 同级节点 指定位置的 NodePath

  • 如果传入数字,则尝试获取 父级节点 指定位置的 NodePath

  • 也可以传入一些特殊的key, 获取一些特殊的内容。

    可以使用 NodePath.listKey属性 查看可以获取的key

例: 寻找其它内容

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
function x(){
  console.log('code 1');
  console.log('code 2');
  var a = 1;
  console.log('code 3');
  console.log('code 4');
}
`;

const ast = parser.parse(jscode);
const visitor = {
  VariableDeclaration(path) {
    console.log('当前节点源码:\n', path.toString());
    console.log('---------------------------------------------');
    console.log('第1个兄弟的源码', path.getSibling(0).toString());
    console.log('第2个兄弟的源码', path.getSibling(1).toString());
    console.log('第3个兄弟的源码', path.getSibling(2).toString());
    console.log('第4个兄弟的源码', path.getSibling(3).toString());
    console.log('第5个兄弟的源码', path.getSibling(4).toString());
    console.log(path.listKey)
    console.log('---------------------------------------------');
  }
}
traverse(ast, visitor);

  结果:

当前节点源码:
 var a = 1;
---------------------------------------------
第1个兄弟的源码 console.log('code 1');
第2个兄弟的源码 console.log('code 2');
第3个兄弟的源码 var a = 1;
第4个兄弟的源码 console.log('code 3');
第5个兄弟的源码 console.log('code 4');
body
---------------------------------------------

  

NodePath.getOpposite()

@return Node

获取相对的对位节点 (left 与 right)

此函数通过调用 NodePath.getSibling(key) , 传入 当前节点的 left 或 right key实现

例:获取对位节点

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
  var a = 1 + 9;
`;

const ast = parser.parse(jscode);
const visitor = {
  NumericLiteral(path) {
    console.log('当前节点源码:\n', path.toString())
    console.log('对应节点源码:\n', path.getOpposite().toString())
    console.log('----------------')
  }
}

traverse(ast, visitor);

  结果:

当前节点源码:
 1
对应节点源码:
 9
----------------
当前节点源码:
 9
对应节点源码:
 1
----------------

  

NodePath.getPrevSibling()

@return Node

获取同级前一个节点的 NodePath

此函数源码就一句 return this.getSibling(this.key - 1);

const parser = require(js_env + "@babel/parser");
const traverse = require(js_env + "@babel/traverse").default;

const jscode = `
 var a = 1 + 9;
 a = a + a;
 console.log(a);
 console.log(b);
`;

const ast = parser.parse(jscode);
const visitor = {
 ExpressionStatement(path) {
  console.log('当前节点源码:\n', path.toString())
  console.log('同级前一个节点源码:\n', path.getPrevSibling().toString())
  console.log('----------------')
 }
}

traverse(ast, visitor);

  结果:

当前节点源码:
 a = a + a;
同级前一个节点源码:
 var a = 1 + 9;
----------------
当前节点源码:
 console.log(a);
同级前一个节点源码:
 a = a + a;
----------------
当前节点源码:
 console.log(b);
同级前一个节点源码:
 console.log(a);
----------------

  

NodePath.getNextSibling()

@return Node

获取同级后一个节点的 NodePath

此函数源码就一句 return this.getSibling(this.key + 1);

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
  var a = 1 + 9;
  a = a + a;
  console.log(a);
  console.log(b);
`;

const ast = parser.parse(jscode);
const visitor = {
  ExpressionStatement(path) {
    console.log('当前节点源码:\n', path.toString())
    console.log('同级后一个节点源码:\n', path.getNextSibling().toString())
    console.log('----------------')
  }
}

traverse(ast, visitor);

  


输出结果:

当前节点源码:
 a = a + a;
同级前一个节点源码:
 console.log(a);
----------------
当前节点源码:
 console.log(a);
同级前一个节点源码:
 console.log(b);
----------------
当前节点源码:
 console.log(b);
同级前一个节点源码:
 
----------------

  

NodePath.getAllPrevSiblings()

@return Array

获取当前节点前的兄弟节点的 NodePath,结果存放在一个数组中返回

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
  var a = 1 + 9;
  a = a + a;
  console.log(a);
  console.log(b);
`;

const ast = parser.parse(jscode);
const visitor = {
  ExpressionStatement(path) {
    console.log('当前节点源码:\n', path.toString())
    const pre_nodepath = path.getAllPrevSiblings()
    console.log('前面的兄弟节点源码:')
    for(var nodepath of pre_nodepath){
      console.log(nodepath.toString())
    }
    console.log('----------------')
  }
}

traverse(ast, visitor);

  

输出结果:

当前节点源码:
 a = a + a;
前面的兄弟节点源码:
var a = 1 + 9;
----------------
当前节点源码:
 console.log(a);
前面的兄弟节点源码:
a = a + a;
var a = 1 + 9;
----------------
当前节点源码:
 console.log(b);
前面的兄弟节点源码:
console.log(a);
a = a + a;
var a = 1 + 9;
----------------

  

NodePath.get(key, context)

@return NodePath

用于获取子孙节点

如果不传入 context 参数, 则以当前 path 对应节点为起点

如果传入,则以传入的 path 对应节点为起点

如果想要获取更多层级的子孙,可以用’.’隔开进行获取

  • 获取某个单个属性节点 .名字

  • 获取某个节点的第 x 个节点 .x

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `function square(n) {
  var a = 1;
  return 1 + 1;
}`;

const ast = parser.parse(jscode);
const visitor = {
  FunctionDeclaration(path) {  // 找到变量声明节点,删除
      var p1 = path.get('body')
      console.log('body 子节点源码:\n', p1.toString())
      var p2 = path.get('body.body.0')
      console.log('body.body.0 子节点源码:\n', p2.toString())
  }
}

traverse(ast, visitor);

  


输出结果:

body 子节点源码:
 {
  var a = 1;
  return 1 + 1;
}
body.body.0 子节点源码:
 var a = 1;

  


removal

移除相关

NodePath.remove()

@return null
删除路径对应的节点

删除以后,对应的removed标识为会被设定,内容会被设定为只读
如果再次执行remove方法,则会报错

例:删除 path 对应的节点

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

const jscode = `function square(n) {
  var a = 1;
  return 1 + 1;
}`;

const ast = parser.parse(jscode);
const visitor = {
  VariableDeclaration(path){  // 找到变量声明节点,删除
      path.remove()
  }
}

traverse(ast, visitor);
console.log(generator(ast)['code'])

  


得到结果:

function square(n) {
  return 1 + 1;
}

  

scope

此模块与作用域相关

Scope

和 作用域 相关的内容被定义在了 Scope类 中
这个类定义位于 @babel/traverse/lib/scope/index.js 文件中

Scope属性

例:输出一些属性,一般不会直接使用,但可以留个印象,后面的函数可能会使用属性

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function squire(i){
    return i * g * i;
}
function i()
{
    var i = 123;
    i += 2;
    return 123;
}
`;
let ast = parser.parse(jscode);
const visitor = {
    VariableDeclaration(path){
        console.log("\n这里是", path.toString())
        console.log('--------------------------------')
        sc = path.scope  // 获取对应的 Scope对象
        console.log('这个对象是否已经初始化:', sc.inited)
        console.log('uid 属性', sc.uid)
        console.log('cached 属性', sc.cached)
        console.log('node 属性', sc.node)
        console.log('作用域节点:', sc.block)
        console.log('作用对应的path:', sc.path.node == sc.block)
        console.log('labels 属性', sc.labels)
        console.log('被绑定量 的信息:', sc.bindings)
        console.log('--------------------------------')
    }
}

traverse(ast, visitor);

  


你能够直接访问Scope对象的属性,它本身也提供了一些方法来访问

Scope方法

Scope.parent

@return Scope | undefined

获取当前作用域的父级作用域

此方法通过引用其 Scope.path 属性的 PathNode.findParent()方法 获取对应PathNode后再次获取作用域的方式获取

例:获取父级作用域

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function squire(i){
    return i * g * i;
}
function i()
{
    var i = 123;
    i += 2;
    return 123;
}
`;
let ast = parser.parse(jscode);
const visitor = {
    VariableDeclaration(path){
        console.log("\n这里是", path.toString())
        console.log('--------------------------------')
        sc = path.scope  // 获取对应的 Scope对象
        console.log('parent结果:', sc.parent)
    }
}

traverse(ast, visitor);

  


结果

Scope.dump()

return null

输出到自底向上的 作用域与被绑定量的信息

执行后会得到类似于这样的输出信息

 

# FunctionDeclaration
 - i { constant: false, references: 0, violations: 1, kind: 'var' }
# Program
 - squire { constant: true, references: 0, violations: 0, kind: 'hoisted' }
 - i { constant: true, references: 0, violations: 0, kind: 'hoisted' }

  


作用域 以#划分,此处有两个作用域 FunctionDeclaration 与 Program

被绑定量 以最前方设置-来标识,一般显示其中的4种信息

  • constant
    量 在声明后,在作用域内是否为常量
    实际上对应对应量的 Binding 对象的Binding.constant属性
  • references
    被引用次数
    实际上对应对应量的 Binding 对象的Binding.references属性
  • violations
    量 被 重新定义/赋值 的次数
    实际上对应对应量的 Binding 对象的Binding.constantViolations的长度。这个属性被用于记录变更位置(每次变更都添加内容)
  • kind
    函数声明类型。常见的有:hoisted提升,var变量, local内部
    实际上对应对应量的 Binding 对象的Binding.kind属性

实际上这些信息大部分 (以一个被绑定量,一个 Binding 对象的方式)储存在 Scope.bindings 这个属性中

例:使用案例

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
function squire(i){
    return i * i * i;
}
function i(){
    var i = 123;
    i += 2;
    return 123;
}
`;
let ast = parser.parse(jscode);
const visitor = {
    "FunctionDeclaration"(path){
        console.log("\n\n这里是函数 ", path.node.id.name + '()')
        path.scope.dump();
    }
}

traverse(ast, visitor);

  


得到结果:

 

这里是函数  squire()
------------------------------------------------------------
# FunctionDeclaration
 - i { constant: true, references: 3, violations: 0, kind: 'param' }
# Program
 - squire { constant: true, references: 0, violations: 0, kind: 'hoisted' }
 - i { constant: true, references: 0, violations: 0, kind: 'hoisted' }
------------------------------------------------------------


这里是函数  i()
------------------------------------------------------------
# FunctionDeclaration
 - i { constant: false, references: 0, violations: 1, kind: 'var' }
# Program
 - squire { constant: true, references: 0, violations: 0, kind: 'hoisted' }
 - i { constant: true, references: 0, violations: 0, kind: 'hoisted' }
------------------------------------------------------------

  

Scope.parentBlock(name)

@return Node

获取 作用域路径 的父级
它的源码就一句 return this.path.parent;

例: 获取作用域路径的父级

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){return g;}
function b(){var z=2; return z;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        var n = path.node.argument.name
        console.log("\n这里是", path.toString())
        console.log('结果:', path.scope.parentBlock)
    }
}

traverse(ast, visitor);

  


结果

 

Scope.getBinding(name)

@return Binding

获取指定 被绑定量 的 Binding对象

如果在 当前作用域 找不到指定的 被绑定量,那么就会递归在父级作用域中寻找

例:获取指定的 被绑定量对象

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){return g;}
function b(){var z=2; return z;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        var n = path.node.argument.name
        console.log("\n这里是", path.toString())
        console.log('被绑定量:', path.scope.getBinding(n))
    }
}

traverse(ast, visitor);

  

Scope.getOwnBinding(name)

@return Binding

传入一个名称,从当前的 作用域 中拿到指定的 被绑定量对象Binding
实际上方法的源码就一句return this.bindings[name];

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){var a=1;return g;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        var n = path.node.argument.name
        console.log("\n这里是", path.toString())
        console.log('获取挡墙定义域里 a的Binding,结果:', path.scope.bindings['a'])
    }
}

traverse(ast, visitor);

  

Scope.getBindingIdentifier(name)

@return Node | void 0
获取指定的 Binding 的定义节点Node

方法作用域获取 Binding ,再通过这个 Binding 获取其定义的节点

这个方法通过 Scope.getBinding(name) 方法获取 Binding ,所以会存在递归向上的情况

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){return g;}
function b(){var z=2; return z;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        var n = path.node.argument.name
        console.log("\n这里是", path.toString())
        console.log(n, '的定义:', path.scope.getBindingIdentifier(n))
    }
}

traverse(ast, visitor);

  


Scope.getOwnBindingIdentifier(name)

@return Node|void 0
获取指定的 Binding ,并通过这个 Binding 获取其定义的节点
这个方法只关注 当前作用域,并不会向上寻找

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){return g;}
function b(){var z=2; return z;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        var n = path.node.argument.name
        console.log("\n这里是", path.toString())
        console.log(n, '的定义:', path.scope.getOwnBindingIdentifier(n))
    }
}

traverse(ast, visitor);

  


得到结果:

这里是 return g;
g 的定义: undefined

这里是 return z;
z 的定义: Node {
  type: 'Identifier',
  start: 53,
  end: 54,
  loc: SourceLocation {
    start: Position { line: 4, column: 17 },
    end: Position { line: 4, column: 18 },
    identifierName: 'z'
  },
  name: 'z'
}

  

Scope.hasOwnBinding(name)

@return bool

获知当前作用域是否有某个被绑定变量得到结果:

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){return g;}
function b(){var z=2; return z;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        var n = path.node.argument.name
        console.log("\n这里是", path.toString())
        console.log('当前作用域有 被绑定变量 z:', path.scope.hasOwnBinding('z'))
        console.log('当前作用域有 被绑定变量 g:', path.scope.hasOwnBinding('g'))
    }
}

traverse(ast, visitor);

  

这里是 return g;
当前作用域有 被绑定变量 z: false
当前作用域有 被绑定变量 g: false

这里是 return z;
当前作用域有 被绑定变量 z: true
当前作用域有 被绑定变量 g: false

  


两个函数的作用域内都不会有g的绑定,因为它被绑定在更上级作用域中

Scope.hasBinding(name, noGlobals)

@return bool
向上递归作用域,获知是否有某个被绑定变量

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
var g = 1;
function a(){return g;}
function b(){var z=2; return z;}
`;
let ast = parser.parse(jscode);
const visitor = {
    ReturnStatement(path){
        console.log("\n这里是", path.toString())
        console.log('作用域有 被绑定变量 z:', path.scope.hasBinding('z'))
        console.log('作用域有 被绑定变量 g:', path.scope.hasBinding('g'))
    }
}

traverse(ast, visitor);

  

 

Binding

Binding 对象用于存储 被绑定在作用域的量 的信息
你可以在 @babel/traverse/lib/scope/binding.js 查看到它的定义

 

Binding属性

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
function a(){
    var a = 1;
    a = a + 1;
    return a;
}
function b(){
    var b = 1;
    var c = 2;
    b = b - c;
    return b;
}
`;
let ast = parser.parse(jscode);
const visitor = {
    BlockStatement(path){
        console.log("\n此块节点源码:\n", path.toString())
        console.log('----------------------------------------')
        var bindings = path.scope.bindings
        console.log('作用域内 被绑定量 数量:', Object.keys(bindings).length)

        for(var binding_ in bindings){
            console.log('名字:', binding_)
            binding_ = bindings[binding_];
            console.log('类型:', binding_.kind)
            console.log('定义:', binding_.identifier)
            console.log('是否为常量:', binding_.constant)
            console.log('被修改信息信息记录', binding_.constantViolations)
            console.log('是否会被引用:', binding_.referenced)
            console.log('被引用次数', binding_.references)
            console.log('被引用信息NodePath记录', binding_.referencePaths)
        }
    }
}

traverse(ast, visitor);

  

comments

注释相关

NodePath.addComment(type, content, line)

@return None

添加注释

实际上只是调用types.addComment 的方法而已

参数:

  • type str 指定添加的注释方式,如果填入"leading",则添加的注释会插入已有注释之前,否则在原有注释之后

  • content str 注释内容

  • line bool 插入行注释还是块注释

例:插入注释

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

const jscode = `
  var a = 1 + 9;
`;

const ast = parser.parse(jscode);
const visitor = {
  NumericLiteral(path) {
    console.log('当前节点源码:\n', path.toString());
    path.addComment('trailing', "注释", false);
  }
}

traverse(ast, visitor);
console.log(generator(ast)['code'])

  

@babel/types

utils

 

Types.shallowEqual(actual, expected)

@return bool

对比函数,expected传入一个字典进行 keyvalue 遍历

获取 actual.key 的值与 value 进行对比

如果有一个不一致,那么返回 false, 否则返回 true

其定义在: @babel/types/lib/validators/generated/index.js

例:判断节点的name是否为a

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");

const jscode = 'var a=1; var b=1+1;';
let ast = parser.parse(jscode);

const visitor = {
  enter(path){
    console.log('当前节点源码:', path.toString())
    console.log('其属性name为a:', t.shallowEqual(path.node, {'name':'a'}))
  }
}

traverse(ast, visitor);

  

Types.isNodeType(node, opts)validators

@return bool

这并不是一个函数,这是一大堆由生成代码生成的函数,大约有290个

这些函数定义在 @babel/types/lib/validators/generated/index.js

函数逻辑都是类似的

  1. if( 没有node ) return false

  2. if(node.type == 声明类型) return false

  3. else if( 没有opts ) return true

  4. else return types.shallowEqual(node, opts)

例: 判断节点是否符合判断

 

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");

const jscode = 'var a=1;var b=1+1;';
let ast = parser.parse(jscode);

const visitor = {
 enter(path){
  console.log('当前节点源码:', path.toString())
  console.log('是 Identifier', t.isIdentifier(path.node))
  console.log('是 Identifier 且其属性name为a:', t.isIdentifier(path.node, {'name':'a'}))
 }
}

traverse(ast, visitor);

  

 

posted @ 2021-01-03 16:54  Eeyhan  阅读(1187)  评论(0编辑  收藏  举报