AST 技巧:还在手动扣代码?AST技术帮你自动扣代码

大家好,今天来分享一种借助 AST 自动扣代码的方案。

扣代码

扣代码是什么我就不介绍了吧,大家应该都知道了。扣代码这种方法比较繁琐,而且像那种强混淆的代码,函数超级多,一个一个的复制要花费很多时间,属实是有点坐不住。

在晚上睡觉的时候,突然就想到一种方法,最近不是一直在写 AST 的文章吗,就想到了是不是可以使用 AST 来扣代码呢?想了一下发现还真可以,今天就给大家来分享一下操作方法。

AST自动扣代码思路

为了使用 AST 来自动扣代码,要先知道手动的流程是什么样子的。

  1. 先复制好需要扣的函数
  2. 然后找到函数定义,复制进来
  3. 查找未定义的函数,或者运行看报错,缺什么补什么
  4. 重复第二步

现在有了手动扣代码的流程,要使上面的流程自动化,就要知道每一步应该如何使用代码来实现。

  1. 第一步直接手动实现,就一行代码直接复制
  2. 找到函数定义,这个操作可以使用 scopegetBinding 方法,根据函数名获取 binding,然后获取 path 属性即可获取函数定义
  3. 查找未定义的函数,一样是使用 getBinding 方法,如果 bindingundefined,则说明该函数没有找到函数定义
  4. 使用循环重复上述步骤

代码实现

思路已经有了,现在就差代码实现了。为了能使用 getBinding 方法获取函数定义,就要先解析一下原始的文件,因为它里面有所有的函数的定义,还要解析一下需要扣代码的文件,然后使用 getBinding 方法获取所有的函数调用,并且判断当前的函数是否有对应的函数定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let astAll = parser.parse(fs.readFileSync('encode.js', {encoding: "utf-8"}));
let map = new Map();
traverse(astAll, {
FunctionDeclaration: {
exit(path) {
let {node} = path;
let name = node.id.name;
if (map.has(name)) {
// console.error('重复了!', name)
} else {
map.set(name, path.toString());
}
}
}
})

首先是获取所有的函数定义,并且将函数名和对应的函数定义放到 map 中,供后续使用。这里需要注意,如果有重复的函数名要记得处理,要看同名函数的定义是不是一样的,如果是一样的可以直接忽略,如果不一样那就要看具体函数的作用域了,可以先考虑使用 AST 重命名一下不同作用域的函数,尽量防止重名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//用于保存找到的函数定义,添加到输出结果中
let list = [];
//获取所有没有函数定义的函数名
traverse(ast, {
CallExpression: {
exit(path) {
let {node, scope} = path;
let binding = scope.getBinding(node.callee.name);
//说明没有找到函数定义
if (binding === undefined) {
//如果可以找到
if (map.has(node.callee.name)) {
console.log(path.toString());
list.push(parser.parse(map.get(node.callee.name)));
map.delete(node.callee.name);
}
}
}
}
})

然后就是获取所有没有函数定义的函数名,使用 getBinding 方法判断是否有函数定义,如果没有定义则判断是否可以在上述 map 中找到对应的函数名,如果可以找到的,删除 map 中对应的函数名,每个函数只能被扣一次,并且将函数定义添加到 list 中,后续将其添加到输出文件中。

为什么这里使用 list 来保存所有的函数定义呢?因为函数定义要添加到程序的最外层才可以,要遍历 Program 节点,如果每次都遍历会很低效,所以先保存起来最后再统一添加。

还有为什么在 push 的时候要解析代码呢?这里是我卡了一会的地方,刚开始想要使用 template 的方式解析字符串形式的函数定义,后来发现一直报错不能用,因为这里涉及到要在两个 AST 语法树之间搜索并添加代码,重新解析代码才不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
//将找到的依赖函数添加到文件中
traverse(ast, {
Program: {
exit(path) {
let {node} = path;
for (const l of list) {
node.body.unshift(l)
}
}
}
})
//解析成代码,重新遍历,因为扣进来的函数中还会有需要扣的函数
ast = parser.parse(generator(ast).code);

将上面找到的所有函数定义添加到 Program 节点中,添加到节点的最开始的位置。别忘了最后一行,将 AST 重新解析一下,因为添加函数定义后,有的函数就可以找出对应的函数定义了,重新解析相当于重新生成 AST 语法树,更新 getBinding 的返回值。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
let sourceCode = fs.readFileSync('encode_ok.js', {encoding: "utf-8"});
let ast = parser.parse(sourceCode);


let astAll = parser.parse(fs.readFileSync('encode.js', {encoding: "utf-8"}));
let map = new Map();
traverse(astAll, {
FunctionDeclaration: {
exit(path) {
let {node} = path;
let name = node.id.name;
if (map.has(name)) {
// console.error('重复了!', name)
} else {
map.set(name, path.toString());
}
}
}
})

for (let i = 0; i < 12; i++) {
//用于保存找到的函数定义,添加到输出结果中
let list = [];
//获取所有没有函数定义的函数名
traverse(ast, {
CallExpression: {
exit(path) {
let {node, scope} = path;
let binding = scope.getBinding(node.callee.name);
//说明没有找到函数定义
if (binding === undefined) {
//如果可以找到
if (map.has(node.callee.name)) {
console.log(path.toString());
list.push(parser.parse(map.get(node.callee.name)));
map.delete(node.callee.name);
}
}

}
}
})
//将找到的依赖函数添加到文件中
traverse(ast, {
Program: {
exit(path) {
let {node} = path;
for (const l of list) {
node.body.unshift(l)
}
}
}
})
//解析成代码,重新遍历,因为扣进来的函数中还会有需要扣的函数
ast = parser.parse(generator(ast).code);
}

完整代码如上,一些导入函数和文件写入方法大家自行补充哈。

来看效果:

image-20250502225420001

这是需要扣代码的文件,名字为 encode_ok.js,看到里面有很多函数是找不到函数定义的状态,pycharm 有波浪线提示。

image-20250502225521440

这是包含所有函数定义的 encode.js

image-20250502225724470

最终效果如上图所示,现在代码的行数来到了 6000 多行,可见扣了很多函数定义过来,并且刚刚的波浪线提示也没有了,说明对应的函数现在都可以找到函数定义了。

有待完善

好了,现在演示完了,我实测了一下效果还可以,基本上补充一下变量的定义就可以正常的跑了,不用自己一个一个函数自己扣了。

不过现在的方案还是相对比较简单,不能扣变量定义,只能扣函数定义,并且只是简单的扣过来的函数放到全局,还是需要人工介入将变量定义补充完整才可以正常跑。有的时候遇到内部函数或者需要按顺序初始化的变量或者函数,会有问题,上述代码还不能自动处理。

如果有特殊需求或者有能力的小伙伴可以根据上述代码自行更改,让它可以变得更通用或者更适合你。

当然了,没有什么方案是可以处理所有情况的,越通用的方案越复杂,越容易出 bug,简单的方案虽然不能处理所有情况,但是可以自动扣几千行的函数也可以节省大量的时间了,有这时间可以多睡会哈哈哈。

总结

我再次强调,现在的方案还仅仅是初级阶段,代码很简单,处理方式也很简单,还有很多需要完善的地方,不推荐直接用到生产环境,仅供参考测试。

本文章首发于个人博客 LLLibra146’s blog

本文作者:LLLibra146

更多文章请关注公众号 (LLLibra146):LLLibra146

版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!

本文链接
https://blog.d77.xyz/archives/2ca13998.html