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

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

扣代码

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

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

AST自动扣代码思路

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

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

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

  1. 第一步直接手动实现,就一行代码直接复制
  2. 找到函数定义,这个操作可以使用 scope 的 getBinding 方法,根据函数名获取 binding,然后获取 path 属性即可获取函数定义
  3. 查找未定义的函数,一样是使用 getBinding 方法,如果 binding 是 undefined,则说明该函数没有找到函数定义
  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