二. Ast - 反混淆(基础篇-api的使用)

在线代码转AST语法树网站:AST explorer

什么是path对象

通过以下的代码,对以上图片中的AST语法树做例子。

  • VariableDeclarator(path) 是一个函数,表示 traverse 遍历AST时,要进入的节点
  • path 参数,表示当前正在遍历的节点路径,path 包含有关节点相关信息的对象。
  • 通过 path 对象,可以访问和操作节点的属性和关系,path 对象中又提供提供了很多内置方法供我们使用
  • 在代码中的这个例子中,当遍历到一个变量声明 VariableDeclarator 节点时,会调用 VariableDeclarator 函数,并传入该变量声明节点的路径作为参数。在函数中,我们可以通过 path 来访问该节点的信息。在代码中还可以通过 path.toString() 输出当前遍历到的节点的js代码
    // todo 编写ast插件
    const visitor = {
        VariableDeclarator(path){
            // console.log(path.toString()); // a = 1
            console.log(path)
           }    
    }
    
    traverse(ast,visitor)

输出后的path对象:

小结:

  • path 对象,表示当前正在遍历的节点路径,path 包含有关节点相关信息的对象。
  • path 对象是一个 NodePath 类型

path中的node

在AST中,path 代表的意思是路径的意思,而 node 表示的是节点,node 是被 path 对象包裹的节点,可以通过 path.node 得到当前插件的节点。node是path对象中的一个属性

举例1:在以下的语法树结构中,可以看到 a = 123; 的节点是 VariableDeclarator 来表示这个语句,在代码中可以编写 VariableDeclarator 节点的插件,然后通过 path.node 输出,查看 VariableDeclarator 包含的一些属性

在代码中可以在 visitor 对象中编写,VariableDeclarator节点的插件,然后通过 path.node 输出,可以看到输出的内容是当前 VariableDeclarator 节点中的内容。VariableDeclarator 就是被path对象包裹的一个 node 节点。

// todo 编写ast插件
const visitor = {
    VariableDeclarator(path){
        console.log(path.node)
        /*
        Node {
              type: 'VariableDeclarator',
              start: 4,
              end: 11,
              loc: SourceLocation {
                start: Position { line: 1, column: 4, index: 4 },
                end: Position { line: 1, column: 11, index: 11 },
                filename: undefined,
                identifierName: undefined
              },
              id: Node {
                type: 'Identifier',
                start: 4,
                end: 5,
                loc: SourceLocation {
                  start: [Position],
                  end: [Position],
                  filename: undefined,
                  identifierName: 'a'
                },
                name: 'a'
              },
              init: Node {
                type: 'NumericLiteral',
                start: 8,
                end: 11,
                loc: SourceLocation {
                  start: [Position],
                  end: [Position],
                  filename: undefined,
                  identifierName: undefined
                },
                extra: { rawValue: 123, raw: '123' },
                value: 123
              }
            }
        * */
    }
}

traverse(ast,visitor)

举例2:在以下的语法树结构中,可以看到 a = 123; 的节点是 VariableDeclarator 来表示这个语句,而VariableDeclarator节点中又包含,Identifier 节点和NumericLiteral节点,Identifier节点用于表示 a 变量名,而NumericLiteral表示 变量值 123。这两个节点可以称为 VariableDeclarator 节点中的子节点。

以下的代码中,当 traverse 遍历规则,遍历到 VariableDeclarator 节点时,就会从VariableDeclarator节点,向下遍历到VariableDeclarator节点中的子节点Identifier,然后在Identifier节点中输出 node 节点的属性

const updateParamsNameVisitor = {
    Identifier(path){
        console.log(path.node)
        /*
        Node {
          type: 'Identifier',
          start: 4,
          end: 5,
          loc: SourceLocation {
            start: Position { line: 1, column: 4, index: 4 },
            end: Position { line: 1, column: 5, index: 5 },
            filename: undefined,
            identifierName: 'a'
          },
          name: 'a'
        }
        * */
    }
}

// todo 编写ast插件
const visitor = {
    VariableDeclarator(path){
        // {} 为空表示不往遍历到的子节点中传输数据
        // {} 在不传输数据的时候也可以忽略
        path.traverse(updateParamsNameVisitor,{})
    }
}

traverse(ast,visitor)

小结:

  • path.node 可以得到当前节点中的node对象,这个对象和AST explorer 解析出来的结构一样
  • 通过 path.node 得到当前节点的node对象后,可以从中取出一些属性值
  • node 只是 path 对象中的一个属性
  • node是一个Node类型

path和node的区别

  • path表示当前正在遍历的节点路径,path包含有关节点相关信息的对象。
  • nodepath对象中的一个属性。
  • 区别在于 path NodePath类型,而node属性是Node类型
  • 区别在于 path 得到的是当前节点的对象 ,而node得到的是当前节点的对象
  • path对象是当前节点本身,node是当前节点的属性
  • path对象是node的父级
  • 注意点:
    • 通过 path.node.属性名称 得到的属性值不能使用path对象提供的方法 (后面会讲)

path对象中属性和方法

  • path.node 获取路径对应的节点
  • path.parent 获取当前路径对应节点的父节点
  • path.parentPath 获取当前路径对应节点的父路径
  • path.scope 表示当前path下的作用域,这个也是写插件经常用到的
  • path.container 用于获取当前path下的所有兄弟节点(包括自身)
  • path.type 获取当前path的节点类型
  • path.get("key") 获取当前path的key值,得到的是一个path对象,能够使用path对象的方法

1. 获取子节点中的属性

通过 path.node 得到的是当前节点中的node对象,node对象中包含一些属性,可以通过 属性名得到node对象中的属性值

代码:通过path.node 得到当前节点的node对象,node对象中又存在着idinit属性,idinit属性也是一个node对象,可以通过node.init的方式访问到init属性,然后取出init属性中对应的值.

const visitor = {
    VariableDeclarator(path){
        console.log(path.node.init.value) // 1
    }
}

traverse(ast,visitor)

需要注意的是,通过这样的方式得到的是属性值或者Node对象的方式,不能使用Path对象提供的方法

2. 获取子path

  • 通过 path中的get方法可以得到当前节点中的子级path,从而能够使用path对象的方法
    • 提示:如果不理解的话,可以理解为,得到的是node对象,但是能够使用path对象的方法
  • 语法格式:path.get("属性名")
    • 如果想要多级别访问:path.get("一级属性名.二级属性名")

示例:获取init的path对象

  • ast语法树:
  • 代码:
    // todo 编写ast插件
    const visitor = {
        VariableDeclarator(path){
            console.log(path.get("init"))
            console.log(path.get("id.type"))
        }
    }
    
    traverse(ast,visitor)
    
  • 输出内容:

3.  path和node转代码

path对象和node对象转换为js代码

  • path对象转代码:
    • path.toString()
    • 通过toString() 函数,将遍历的path节点路径,转换为javascript代码
  • node对象转代码:
    • generator(path.node).code 
    • 通过generator组件,将node对象转换为javascript代码

示例代码:

// todo 编写ast插件
const visitor = {
    VariableDeclarator(path){
        console.log(path.toString())
        console.log(generator(path.node).code)
    }
}

traverse(ast,visitor)

4. 判断path的类型

  • path对象提供对应的方法用来判断自身类型,使用方法和types组件差不多
  • path对象和types组件判断类型的区别:
    • path对象判断的是自身类型
    • types组件判断的是node类型

示例:

  • 抽象语法树:
  • 代码:
    // todo 编写ast插件
    const visitor = {
        VariableDeclarator(path){
            // 获取当前节点下的init属性path对象,并判断init的节点类型type类型是否为NumericLiteral
            console.log(path.get("init").isNumericLiteral()); // true
        }
    }
    
    traverse(ast,visitor)

5. 替换节点属性值

通过types组件将节点中的属性值替换为指定的属性值

示例:将当前VariableDeclarator节点下的init属性的属性值替换为 123123

  • 抽象语法树:
  • 代码:
    // todo 编写ast插件
    const visitor = {
        VariableDeclarator(path){
            path.node.init = types.numericLiteral(123123)
        }
    }
    
    traverse(ast,visitor)

关于valueToNode

在babel中,不同的字面量需要调用不同的方法去生成,当遇到比较多的字面量时,就会很麻烦。

在babel中,为了避免这样的问题,提供了valueToNode方法。会根据传入的数据的数据类型自动转换节点类型

6. 替换整个节点

Path对象中替换相关的方法: 

  • replaceWith
  • replaceWithMultiple
  • replaceInline
  • replaceWithSourceString

replaceWith,该方法是节点替换节点,并且是严格的一换一,替换的内容是遍历到的节点中的属性值

例如:将变量值全部修改为123321

// todo 编写ast插件
const visitor = {
    NumericLiteral(path){
        path.replaceWith(valueToNode("123321"))
    }
}

/*
* 替换后的代码:
    var a = "123321";
    var b = "123321";
* */
traverse(ast,visitor)


replaceWithMultiple,该方法是节点替换节点,是多个节点还一个节点。

例如,返回语句是返回一条,但是可以通过replaceWithMultiple方法替换为指定的多条返回语句

例如:将函数内容替换为返回多条语句

// todo 编写ast插件
const visitor = {
    ReturnStatement(path){
        path.replaceWithMultiple([
            types.returnStatement(),
            types.expressionStatement(types.stringLiteral("mankvis")),
            types.expressionStatement(types.numericLiteral(10)),
        ]);
        path.stop();
    }
}
/*
* 替换结果:
function add() {
  return;
  "mankvis";
  10;
}
* */
traverse(ast,visitor)

上述代码中有两处要特别说明:

  • 当表达式语句单独在一行时(没有赋值),最好用 expressionStatement 包裹;替换后的节点,traverse 也是能遍历到的,因此替换时要极其小心,否则容易造成不合理的递归调用。
  • 例如上述代码,把 return 语句进行替换,但是替换里面又有 return 语句,就会陷入死循环,解决方法是加入 path.stop,替换完成之后立刻停止遍历当前节点和后续的子节点


replaceInline,该方法是replaceWithMultiple和replaceWith的结合,会根据传入的参数决定使用那个方法。

  • 如果传入的是一个参数,那么replaceInline就等同于replaceWith
  • 如果传入的参数是一个数组,那么replaceInline就等同于replaceWithMultiple

例如:传入的参数是一个参数,将123替换为66666

// todo 编写ast插件
const visitor = {
    NumericLiteral(path){
        path.replaceInline(valueToNode("66666"))
    }
}
/*
* 替换结果:
var a = "66666";
* */
traverse(ast,visitor)


replaceWithSourceString,该方法是用字符串源码替换节点

例如:将123替换为一个函数

// todo 编写ast插件
const visitor = {
    NumericLiteral(path){
        path.replaceWithSourceString(`function add(a,b){return a + b}`)
    }
}
/*
* 替换结果:
var a = function add(a, b) {
  return a + b;
};
* */
traverse(ast,visitor)

7. 删除节点

如果想ast遍历到当前某个没有用的节点时候进行删除可以使用,path.remove() 方法

8. 插入节点

插入节点分为两种方式插入:

  • path.insertBefore 在节点前插入
  • path.insertAfter 在节点后插入

例如:在return语句的前面插入 "Before" 和return 语句后面插入 "After"

// todo 编写ast插件
const visitor = {
    ReturnStatement(path){
        path.insertBefore(valueToNode("Before"));
        path.insertAfter(valueToNode("After"));
    }
}
/*
* 替换结果:
function add() {
  "Before"
  return "hello world";
  "After"
}
* */
traverse(ast,visitor)

父级path

将代码 var a = 1; 转换为抽象语法树后,通过以下的代码输出path对象

// todo 编写ast插件
const visitor = {
    VariableDeclarator(path){
        console.log(path)
    }
}
traverse(ast,visitor)

在输出的内容中,可以看到path对象中存在两个属性,分别是parentPathparent

  • parentPathNodePath类型,而NodePath中又包含了Node,所以NodePath是父级path
  • parentNode类型,Node是父节点,也就是当前插件的节点。
    • 例如:visitor对象中的插件VariableDeclarator,就可以理解为Node父节点
  • 所以只要获取了父级path,就可以调用path对象的方法去操作父节点。

  • 通过path.parentPath方法可以获取到父级Path,就可以使用path对象提供的方法去对父节点进行一系列的操作
  • 父级path的父节点可以通过 path.parent 方法获取
  • 父级path的父节点也可以通过 path.parentPath.node 方法获取,因为父节点在父级path
    // todo 编写ast插件
    const visitor = {
        VariableDeclarator(path){
            console.log(path.parentPath.node)
            console.log("=========")
            console.log(path.parent)
        }
    }
    traverse(ast,visitor)

1. parentPath和parent的关系

  • path.parentPath.node 等价于 path.parent,parent是parentPath中的一部分
  • 通过以下的代码输出,可以看到得到的是相同的结果,所以印证了上面的话
    // todo 编写ast插件
    const visitor = {
        VariableDeclarator(path){
            console.log(path.parentPath.node)
            console.log("=========")
            console.log(path.parent)
        }
    }
    traverse(ast,visitor)

2. path.findParent()

需要从当前节点向上遍历语法树,直到满足响应的条件,可以使用path对象提供的findParent方法

示例:通过以下代码的输出可以看到,使用了findparent方法是从当前的Identifier节点向上遍历。通过这个方法可以在箭头函数中做一些逻辑判断,是否到达了指定的节点

// todo 编写ast插件
const visitor = {
    Identifier(path){
        path.findParent((p)=>{
            console.log(p.type)
            /*
            VariableDeclarator
            VariableDeclaration
            Program
            * */

            if(p.isVariableDeclarator()){
                console.log("到VariableDeclarator节点了")
            }
        })
    }
}
traverse(ast,visitor)

3. path.find()

path.find方法也是从当前节点向上查找,但是包含了当前的节点,可以看到输出的节点类型,包含了当前自身

// todo 编写ast插件
const visitor = {
    Identifier(path){
        path.find((p)=>{
            console.log(p.type)
            /*
            Identifier
            VariableDeclarator
            VariableDeclaration
            Program
            * */
        })
    }
}
traverse(ast,visitor)

4. path.getFunctionParent

path.getFunctionParent 方法是向上查找与当前节点最接近的 父函数,返回的也是 Path 对象

5. path.getStatementParent

path.getStatementParent方法是向上遍历语法树直到找到语句父节点。例如,声明语句、return 语句、if 语句、switch 语句和 while 语句,返回的也是 Path 对象。

该方法从当前节点开始找,如果想要找到 return 语句的父语句,就需要从 parentPath 中去调用,代码如下:

const visitor = {
  ReturnStatement(path) {
    const findPath = path.parentPath.getStatementParent();
    console.log(findPath.toString());
  }
}

traverse(ast, visitor);

同级path

1. 什么是同级path

  1. 在AST语法树中,同级path指的是同一层级的节点。
  2. 每个节点在AST语法树中都有一个唯一的路径,路径是由祖先节点的类型组成
  3. 同级path表示具有相同祖先节点的节点的路径(path)

例如在以下的图中,左侧定义了两个变量语句,在AST语法树结构的body数组中,存在两个VariableDeclaration祖先节点,这两个VariableDeclaration就是同级path。

同级path不只是这一种,还有被包裹在其他方法里面的。也称为同级path

2. container

container(容器),

3. 同级path种的方法和属性

  • path.inList 用于判断是否有同级节点。
    • 注意:当container为数组,但只有一个成员时,返回true
  • path.key 获取当前节点在容器中的索引
  • path.container 获取容器(包含所有同级节点的数组)
  • path.listKey 获取容器名
  • path.getSibling(index) 用于获取 同级path,其中参数 index 为容器数组中的索引,index通过 path.key 来获取,可以对path.key 进行加减操作来定位到不同的同级path
  • path.getPrevSibling 获取当前节点的上一个兄弟节点
  • path.getNextSibling() 获取当前节点的下一个兄弟节点
  • unshiftContainer 与 pushContainer
    • unshiftContainer 是往容器最前面添加节点
    • pushContainer 是往容器最后面添加节点

scope作用域

在AST(抽象语法树)中,scope(作用域) 表示一个代码块中变量和函数的可见性和访问权限。它决定了在给定位置访问那些标识符是合法的

scope提供了一些属性和方法,可以方便的查找标识符的作用域:

  • 获取标识符的所有引用
  • 修改标识符的所有引用
  • 判断标识符是否为参数
  • 判断标识符是否为常量,如果不是常量,也可以知道从哪里修改它
  • 关于什么是标识符?
    • 标识符是用于,标识变量、函数、属性和参数的名称

例如,以下的代码中定义了一个 let b = 2000; 的变量,这时候想看变量 b 所有的引用地方在哪里,就可以通过 scope 的 binging(捆绑) 去处理

const a = 1000;
let b = 2000;
let obj = {
    name:"AST-0基础菜鸟入门学习",
    add:function (a) {
        a = 400;
        b = 300;
        let e = 700;
        function demo(){
            let d = 600;
        }
        demo();
        return a + a + b + 1000 + this.name;
    }
}

在以上的代码中,obj 对象中,编写了一个add函数,在add函数中又定义了一个 demo 函数。这种函数在AST中的类型为:FunctionDeclaration

1. scope.block

scope.block 属性可以获取标识符的作用域,返回的是Node对象。

标识符为变量时:

// todo 编写ast插件
const visitor = {
    Identifier(path){
        if(path.node.name === "e"){
            // console.log(path.scope.block) // 得到的是一个Node对象
            // 通过generator将node对象转换为代码
            console.log(generator(path.scope.block).code)
            /* 输出内容结果
           function (a) {
              a = 400;
              b = 300;
              let e = 700;
              function demo() {
                let d = 600;
              }
              demo();
              return a + a + b + 1000 + this.name;
            }
            * */
        }
    }
}
traverse(ast,visitor)

  • 代码中,当traverse遍历到节点为Identifier时,并且条件path.node.name === “e” 成立,就将path.scope.block 转换为代码,并输出。
  • 由于path.scope.block 得到的是一个Node对象,所以可以使用generator组件转换为代码
  • 可以看到以上的代码中输出的是 a 变量所在的作用域的代码。也就是 add 函数

标识符为函数时:

// todo 编写ast插件
const visitor = {
    FunctionDeclaration(path){
        console.log(generator(path.scope.block).code)
        /* 输出结果
        function demo() {
          let d = 600;
        }
        * */
    }
}
traverse(ast,visitor)
  • 以上的代码中,当遍历到节点为FunctionDeclaration时,将path.scope.block得到的Node对象转换为代码并输出。
  • 通过输出的结果可以知道,当前demo函数的作用域是 add 范围,但是只输出了 demo 函数,并没有输出 demo 函数所在作用域的代码,遇到这种情况需要获取父级path作用域
    // todo 编写ast插件
    const visitor = {
        FunctionDeclaration(path){
            console.log(generator(path.scope.parent.block).code)
            /* 输出结果
            function (a) {
              a = 400;
              b = 300;
              let e = 700;
              function demo() {
                let d = 600;
              }
              demo();
              return a + a + b + 1000 + this.name;
            }
            * */
        }
    }
    traverse(ast,visitor)
    • 代码中的 path.scope.parent 获取的是当前节点的父节点的作用域,然后在通过 block 获取标识符作用域

2. scope.dump

scope.dump 会得到自己向上的作用域与变量信息,先输出自己当前的作用域,在输出父级作用域,在输出父级的父级的作用域,直到顶级作用域。

// todo 编写ast插件
const visitor = {
    FunctionDeclaration(path){
        console.log("函数: ",path.node.id.name + "()")
        path.scope.dump()
        /* 输出结果:
        函数:  demo()
        ------------------------------------------------------------
        # FunctionDeclaration
         - d { constant: true, references: 0, violations: 0, kind: 'let' }
        # FunctionExpression
         - a { constant: false, references: 2, violations: 1, kind: 'param' }
         - e { constant: true, references: 0, violations: 0, kind: 'let' }
         - demo { constant: true, references: 1, violations: 0, kind: 'hoisted' }
        # Program
         - a { constant: true, references: 0, violations: 0, kind: 'const' }
         - b { constant: false, references: 1, violations: 1, kind: 'let' }
         - obj { constant: true, references: 0, violations: 0, kind: 'let' }
        ------------------------------------------------------------
        * */
    }
}
traverse(ast,visitor)
  • 可以看到输出的结果:输出了三个作用域
    • 输出自己当前的作用域:FunctionDeclaration
    • 输出父级作用域:FunctionExpression
    • 输出父级的父级作用域:Program
  • 输出内容解读:
    • # 开头的是每一个作用域,上面输出的内容中一共有3个作用域
    • - 开头的是每一个绑定(binding),每一个binding都会包含几个关键信息,分别是:constant,references,violations,kind
      • constant:表示是否为常量,true为常量,否false
      • references:表示被引用的次数
      • kind:表示声明类型
        • param 表示参数
        • hoistend 提升
        • var 变量
        • local 内部
      • 以上的参数并不是全部,剩下的会在binding对象说明

3. scope.getBinding

scope.getBinding 方法接收一个字符串类型的参数,用来获取对应标识符的绑定(binding)。为了更直观的说明绑定的含义,通过以下代码,遍历FunctionDeclaration,符合要求的就只有demo函数,然后获取当前节点下的绑定a,输出binding。

// todo 编写ast插件
const visitor = {
    FunctionDeclaration(path){
        let binding = path.scope.getBinding("a")
        console.log(binding)
    }
}
traverse(ast,visitor)
  • 提示:这里获得绑定的 a标识符,是当前demo函数的所在作用域的a。不明白的可以通过generator(path.scope.parent.block).code 输出demo函数当前所在的作用域。
  • getBinding中传入的值必须是当前节点能够引用到的标识符名称,如果传入一个不存在的 g ,这个标识符并不存在于当前demo函数的所在的作用域中,或者当前节点不能够引用到 g 标识符。那么getBinding会返回undefined
  • 当前FunctionDeclaration节点所在的作用域本身是dem函数,为什么能找到a呢?因为demo本身的作用于是add这个函数,所以能找到 a 标识符
  • 输出结果:
    • constant:表示是否为常量,true为常量,否false
    • references:表示被引用的次数
    • kind:表示声明类型
      • param 表示函数参数
      • hoistend 提升
      • var 变量
      • local 内部
      • 注意:以上代码中,a是add函数的参数,当函数中局部变量和全局变量重名时,使用的是局部变量
    • identifier:是a标识符的Node对象(节点)
    • path 是 a 标识符的Path对象
    • referencePaths 标识符如果被引用,那么referencePaths中会存放所有该标识符的节点的path对象
    • constantViolations 标识符如果被修改,那么constantViolations中会存放所有修改该标识符的节点的path对象

在使用Binging方法的时候,返回的是一个binding对象,binding对象中也有scope。以上的代码中因为获取的是a的binging,所以是a的scope。

通过以下的代码,加入获得是demo的binding对象和a的binding对象,将其转换为代码,输出的是相同的作用域,add函数。

traverse(ast, {
  FunctionExpression(path) {
    let bindingA = path.scope.getBinding('a');
    let bindingDemo = path.scope.getBinding('demo');
    console.log(bindingA.referenced);
    console.log(bindingA.references);
    console.log(generator(bindingA.scope.block).code);
    console.log(generator(bindingDemo.scope.block).code);
  },
})

/*
true
2
下面代码输出2次
function (a) {
  a = 400;
  b = 300;
  let e = 700;

  function demo() {
    let d = 600;
  }

  demo();
  return a + a + b + 1000 + obj.name;
}
 */

4. scope.getOwnBinding

scope.getOwnBinding 该函数用于获取当前节点自己的binding(绑定),也就是不包含父级作用域中定义的标识符。

以下是获取当前demo函数作用域中的标识符 d 的 binding(绑定)

// todo 编写ast插件
const visitor = {
    FunctionDeclaration(path){
        let binding = path.scope.getOwnBinding("d")
        console.log(binding)
    }
}
traverse(ast,visitor)

但是getOwnBinding这个方法有一个缺点,就是会获取子函数中定义的标识符。

let TestOwnBinding = (path)=>{
    path.traverse({
        Identifier(path){
            let name = path.node.name;
            console.log(`标识符: ${name} ${!!path.scope.getOwnBinding(name)}`)
        }
    })
}


// todo 编写ast插件
const visitor = {
    FunctionExpression(path){
        TestOwnBinding(path)
    }
}

/* 输出结果
标识符: a true
标识符: a true
标识符: b false
标识符: e true
标识符: demo false
标识符: d true // 子函数demo中的标识符 d 
标识符: demo true
标识符: a true
标识符: a true
标识符: b false
标识符: name false
* */

traverse(ast,visitor)
  1. 上述代码中,当traverse遍历到节点为FunctionExpress(FunctionExpress节点也就是add函数),
  2. 然后遍历该函数下的所有Identifier,输出标识符名称,通过getOwnBinding方法得到的绑定结果。
  3. 通过输出结果可以看到,子函数demo中的标识符 d,也可以被getOwnBinding方法得到,也就是说如果想获取当前节点下定义的标识符,而不涉及子函数中的标识符的话,需要进一步的判断,可以通过判断标识符的作用域是否与当前函数作用域来确定

getBinding获取当前作用域下的标识符而不是涉及子函数中的标识符

let TestOwnBinding = (path)=>{
    path.traverse({
        Identifier(p){
            let name = p.node.name;
            let binding = p.scope.getBinding(name)
            // console.log(!!binding)
            // console.log(generator(binding.scope.block).code)

            binding && console.log(`标识符: ${name} `, generator(binding.scope.block).code === path.toString())
        }
    })
}

// todo 编写ast插件
const visitor = {
    FunctionExpression(path){
        TestOwnBinding(path)
    }
}

/* 输出结果, 结果为true的就是当前add函数作用域中的标识符,false都是子函数中的标识符
标识符: a  true
标识符: a  true
标识符: b  false
标识符: e  true
标识符: demo  true
标识符: d  false
标识符: demo  true
标识符: a  true
标识符: a  true
标识符: b  false
* */

traverse(ast,visitor)

通过以上的代码,得到了当前add函数作用域中的标识符,而不会涉及到子函数demo中的标识符

5. scope.traverse

scope.traverse方法用于遍历当前作用域中的节点。可以使用path对象中的scope遍历path.scope.traverse,也可以使用binding中的scope遍历binding.scope.traverse,更推荐使用binding.scope.traverse

// todo 编写ast插件
traverse(ast, {
    FunctionDeclaration(path) {
        let binding = path.scope.getBinding('d');
        // binding.scope.block 表示在遍历时,在当前作用域中遍历
        binding.scope.traverse(binding.scope.block, {
            VariableDeclarator(p) {
                if (p.node.id.name === "d"){
                    p.node.init = types.numericLiteral(10000100001000010000)
                }
            },
        })
    }
})

6. scope.rename

使用scope.rename 可以将标识符进行重命名,这个方法会同时修改所有引用该标识符的地方,例如将add函数中的变量重命名为 bbbbbbbb

// todo 编写ast插件
traverse(ast, {
    FunctionExpression(path) {
        // 首先要拿到b标识符的绑定
        let binding = path.scope.getBinding("b")
        binding.scope.rename("b","bbbbbbbbbbbbbbb")
    }
})
/*修改后的结果
const a = 1000;
let bbbbbbbbbbbbbbb = 2000;
let obj = {
  name: "AST-0基础菜鸟入门学习",
  add: function (a) {
    a = 400;
    bbbbbbbbbbbbbbb = 300;
    let e = 700;
    function demo() {
      let d = 600;
    }
    demo();
    return a + a + bbbbbbbbbbbbbbb + 1000 + this.name;
  }
};
* */

7. scope.hashBinding

scope.hashBinding方法用于查询某个标识符是否有绑定,返回true和false。这个方法也可以用scope.getBinding方法代替,因为getBinding如果找不到标识符会返回undefined,找到的话返回一个binding对象

8. scope.hashOwnBinding

scope.hashOwnBinding方法用于查询当前节点中是否有自己的绑定,返回true和false。可以使用scope.getOwnBinding来代替,因为都是查询自身作用域中是否存在绑定,而getOwnBinding查询到有绑定的话会返回一个binding对象,否则undefined

9. scope.getAllBindings

scope.getAllBindings 方法用于查询当前节点的所有绑定。返回的对象是以标识符名为属性为,对用的Binding为属性值

// todo 编写ast插件
traverse(ast, {
    FunctionExpression(path) {
        console.log(path.scope.getAllBindings())
    }
})
/*输出结果
a = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "param", constantViolations: Array(1), ...}
e = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "let", constantViolations: Array(0), ...}
demo = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "hoisted", constantViolations: Array(0), ...}
b = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "let", constantViolations: Array(1), ...}
obj = Binding {identifier: Node, scope: Scope, path: NodePath, kind: "let", constantViolations: Array(0), ...}
* */

遍历通过bindings属性得到的所有绑定

// todo 编写ast插件
traverse(ast, {
    BlockStatement(path) {
        console.log('\nBlockStatement节点源码: \n', path.toString());
        let bindings = path.scope.bindings;
        console.log('作用域内被绑定数量:', Object.keys(bindings).length);
        for (const bindingsKey in bindings) {
            console.log('名字', bindingsKey);
            let binding_ = bindings[bindingsKey];
            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);
        }
        console.log('-------------------------------------');
    },
})

10. scope.hasReference

scope.hasReference("a") 表示查询当前节点中是否有a标识符的引用,返回布尔值

11. scope.getBindingIdentifier

scope.getBindingIdentifier("a") 表示获取当前节点中绑定a的标识符,返回Identifier的Node对象。

总结:

在Babel中,抽象语法树(AST)中的几个重要概念包括:path对象、node对象、scope对象和binding对象。

1. path对象:path对象表示AST中的一个节点,并提供了对该节点进行操作的方法。每个path对象都与一个具体的node对象相关联。通过path对象可以访问并修改AST的结构和属性。它提供了一系列方法,比如`node()`, `parent()`, `siblings()`, `insertBefore()`等,可以用于获取、修改和操作AST的节点。

2. node对象:node对象是AST中的一个节点,代表了JavaScript代码中的某个语法结构(如函数、变量声明、表达式等)。每个node对象都包含了该语法结构的类型和相关的属性。Babel使用不同的node对象来表示不同的语法结构,比如`FunctionDeclaration`表示函数声明,`VariableDeclaration`表示变量声明等。

3. scope对象:scope对象表示一个代码块(如函数或块级作用域)中的变量和函数的作用域。每个scope对象都有一个或多个binding对象,代表了该作用域中的变量和函数。通过scope对象可以查找和管理作用域中的标识符。

4. binding对象:binding对象表示一个变量或函数的引用。每个binding对象都与一个具体的标识符相关联,并包含了该标识符在作用域中的绑定信息,比如变量的名称、作用域、声明的位置等。通过binding对象可以获取标识符的绑定信息,比如变量的值、类型等。

综上所述,path对象用于操作AST中的节点,node对象表示AST中的语法结构,scope对象表示代码块的作用域,binding对象表示变量或函数的引用。它们共同组成了Babel中对JavaScript代码进行解析和转换的基础。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
js逆向AST混淆是一种通过解析和修改JavaScript的抽象语法树(AST)来还原混淆代码的过程。首先,我们需要获取到混淆代码的AST表示形式。然后,根据特定的混淆算法,对AST进行遍历和修改,以还原原始代码的结构和逻辑。在这个过程中,我们可以使用不同的技术和工具来帮助我们完成混淆任务。 在提供的引用中,涉及了一些对AST进行遍历和修改的代码片段。例如,在引用中,使用了traverse函数来遍历AST,然后通过修改AST节点来进行替换和替换。在引用中,通过迭代和遍历AST,找到变量名和取值方法名,然后将它们替换或删除。在引用中,使用了traverse函数和eval函数来移除赋值表达式和成员表达式。 以上是一些常见的技术和方法,用于js逆向AST混淆。具体的混淆过程可能因代码结构和混淆方式而有所不同。为了成功混淆代码,可能需要更多的详细信息和专业知识。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【JavaScript 逆向】AST 混淆](https://blog.csdn.net/pyzzd/article/details/130613135)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值