# AST 基本命令

# path

path 为 AST 解析后的产物,在解析时,先将所有代码解析为一个大 file, 其中拥有 path 属性,若是针对一种表达式解析 (例如 VariableDeclarator 赋值表达式),那么 path 指代的就为赋值表达式所在的路径,其中又包含很多属性,主要的有:

  • type: 表示表达式类型
  • node 表示该节点、为一个对象、包含很多属性
  • parent: 该路径的父路径,这里指代的是 node 类型,若要获取 path 类型需要 path.parentPath.

如 var a = b; 那么 a=b 为赋值表达式当前路径、var a = b 为父路径。

  • scope: 作用域

path.toString () 可以打印出当前代码位置.

# node

node 本质上是一个对象,内置属性如下 (仍以 VariableDeclarator 为例):

  • type: 表示表达式类型
  • start: 开始位置
  • end: 结束位置
  • id: 对应左半部分,也为节点类型
  • init: 对应右半部分,也为节点类型

node 可以采用 generator (path.node).code 的方式获取当前代码位置.

# binding

绑定,为 scope 中的属性,要使用 path.scope.getBinding (name) 得到,其中 name 为想要绑定对象昵称。

block: 指代整体 path?

binding.constant:Boolean (表示是否被修改过值),constantViolations 表示修改值的位置。

referencePaths: 绑定对象所在区域,例如:

var a = 1;
var b = a + 1;

在对第一个表达式遍历的时候,a 对应的 referencePaths 为 var b = a+ 1 其中的 a,即我们可以通过该方式替换节点的值,达到批量修改的目的。

# 浅层检查
t.isIdentifier(path.node.left, { name: "n" })
等价于
path.node.left != null &&
    path.node.left.type === "Identifier" &&
    path.node.left.name === "n"
# get 方法

使用 path.get ('target'),可以获得对应的 path 路径,相比于直接获取 node 节点,在确定作用域的时候更有效,比如:

(function(a,b){console.log(a+b);}());
// 对于上述自执行函数,我们通过遍历 CallExpression 节点,然后通过 path.scope.getBinding ('a') 的方法只能获取到 undefined, 这是由于,当前的 path 指代的是外部作用域,不能指向自执行函数内部,因此,想要获取到 'a' 的 binding, 就需要通过 path.get ('callee') 的方法,获取内部函数的作用域,这样一来就可以通过遍历的方式 j

# 插件优化

# 变量替换

目的:将赋值表达式值为字面量、数字的直接批量替换

const visitor = {
    VariableDeclarator(path){
        const {id,init} = path.node;
        // 获取对应变量的绑定
        var binding = path.scope.getBinding(id.name);
        // 获取绑定作用域
        referpaths = binding.referencePaths;
        // 如果变量没引用,则返回
        if(referpaths.length === 0) return;
        // 保证变量的 init 部分值唯一,数字、字符类型直接替换,函数定义也可直接替换,前提是引用部分为单引用,若为函数调用,可考虑直接计算值。
        // 遍历绑定作用域
        for (referpath of referpaths){
            // 节点值替换
            referpath.replaceWith(path.node.init);
        };
        // 节点删除
        path.remove();
    }
};
# 数组对象还原
/* 
var b = ['asdad'].concat(function(){return arguments});
var c = b[1];
b.shift();
var d = b[0];
*/
// 通过遍历声明节点,找到数组命名的位置,然后将其写入内存,接着在将
const visitor12 = {
    VariableDeclarator(path) {
        const {id, init} = path.node;
        var func_name = id.name;
        var binding = path.scope.getBinding(func_name);
        var referpaths = binding.referencePaths;
        // 如果变量没引用,则返回
        if (referpaths.length === 0) return;
        // 保证变量的 init 部分值唯一,数字、字符类型直接替换,函数定义也可直接替换,前提是引用部分为单引用,若为函数调用,可考虑直接计算值。
        // 单独针对这种情况,不考虑单纯的数组。
        if (types.isCallExpression(init) && types.isArrayExpression(init.callee.object)) {
            // 将数组中的内容直接写入内存
            if(init.callee.property.name !== 'concat') return;
            var old_arr = init.callee.object.elements.concat(init.arguments[0]);
            global[func_name] = old_arr;
            // 遍历作用域,获取修改值的地址
            binding.scope.traverse(binding.scope.block, {
                MemberExpression(_path) {
                    // 后面可能还要确保只修改一次的情况
                    console.log(_path.toString());
                    if (_path.node.property.value >= 0) {
                        let index_value = _path.node.property.value;
                        _path.replaceInline(eval(func_name)[index_value]);
                    };
                    // 若包含 shift 操作
                    if (_path.node.property && _path.node.property.name === 'shift') {
                        global[func_name].shift()
                    }
                }
            });
            path.remove();
        }
    }
}
# 自执行函数还原
# 思路 1: 使用 referencePath 对其进行回填。
/*
(function (a,b){console.log (a+b)})(3,4)------->console.log (3+4)
自执行函数为 CallExpression 块,callee 指向 FunctionExpression, 需要做的就是将 block 块中提取,然后将变量值回填,
简单来讲,假如这个值没有被修改啥的,直接遍历即可
*/
(function(a,b){
    var arr = arguments;
    if(a!==3) return;
    for(var i=0;i<2;i++) {
        arr[i] += i;
    };
    a = a+1;
    var c = a + b;
})(3,4);
-------------------------------------
// 自执行函数回填
const self_func_replace = {
    CallExpression(path) {
        let callee = path.get('callee');
        const {arguments} = path.node;
        if(!types.isFunctionExpression(callee.node)) return;
        var {params, body} = callee.node;
        // 遍历形参数组,进行回填
        for(var i=0;i<arguments.length;i++){
            binding = callee.scope.getBinding(params[i].name);
            references = binding.referencePaths;
            // 遍历当前形参的 Binding 节点,并替换。
            console.log(binding.constantViolations);
            if(!binding || !binding.constant){
                continue;
            };
            for(reference of references){
                reference.replaceInline(types.valueToNode(arguments[i].value));
            }
        };
    }
};    
----------------------------------
(function (a, b) {
  var arr = arguments;
  if (a !== 3) return;
  for (var i = 0; i < 2; i++) {
    arr[i] += i;
  }
  ;
  a = a + 1;
  var c = a + 4;
})(3, 4);

可以看到,由于 a 在作用域中的值被改变了,所以并没有进行回填。

但是,从实际上来看,这两个值其实都被修改了,所以说,要想用 referencePath 来对其自执行函数进行回填有一定的局限性。

既然我们知道了,是通过一个 for 循环来对传入的 arguments 进行的处理,那么,我们只要把这个 for 循环的算法封装成一个函数,直接输出一个新的 arguments 数组,这样一来,后面替换的时候,就采用新的数组替换不就完了么。

function get_arr_node(arguments){
    arr = [];
    for(i of arguments){
        arr.push(i.value);
    };
    // 对字符的一个处理
    for(var i=0;i<2;i++) {
        arr[i] += i;
    };
    return arr
};
// 自执行函数回填
const self_func_replace = {
    CallExpression(path) {
        let callee = path.get('callee');
        const {arguments} = path.node;
        var new_arguments = get_arr_node(arguments);
        if(!types.isFunctionExpression(callee.node)) return;
        var {params, body} = callee.node;
        // 遍历形参数组,进行回填
        for(var i=0;i<arguments.length;i++){
            binding = callee.scope.getBinding(params[i].name);
            references = binding.referencePaths;
            // 遍历当前形参的 Binding 节点,并替换。
            if(!binding || !binding.constant){
                continue;
            };
            for(reference of references){
                reference.replaceInline(types.valueToNode(new_arguments[i]));
            }
        };
    }
};
--------------------------------------------
(function (a, b) {
  if (a !== 3) return;
  for (var i = 0; i < 2; i++) {
    arguments[i] += i;
  }
  ;
  a = a + 1;
  var c = a + 5;
})(3, 4);

可以看到,这里已经成功的把 b 填进去,同时也为正确的数了,要做到完全还原,也只需要遍历 a 进行回填即可。

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

Mr2 WeChat Pay

WeChat Pay