reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
57
vendor-new/xbsReverseSkill/ast-deobfuscation/SKILL.md
Normal file
57
vendor-new/xbsReverseSkill/ast-deobfuscation/SKILL.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: ast-deobfuscation
|
||||
description: 使用 Babel AST 对 JavaScript 做分层、可回退的定向反混淆。适用于 `_0x` 标识符、字符串表、自执行解码包装、dispatcher 对象、虚假常量分支、`while/for + switch` 控制流平坦化、`if (literal === opcode)` 分发链,以及需要按站点或混淆家族命中特征切换专用脚本的场景。用户明确提到 reese84、顶象、极验4、同花顺、网易易盾、小红书、OB 变种或类似站点适配时也使用本 skill。
|
||||
---
|
||||
|
||||
# AST 反混淆
|
||||
|
||||
优先使用分层入口,而不是继续把站点特有逻辑堆进通用脚本。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 先运行 `scripts/detect-patterns.js <input.js> [hint]` 做模式检测。
|
||||
2. 只读取命中的站点或混淆家族规则文档。
|
||||
3. 运行 `scripts/run-pipeline.js <input.js> <output-dir> [hint]` 执行选中的流水线。
|
||||
4. 对照参考产物或原始混淆症状,判断是否还需要新增专用适配脚本。
|
||||
|
||||
## 设计规则
|
||||
|
||||
- 通用脚本只保留低风险、可复用的改写,例如结构标准化、虚假分支清理、dispatcher 内联、控制流拍平和 `if -> switch`。
|
||||
- 一旦某个模式明显是站点特有的,就同时补齐四部分:规则文档、检测器命中项、专用适配脚本、流水线配置项。
|
||||
- 对于高开销步骤,优先按家族跳过或重排,不要强行要求所有样本走同一套顺序。像 `reese84` 这种大样本,如果太晚执行 `inline-literals`,很容易卡住。
|
||||
- 专用适配脚本应当保持窄而准。如果某条规则没有在多个无关样本中复用,不要急着回灌到通用脚本。
|
||||
- 结构性改写后要重新 parse,并保证每一阶段都可以独立运行和排查。
|
||||
|
||||
## 入口脚本
|
||||
|
||||
- `scripts/detect-patterns.js`
|
||||
根据文件路径、可选 hint 和源码症状判断最可能命中的站点或混淆家族。
|
||||
- `scripts/run-pipeline.js`
|
||||
把样本复制到测试目录,执行选中的步骤,记录耗时并输出流水线报告。
|
||||
- `scripts/collect-residue-metrics.js`
|
||||
统计仍未解开的症状,例如 `split('|')`、直接 `loop/switch` 平坦化、opcode `if` 链、dispatcher wrapper 和 `_0x` 标识符。
|
||||
- `scripts/compare-with-reference.js`
|
||||
将最新流水线输出与 `decode.js` 对比,汇总剩余差距。
|
||||
|
||||
## 参考文档
|
||||
|
||||
- 新增或调整适配器时,先读 `references/pattern-layering.md`。
|
||||
- 任何逻辑想放进通用脚本前,先读 `references/safe-rewrite-rules.md`。
|
||||
- 处理字符串表、解码 stub、最小运行时求值时,读 `references/string-array-and-minimal-eval.md`。
|
||||
- 处理控制流平坦化、opcode 分发器和 VM 类 handler 时,读 `references/control-flow-and-opcode-patterns.md`。
|
||||
- 处理逗号表达式、IIFE、语句提升时,读 `references/sequence-normalization.md`。
|
||||
- 检测器命中后,只读取对应的一份站点规则文档:
|
||||
- `references/patterns/reese84.md`
|
||||
- `references/patterns/dingxiang.md`
|
||||
- `references/patterns/geetest4.md`
|
||||
- `references/patterns/tonghuashun.md`
|
||||
- `references/patterns/yidun.md`
|
||||
- `references/patterns/xiaohongshu.md`
|
||||
- `references/patterns/cn-bidding-ob.md`
|
||||
- `references/patterns/mps-ob.md`
|
||||
|
||||
## 校验
|
||||
|
||||
- 每个新增或改过的 JavaScript 辅助脚本,在接入流水线前都要至少过一次 `node --check` 或等价的加载校验。
|
||||
- 修改 skill 后,运行 `C:\Users\25198\.codex\skills\.system\skill-creator\scripts\quick_validate.py C:\Users\25198\.codex\skills\ast-deobfuscation`。
|
||||
- 测试案例时,把中间产物、检测结果、对比结果和流水线耗时都保留在案例目录里,方便定位慢步骤和失败步骤。
|
||||
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "AST Deobfuscation"
|
||||
short_description: "Layered Babel AST deobfuscation with detectors, reusable passes, and site-specific adapters."
|
||||
default_prompt: "Use $ast-deobfuscation to detect the obfuscation family first, then run the layered pipeline with generic passes and any matching site adapter."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,55 @@
|
||||
# 控制流平坦化与 Opcode 模式
|
||||
|
||||
这份文档用于处理控制流平坦化、opcode `if` 链、虚拟机解释器主分发器,以及测试位噪声分支。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- `for (...; ![];) { switch (...) { ... } }`
|
||||
- `while (true) { switch (...) { ... } }`
|
||||
- `"3|1|2".split('|')` 一类数组或顺序表驱动的平坦化
|
||||
- 大量 `if (0x31 === opcode) ... else if ...`
|
||||
- 虚拟机解释器里围绕同一个 opcode 变量做长链分发
|
||||
|
||||
## 控制流展平原则
|
||||
|
||||
- 优先写定向 visitor,不要一开始追求通用大而全。
|
||||
- 先验证 `init`、`test`、`update`、`body` 结构是否稳定。
|
||||
- 只要结构不匹配,就直接跳过,不要强行展平。
|
||||
- 拼接 case 时同步清理仅用于跳转的赋值、无意义 `break` 和明显噪声语句。
|
||||
|
||||
## `if` 链改写为 `switch`
|
||||
|
||||
仅在以下条件同时满足时改写:
|
||||
|
||||
- 整条链都在判断同一个判别表达式。
|
||||
- 每个分支都拿这个表达式与字面量比较。
|
||||
- 分支数量足够长,改写后可读性明显提升。
|
||||
|
||||
## 测试位噪声与哨兵比较
|
||||
|
||||
某些样本会在测试条件里塞入噪声,例如:
|
||||
|
||||
```js
|
||||
cond && fn(...) !== {}
|
||||
cond && fn(...) === {}
|
||||
```
|
||||
|
||||
只在以下前提下清理:
|
||||
|
||||
- 它位于 `if`、`for.test` 或 `while.test` 位置。
|
||||
- 右侧确实只是混淆器注入的空对象比较噪声。
|
||||
- 删除后不会影响左侧真实条件的副作用顺序。
|
||||
|
||||
## 虚拟机解释器建议
|
||||
|
||||
- 第一轮只求看清主分发器、栈变量、opcode 读取器。
|
||||
- 清掉虚假分支后,再把 `if` 链改成 `switch`。
|
||||
- 第二轮再考虑简化 case 内的局部结构。
|
||||
|
||||
## 停止条件
|
||||
|
||||
- 主 opcode 分发器已可读。
|
||||
- 关键 case 已经可以逐个分析。
|
||||
- 继续静态展开开始明显增加误改写风险。
|
||||
|
||||
到这里就可以转入人工分析或结合运行时证据继续逆向。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 模式分层
|
||||
|
||||
将这个 skill 保持为三层结构:
|
||||
|
||||
1. 通用变换层
|
||||
只处理稳定、低风险、能跨多个样本复用的改写。
|
||||
2. 检测与流水线选择层
|
||||
先命中混淆家族或站点,再决定步骤顺序。
|
||||
3. 家族或站点适配层
|
||||
当某种残留可以稳定复现,但又不适合泛化时,新增窄范围脚本和配套规则文档。
|
||||
|
||||
新增一个适配器时,要一起更新这些位置:
|
||||
|
||||
- `references/patterns/<pattern>.md`
|
||||
- `scripts/patterns/<pattern>.js`
|
||||
- `scripts/pipeline-config.js`
|
||||
- `scripts/detect-patterns.js` 对应的配置命中项
|
||||
|
||||
除非某条规则已经在多个无关样本中重复出现,而且安全边界依然清晰,否则不要把站点规则回灌进通用脚本。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 变种OB混淆
|
||||
|
||||
当样本是OB变种混淆时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 仍然能看到 `_0x` 风格的标识符。
|
||||
- 经过通用处理后,OB 家族的包装层依旧存在。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 将 OB 家族逻辑保留在专用适配器里,只给 OB 类样本复用。
|
||||
- 适配器运行后,直接把 `loop/switch` 残留与参考 `decode` 结果对比。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然残留的直接 `loop/switch` 平坦化结构。
|
||||
- 任何高于参考基线的 OB 风格命名或包装层痕迹。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 顶象
|
||||
|
||||
当样本属于顶象或非常接近的混淆家族时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- IIFE 的参数会喂给后续平坦化阶段使用的数组或查找对象。
|
||||
- 清理 dispatcher 会有帮助,但更关键的是恢复由数组承载的执行顺序。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 先解析由参数驱动的数组,再判断 `loop/switch` 平坦化是否仍然存在。
|
||||
- 平坦化完成后保持流程保守,默认不要再追加第二轮高开销的字面量内联。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 相对 `decode.js` 仍然存在的结构噪音。
|
||||
- 在直接 `loop/switch` 块已经清零后,依然残留的 dispatcher 包装层差距。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 极验4
|
||||
|
||||
当样本属于极验4,或属于相近的 guarded-switch VM 家族时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- `loop/switch` 平坦化外层还包着 guarded `if` 结构。
|
||||
- 即使通用平坦化器已经能处理较简单的 handler,输出与参考结果仍然差很多。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 将 guarded-switch 适配器作为家族专用步骤运行,不要塞进通用平坦化器内部。
|
||||
- 优先压低直接 `loop/switch` 残留,再考虑命名或可读性层面的清理。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然残留的直接 `loop/switch` 热点。
|
||||
- 仍然比 `decode.js` 更嘈杂的 guarded opcode dispatcher 结构。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 中华人民共和国公安部
|
||||
|
||||
当样本属于中华人民共和国公安部,并且符合 OB 家族特征时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 存在 `_0x` 风格标识符以及 OB 家族的包装结构。
|
||||
- 虽然与招投标样本属于同一家族,但 `loop/switch` 残留画像并不相同。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 复用共享的 OB 适配器,但为这种残留画像保留独立规则文档。
|
||||
- 以直接 `loop/switch` 块是否降到参考基线作为成功判断标准。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 经过 OB 适配器后依然存在的 `loop/switch` 平坦化残留。
|
||||
- 持续存在的 `_0x` 命名或包装层差距。
|
||||
@@ -0,0 +1,18 @@
|
||||
# reese84
|
||||
|
||||
当文件路径、hint 或样本标签明确指向 `reese84` 时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 样本体量大,呈现明显的 VM 风格,并且 dispatcher 残留较重。
|
||||
- 完成结构性处理后,`inline-literals` 的开销会显著升高。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 优先采用 `normalize -> prune -> inline-dispatchers -> flatten -> if-to-switch` 这条顺序。
|
||||
- 除非在更小的裁剪样本上已经验证安全,否则跳过后置的 `inline-literals`。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然存在的 dispatcher 包装层。
|
||||
- 相对 `decode.js` 的最终行数差距。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 同花顺
|
||||
|
||||
当样本属于同花顺,或属于相关的 split-order wrapper 家族时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 残留的 `split('|')` 顺序字符串会驱动 `loop/switch` 平坦化。
|
||||
- 在抵达真正的顺序字符串之前,包装对象还会层层指向其他包装对象。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 在 `flatten-array-control-flow.js` 之前插入专门的顺序源适配器。
|
||||
- 不要默认把 `inline-literals` 当成这里的瓶颈解法。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 尚未解开的 `split('|')` 顺序数组。
|
||||
- 由多层包装对象驱动的剩余 `loop/switch` 代码块。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 小红书
|
||||
|
||||
当样本属于小红书时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- wrapper 清理相对较轻,剩余平坦化也不算多。
|
||||
- 可读性提升主要依赖结构规范化,而不是更深的求值或执行。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 让适配器保持轻量。
|
||||
- 优先做规范化和包装层清理,而不是激进的常量传播。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 仍然与 `decode.js` 存在差异的小型 dispatcher 或包装层残留。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 网易易盾
|
||||
|
||||
当样本属于网易易盾时,使用这份规则。
|
||||
|
||||
识别信号:
|
||||
|
||||
- 通用 dispatcher 处理之后,wrapper 仍然比较密集。
|
||||
- 做一轮字面量传播通常仍然有帮助,而且不至于把文件推向病态体积。
|
||||
|
||||
流水线建议:
|
||||
|
||||
- 让 dispatcher 适配器尽量贴近 `inline-dispatchers` 这一步。
|
||||
- 允许执行一轮 `inline-literals`,但不要盲目重复。
|
||||
|
||||
当前残留重点:
|
||||
|
||||
- 相对 `decode.js` 的 dispatcher 包装层数量差距。
|
||||
- 主平坦化已经消除后,仍然存在的少量结构差距。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 安全改写规则
|
||||
|
||||
这份文档只回答一个问题:当前节点是否“确定可还原”。
|
||||
|
||||
只有在答案明确为“是”时,才做 AST 替换。
|
||||
|
||||
## 可以直接替换的典型场景
|
||||
|
||||
- 数组字面量下标访问,且下标是 `NumericLiteral`,数组内容本身也稳定。
|
||||
- 已建立最小解密环境后,调用参数全是数字字面量或纯字面量。
|
||||
- `['a', 'b', 'c'].join('')`、`String.fromCharCode(...)`、`split('').reverse().join('')` 这类纯计算表达式。
|
||||
- 明确的字符串或数字别名链,例如 `var a = 'x'; var b = a;`。
|
||||
- 可以静态确认结果的恒真或恒假比较,例如 `if ('a' === 'a')`。
|
||||
|
||||
## 不要直接替换的高风险场景
|
||||
|
||||
- 依赖 `this`、`window`、闭包外部变量或浏览器环境的调用。
|
||||
- 含 getter、setter、代理对象、动态属性访问、副作用函数调用的表达式。
|
||||
- 不能确认 binding 是否稳定的别名链。
|
||||
- 顺序很关键的逻辑表达式、逗号表达式、条件测试表达式。
|
||||
- 需要跨作用域推断执行结果的节点。
|
||||
|
||||
## 默认判断顺序
|
||||
|
||||
1. 先看节点结果是否只依赖字面量和局部稳定绑定。
|
||||
2. 再看求值过程中是否存在副作用。
|
||||
3. 再看是否依赖执行顺序、宿主环境或外部状态。
|
||||
4. 任意一步无法确认,就不要替换。
|
||||
|
||||
## 常见误区
|
||||
|
||||
- 不要把所有 `CallExpression` 都拿去局部求值。
|
||||
- 不要把 `LogicalExpression` 默认折叠成左值或右值,尤其是含函数调用时。
|
||||
- 不要把 `StringLiteral`、`NumericLiteral`、`BooleanLiteral` 再次纳入“循环 evaluate 并回写”的流程。
|
||||
- 不要为了追求完全还原而牺牲稳定性。
|
||||
|
||||
## 推荐做法
|
||||
|
||||
- 先得到“结构可读、关键字符串已恢复、主控制流已展开”的结果。
|
||||
- 把不能静态确认的区域保留下来,等后续结合运行时证据再处理。
|
||||
- 每完成一批替换后重新 parse 一次,确保输出仍然稳定。
|
||||
@@ -0,0 +1,61 @@
|
||||
# 逗号表达式与结构标准化
|
||||
|
||||
这份文档用于处理 `SequenceExpression`、表达式语句、`if`/`for` 中的逗号表达式,以及部分可安全收敛的结构标准化。
|
||||
|
||||
## 总原则
|
||||
|
||||
- 不要把所有逗号表达式都统一拆成多条语句。
|
||||
- 必须先看父节点,再决定改写方式。
|
||||
- 必须保证执行顺序和副作用顺序不变。
|
||||
|
||||
## 按父节点判断
|
||||
|
||||
### `ExpressionStatement`
|
||||
|
||||
- 通常可以直接拆成多条表达式语句。
|
||||
- 但如果末尾结果还被后续逻辑隐式依赖,先确认再拆。
|
||||
|
||||
### `IfStatement.test`
|
||||
|
||||
- `if (a(), b(), test)` 可以改写为:
|
||||
1. 先把 `a()`、`b()` 提到 `if` 之前。
|
||||
2. 保留 `if (test)`。
|
||||
|
||||
### `ForStatement.init`
|
||||
|
||||
- `for (a(), b(), c(); test; update)` 中的 `init` 逗号表达式最适合先拆,因为它只执行一次。
|
||||
|
||||
### `ForStatement.test`
|
||||
|
||||
- `for (init; a(), b(), test; update)` 可以改写为:
|
||||
1. 保留循环头里的稳定部分。
|
||||
2. 把 `a()`、`b()` 放到循环体开头。
|
||||
3. 在顶部加基于 `test` 的 `break` 守卫。
|
||||
|
||||
### `ForStatement.update`
|
||||
|
||||
- 只在循环体中不存在 `continue` 风险时再拆。
|
||||
- 如果存在 `continue`,把更新表达式搬到循环体尾部可能会改变行为,应保守跳过。
|
||||
|
||||
### `ReturnStatement` 与 `VariableDeclarator`
|
||||
|
||||
- 这两类需要分别处理,不能简单套用表达式语句的拆法。
|
||||
- 如果暂时没有足够稳定的模板,宁可不动。
|
||||
|
||||
## 可顺手清理的结构
|
||||
|
||||
- 零参数、立即执行、纯包裹的 IIFE。
|
||||
- 表达式语句里的简单三元噪声。
|
||||
- 可静态确认且不改变副作用顺序的简单逻辑表达式语句。
|
||||
|
||||
## 不要做的事
|
||||
|
||||
- 不要把复杂 `LogicalExpression` 默认改写成 `if`。
|
||||
- 不要在不知道父节点语义的情况下使用批量 `replaceWithMultiple`。
|
||||
- 不要只因为“文本更好看”就重排表达式执行顺序。
|
||||
|
||||
## 验证建议
|
||||
|
||||
- 结构标准化后立即重新 parse。
|
||||
- 对 `for.test` 和 `for.update` 的改写,优先用代表性样本回归一次。
|
||||
- 如果改写后只提升了少量可读性,却增加了行为不确定性,就回退。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 字符串表与最小求值
|
||||
|
||||
这份文档用于处理字符串表、解码包装、局部别名链和最小引导 `eval`。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 文件开头几条语句就在初始化解码器。
|
||||
- 顶层 IIFE 里传入数组、对象或字符串表。
|
||||
- 源码里大量出现 `fn(12)`、`arr[123]`、`String.fromCharCode(...)`。
|
||||
- 第一层去壳之后,函数内部还存在二次字符串恢复。
|
||||
|
||||
## 推荐顺序
|
||||
|
||||
1. 先定位数组字面量、解码函数和别名链。
|
||||
2. 尽量优先做静态恢复,例如数组下标、纯字面量拼接、纯数字参数调用。
|
||||
3. 只有静态无法推进时,才执行最小引导代码。
|
||||
4. 删除已经消费掉的引导声明或包装层。
|
||||
5. 重新 parse 一次,再进入控制流还原阶段。
|
||||
|
||||
## 最小引导 `eval` 原则
|
||||
|
||||
- 只执行前几条足以构造解码器的代码。
|
||||
- 不要直接执行整份源文件。
|
||||
- 不要在未隔离的环境里执行明显依赖 DOM、`window`、定时器或网络的逻辑。
|
||||
- 求值目标应尽量窄,例如“只恢复数字参数到字符串的函数调用”。
|
||||
|
||||
## 常见模式
|
||||
|
||||
### 顶层 IIFE 带数组参数
|
||||
|
||||
- 从顶层调用提取 `formalParam -> actualArg` 映射。
|
||||
- 只替换明确命中的数组下标访问。
|
||||
- 处理包装函数里直接可展开的语句块。
|
||||
|
||||
### 前几条语句就是解密引导
|
||||
|
||||
- 只截取前 2 到 5 条关键语句。
|
||||
- 建立最小运行时后,恢复只带字面量参数的调用点。
|
||||
- 删除已消费的引导语句。
|
||||
|
||||
### 去掉第一层后,局部函数还有二次恢复
|
||||
|
||||
- 先展开顶层 IIFE。
|
||||
- 重新 parse。
|
||||
- 再识别函数内部那组“构造器 + 成员表达式 + 数字下标”的模式。
|
||||
|
||||
## 停止条件
|
||||
|
||||
- 主要字符串表已经恢复。
|
||||
- 核心解码器入口已经清楚。
|
||||
- 剩余部分不再适合纯静态恢复。
|
||||
|
||||
满足以上条件后,应切换到控制流或结构标准化阶段。
|
||||
@@ -0,0 +1,271 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const parser = require("@babel/parser");
|
||||
const traverse = require("@babel/traverse").default;
|
||||
const generator = require("@babel/generator").default;
|
||||
const t = require("@babel/types");
|
||||
|
||||
function countMatches(text, regex) {
|
||||
const matches = text.match(regex);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
function parseText(text) {
|
||||
return parser.parse(text, {
|
||||
sourceType: "unambiguous",
|
||||
plugins: ["jsx"]
|
||||
});
|
||||
}
|
||||
|
||||
function printNode(node) {
|
||||
return generator(node, {
|
||||
compact: true,
|
||||
comments: false,
|
||||
jsescOption: { minimal: true }
|
||||
}).code;
|
||||
}
|
||||
|
||||
function isLiteralLike(node) {
|
||||
return (
|
||||
t.isStringLiteral(node) ||
|
||||
t.isNumericLiteral(node) ||
|
||||
t.isBooleanLiteral(node) ||
|
||||
t.isNullLiteral(node) ||
|
||||
t.isBigIntLiteral(node) ||
|
||||
t.isIdentifier(node, { name: "undefined" })
|
||||
);
|
||||
}
|
||||
|
||||
function isOpcodeDiscriminant(node) {
|
||||
return t.isIdentifier(node) || t.isMemberExpression(node);
|
||||
}
|
||||
|
||||
function getOpcodeIfDescriptor(node) {
|
||||
if (!t.isIfStatement(node) || node.alternate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const test = node.test;
|
||||
if (!t.isBinaryExpression(test) || !["==", "===", "!=", "!=="].includes(test.operator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLiteralLike(test.left) && isOpcodeDiscriminant(test.right)) {
|
||||
return {
|
||||
discriminantKey: printNode(test.right),
|
||||
literalKey: printNode(test.left)
|
||||
};
|
||||
}
|
||||
|
||||
if (isLiteralLike(test.right) && isOpcodeDiscriminant(test.left)) {
|
||||
return {
|
||||
discriminantKey: printNode(test.left),
|
||||
literalKey: printNode(test.right)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function countOpcodeIfChains(ast) {
|
||||
const statementLists = [];
|
||||
|
||||
traverse(ast, {
|
||||
Program(path) {
|
||||
statementLists.push(path.node.body);
|
||||
},
|
||||
BlockStatement(path) {
|
||||
statementLists.push(path.node.body);
|
||||
},
|
||||
SwitchCase(path) {
|
||||
statementLists.push(path.node.consequent);
|
||||
}
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
statementLists.forEach((statements) => {
|
||||
for (let index = 0; index < statements.length; index += 1) {
|
||||
const firstDescriptor = getOpcodeIfDescriptor(statements[index]);
|
||||
if (!firstDescriptor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const seenLiterals = new Set([firstDescriptor.literalKey]);
|
||||
let chainLength = 1;
|
||||
let cursor = index + 1;
|
||||
while (cursor < statements.length) {
|
||||
const nextDescriptor = getOpcodeIfDescriptor(statements[cursor]);
|
||||
if (!nextDescriptor || nextDescriptor.discriminantKey !== firstDescriptor.discriminantKey) {
|
||||
break;
|
||||
}
|
||||
seenLiterals.add(nextDescriptor.literalKey);
|
||||
chainLength += 1;
|
||||
cursor += 1;
|
||||
}
|
||||
|
||||
if (chainLength >= 2 && seenLiterals.size >= 2) {
|
||||
count += chainLength;
|
||||
index = cursor - 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function getReturnArgument(fnNode) {
|
||||
if (t.isArrowFunctionExpression(fnNode) && !t.isBlockStatement(fnNode.body)) {
|
||||
return fnNode.body;
|
||||
}
|
||||
|
||||
if (!t.isFunctionExpression(fnNode) && !t.isArrowFunctionExpression(fnNode)) {
|
||||
return null;
|
||||
}
|
||||
if (!t.isBlockStatement(fnNode.body) || fnNode.body.body.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [statement] = fnNode.body.body;
|
||||
return t.isReturnStatement(statement) ? statement.argument : null;
|
||||
}
|
||||
|
||||
function collectReferencedParameterNames(node) {
|
||||
const names = new Set();
|
||||
if (!node) {
|
||||
return names;
|
||||
}
|
||||
|
||||
traverse(t.file(t.program([t.expressionStatement(t.cloneNode(node, true))])), {
|
||||
noScope: true,
|
||||
Identifier(path) {
|
||||
if (!path.isReferencedIdentifier()) {
|
||||
return;
|
||||
}
|
||||
names.add(path.node.name);
|
||||
}
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
function isThinWrapperFunction(fnNode) {
|
||||
const returnArgument = getReturnArgument(fnNode);
|
||||
if (!returnArgument) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paramNames = new Set(
|
||||
fnNode.params.filter((param) => t.isIdentifier(param)).map((param) => param.name)
|
||||
);
|
||||
if (paramNames.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const referencedNames = collectReferencedParameterNames(returnArgument);
|
||||
for (const name of referencedNames) {
|
||||
if (!paramNames.has(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (t.isBinaryExpression(returnArgument) || t.isLogicalExpression(returnArgument)) {
|
||||
return true;
|
||||
}
|
||||
if (t.isUnaryExpression(returnArgument) && t.isIdentifier(returnArgument.argument)) {
|
||||
return true;
|
||||
}
|
||||
if (t.isCallExpression(returnArgument)) {
|
||||
return t.isIdentifier(returnArgument.callee) || t.isMemberExpression(returnArgument.callee);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function countDispatcherWrappers(ast) {
|
||||
let count = 0;
|
||||
|
||||
traverse(ast, {
|
||||
ObjectExpression(path) {
|
||||
let thinWrapperCount = 0;
|
||||
let literalPropertyCount = 0;
|
||||
|
||||
path.get("properties").forEach((propertyPath) => {
|
||||
if (!propertyPath.isObjectProperty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valuePath = propertyPath.get("value");
|
||||
if (valuePath.isStringLiteral() || valuePath.isNumericLiteral() || valuePath.isBooleanLiteral() || valuePath.isNullLiteral()) {
|
||||
literalPropertyCount += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThinWrapperFunction(valuePath.node)) {
|
||||
thinWrapperCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const looksLikeDispatcherObject =
|
||||
thinWrapperCount >= 2 || (thinWrapperCount >= 1 && literalPropertyCount >= 2);
|
||||
if (looksLikeDispatcherObject) {
|
||||
count += thinWrapperCount;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function collectMetrics(text) {
|
||||
let opcodeIfChainCount = countMatches(text, /if\s*\(\s*(?:0x[\da-f]+|\d+|["'][^"'\\]*(?:\\.[^"'\\]*)*["'])\s*={2,3}\s*[A-Za-z_$][\w$]*\s*\)/gi);
|
||||
let dispatcherWrapperCount = countMatches(text, /:\s*function\s*\([^)]*\)\s*\{\s*return\b/g);
|
||||
|
||||
try {
|
||||
const ast = parseText(text);
|
||||
opcodeIfChainCount = countOpcodeIfChains(ast);
|
||||
dispatcherWrapperCount = countDispatcherWrappers(ast);
|
||||
} catch (error) {
|
||||
// Keep regex fallbacks when the file is not parseable.
|
||||
}
|
||||
|
||||
return {
|
||||
lineCount: text.split(/\r?\n/).length,
|
||||
splitPipeCount: countMatches(text, /\.split\(\s*["']\|["']\s*\)/g),
|
||||
loopSwitchCount: countMatches(text, /(?:while|for)\s*\([^)]*\)\s*\{[\s\S]{0,120}?switch\s*\(/g),
|
||||
opcodeIfChainCount,
|
||||
dispatcherWrapperCount,
|
||||
hexIdentifierCount: countMatches(text, /\b_0x[a-f0-9]+\b/gi)
|
||||
};
|
||||
}
|
||||
|
||||
function collectMetricsFromFile(filePath) {
|
||||
const text = fs.readFileSync(filePath, "utf8");
|
||||
return {
|
||||
filePath: path.resolve(filePath),
|
||||
...collectMetrics(text)
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [, , inputPath, outputPath = ""] = process.argv;
|
||||
if (!inputPath) {
|
||||
console.error("Usage: node collect-residue-metrics.js <input.js> [output.json]");
|
||||
process.exit(1);
|
||||
}
|
||||
const result = collectMetricsFromFile(inputPath);
|
||||
const json = JSON.stringify(result, null, 2);
|
||||
if (outputPath) {
|
||||
fs.writeFileSync(outputPath, json, "utf8");
|
||||
} else {
|
||||
process.stdout.write(`${json}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
collectMetrics,
|
||||
collectMetricsFromFile
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { collectMetricsFromFile } = require("./collect-residue-metrics");
|
||||
|
||||
function pushGap(gaps, condition, message) {
|
||||
if (condition) {
|
||||
gaps.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
function compareOutputs(caseId, oursPath, referencePath) {
|
||||
const ours = collectMetricsFromFile(oursPath);
|
||||
const reference = collectMetricsFromFile(referencePath);
|
||||
const gaps = [];
|
||||
|
||||
pushGap(gaps, ours.splitPipeCount > reference.splitPipeCount, "still contains unresolved split('|') order sources");
|
||||
pushGap(gaps, ours.loopSwitchCount > reference.loopSwitchCount, "still contains direct loop/switch flattening");
|
||||
pushGap(gaps, ours.opcodeIfChainCount > reference.opcodeIfChainCount, "still contains opcode if-chain residue");
|
||||
pushGap(gaps, ours.dispatcherWrapperCount > reference.dispatcherWrapperCount, "still contains uninlined dispatcher wrappers");
|
||||
pushGap(gaps, ours.hexIdentifierCount > reference.hexIdentifierCount, "still contains more _0x-style identifiers than the reference");
|
||||
pushGap(gaps, ours.lineCount > reference.lineCount * 1.2, "output is still significantly noisier than the reference");
|
||||
|
||||
return {
|
||||
caseId,
|
||||
ours,
|
||||
reference,
|
||||
gaps,
|
||||
status: gaps.length === 0 ? "matched-or-better-on-heuristics" : "residue-remains"
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [, , oursPath, referencePath, outputPath = "", caseId = "unknown"] = process.argv;
|
||||
if (!oursPath || !referencePath) {
|
||||
console.error("Usage: node compare-with-reference.js <ours.js> <reference.js> [output.json] [caseId]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = compareOutputs(caseId, oursPath, referencePath);
|
||||
const json = JSON.stringify(result, null, 2);
|
||||
if (outputPath) {
|
||||
fs.writeFileSync(outputPath, json, "utf8");
|
||||
} else {
|
||||
process.stdout.write(`${json}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compareOutputs
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { detectPatterns } = require("./pipeline-config");
|
||||
|
||||
function main() {
|
||||
const [, , inputPath, hint = "", outputPath = ""] = process.argv;
|
||||
if (!inputPath) {
|
||||
console.error("Usage: node detect-patterns.js <input.js> [hint] [output.json]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceText = fs.readFileSync(inputPath, "utf8");
|
||||
const result = detectPatterns({
|
||||
inputPath: path.resolve(inputPath),
|
||||
hint,
|
||||
sourceText
|
||||
});
|
||||
|
||||
const serializable = {
|
||||
inputPath: path.resolve(inputPath),
|
||||
hint,
|
||||
bestId: result.bestId,
|
||||
selected: {
|
||||
id: result.selected.id,
|
||||
displayName: result.selected.displayName,
|
||||
reference: result.selected.reference,
|
||||
families: result.selected.families,
|
||||
notes: result.selected.notes
|
||||
},
|
||||
detections: result.detections
|
||||
};
|
||||
|
||||
const json = JSON.stringify(serializable, null, 2);
|
||||
if (outputPath) {
|
||||
fs.writeFileSync(outputPath, json, "utf8");
|
||||
} else {
|
||||
process.stdout.write(`${json}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,775 @@
|
||||
const generator = require("@babel/generator").default;
|
||||
const { clone, parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
function getCaseStatements(casePath) {
|
||||
return casePath.get("consequent").filter(stmt => !stmt.isContinueStatement() && !stmt.isBreakStatement()).map(stmt => clone(stmt.node));
|
||||
}
|
||||
|
||||
function hasLexicalBindings(nodes) {
|
||||
return nodes.some((node) => t.isVariableDeclaration(node) && node.kind !== "var");
|
||||
}
|
||||
|
||||
function isSimpleDiscriminant(node) {
|
||||
return t.isIdentifier(node) || t.isLiteral(node);
|
||||
}
|
||||
|
||||
function normalizeCaseConsequent(nodes) {
|
||||
const statements = nodes.map((node) => clone(node));
|
||||
if (statements.length === 0) {
|
||||
return [t.continueStatement()];
|
||||
}
|
||||
|
||||
const last = statements[statements.length - 1];
|
||||
if (t.isBreakStatement(last)) {
|
||||
statements.pop();
|
||||
statements.push(t.continueStatement());
|
||||
return statements;
|
||||
}
|
||||
if (t.isContinueStatement(last) || t.isReturnStatement(last) || t.isThrowStatement(last)) {
|
||||
return statements;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildIfChainFromSwitch(switchNode) {
|
||||
if (!isSimpleDiscriminant(switchNode.discriminant)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let root = null;
|
||||
let current = null;
|
||||
let defaultBlock = null;
|
||||
|
||||
for (const caseNode of switchNode.cases) {
|
||||
const normalized = normalizeCaseConsequent(caseNode.consequent);
|
||||
if (!normalized || hasLexicalBindings(normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (caseNode.test === null) {
|
||||
defaultBlock = t.blockStatement(normalized);
|
||||
continue;
|
||||
}
|
||||
|
||||
const test = t.logicalExpression(
|
||||
"&&",
|
||||
t.binaryExpression("===", clone(switchNode.discriminant), clone(caseNode.test)),
|
||||
t.booleanLiteral(true)
|
||||
);
|
||||
const nextIf = t.ifStatement(test, t.blockStatement(normalized));
|
||||
|
||||
if (!root) {
|
||||
root = nextIf;
|
||||
} else {
|
||||
current.alternate = nextIf;
|
||||
}
|
||||
current = nextIf;
|
||||
}
|
||||
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
if (current && defaultBlock) {
|
||||
current.alternate = defaultBlock;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function replaceLoopSwitchWithIf(path) {
|
||||
const body = path.get("body");
|
||||
if (!body.isBlockStatement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const statements = body.get("body");
|
||||
if (statements.length !== 1 || !statements[0].isSwitchStatement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ifChain = buildIfChainFromSwitch(statements[0].node);
|
||||
if (!ifChain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
body.replaceWith(t.blockStatement([ifChain]));
|
||||
return true;
|
||||
}
|
||||
|
||||
const UNRESOLVED = Symbol("unresolved");
|
||||
|
||||
function getResolvedFunctionCallPath(functionPath) {
|
||||
if (functionPath.parentPath.isCallExpression() && functionPath.parentKey === "callee") {
|
||||
return functionPath.parentPath;
|
||||
}
|
||||
if (
|
||||
functionPath.parentPath.isUnaryExpression() &&
|
||||
functionPath.parentPath.parentPath &&
|
||||
functionPath.parentPath.parentPath.isCallExpression() &&
|
||||
functionPath.parentPath.parentKey === "callee"
|
||||
) {
|
||||
return functionPath.parentPath.parentPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveObjectPropertyFromBinding(objectPath, propertyValue, seenBindings = new Set()) {
|
||||
if (!objectPath || !objectPath.node || !objectPath.isIdentifier()) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
const binding = objectPath.scope.getBinding(objectPath.node.name);
|
||||
if (!binding || seenBindings.has(binding)) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
seenBindings.add(binding);
|
||||
|
||||
const declaratorPath = getDeclaratorForBinding(binding);
|
||||
if (!declaratorPath) {
|
||||
seenBindings.delete(binding);
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
const initPath = declaratorPath.get("init");
|
||||
if (!initPath.node) {
|
||||
seenBindings.delete(binding);
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
if (initPath.isIdentifier()) {
|
||||
const resolved = resolveObjectPropertyFromBinding(initPath, propertyValue, seenBindings);
|
||||
seenBindings.delete(binding);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
if (!initPath.isObjectExpression()) {
|
||||
seenBindings.delete(binding);
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
for (const propertyPath of initPath.get("properties")) {
|
||||
if (!propertyPath.isObjectProperty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keyPath = propertyPath.get("key");
|
||||
const key = propertyPath.node.computed ? resolveStatic(keyPath, seenBindings) : keyPath.isIdentifier() ? keyPath.node.name : resolveStatic(keyPath, seenBindings);
|
||||
if (key === UNRESOLVED || String(key) !== String(propertyValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolved = resolveStatic(propertyPath.get("value"), seenBindings);
|
||||
seenBindings.delete(binding);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
seenBindings.delete(binding);
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
function resolveStatic(path, seenBindings = new Set()) {
|
||||
if (!path || !path.node) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
if (path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()) {
|
||||
return path.node.value;
|
||||
}
|
||||
if (path.isNullLiteral()) {
|
||||
return null;
|
||||
}
|
||||
if (path.isTemplateLiteral() && path.node.expressions.length === 0) {
|
||||
return path.node.quasis[0].value.cooked;
|
||||
}
|
||||
if (path.isIdentifier({ name: "undefined" })) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (path.isArrayExpression()) {
|
||||
const values = [];
|
||||
for (const elementPath of path.get("elements")) {
|
||||
if (!elementPath.node) {
|
||||
values.push(undefined);
|
||||
continue;
|
||||
}
|
||||
const value = resolveStatic(elementPath, seenBindings);
|
||||
if (value === UNRESOLVED) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
if (path.isObjectExpression()) {
|
||||
const out = Object.create(null);
|
||||
for (const propPath of path.get("properties")) {
|
||||
if (!propPath.isObjectProperty()) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
const keyPath = propPath.get("key");
|
||||
const key = propPath.node.computed ? resolveStatic(keyPath, seenBindings) : keyPath.isIdentifier() ? keyPath.node.name : resolveStatic(keyPath, seenBindings);
|
||||
if (key === UNRESOLVED) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
const value = resolveStatic(propPath.get("value"), seenBindings);
|
||||
if (value === UNRESOLVED) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (path.isIdentifier()) {
|
||||
const binding = path.scope.getBinding(path.node.name);
|
||||
if (!binding || seenBindings.has(binding)) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
seenBindings.add(binding);
|
||||
|
||||
let resolved = UNRESOLVED;
|
||||
if (binding.path.parentPath && binding.path.parentPath.isVariableDeclarator()) {
|
||||
resolved = resolveStatic(binding.path.parentPath.get("init"), seenBindings);
|
||||
} else if (binding.kind === "param" && binding.path.parentPath && binding.path.parentPath.isFunction()) {
|
||||
const callPath = getResolvedFunctionCallPath(binding.path.parentPath);
|
||||
if (callPath && typeof binding.path.key === "number") {
|
||||
resolved = resolveStatic(callPath.get(`arguments.${binding.path.key}`), seenBindings);
|
||||
}
|
||||
}
|
||||
|
||||
seenBindings.delete(binding);
|
||||
if (resolved !== UNRESOLVED) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
if (path.isMemberExpression()) {
|
||||
const propertyValue = path.node.computed
|
||||
? resolveStatic(path.get("property"), seenBindings)
|
||||
: path.get("property").isIdentifier()
|
||||
? path.node.property.name
|
||||
: resolveStatic(path.get("property"), seenBindings);
|
||||
if (propertyValue === UNRESOLVED) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
const directPropertyValue = resolveObjectPropertyFromBinding(path.get("object"), propertyValue, seenBindings);
|
||||
if (directPropertyValue !== UNRESOLVED) {
|
||||
return directPropertyValue;
|
||||
}
|
||||
|
||||
const objectValue = resolveStatic(path.get("object"), seenBindings);
|
||||
if (objectValue === UNRESOLVED) {
|
||||
return UNRESOLVED;
|
||||
}
|
||||
if (Array.isArray(objectValue)) {
|
||||
const index = Number(propertyValue);
|
||||
return Number.isInteger(index) ? objectValue[index] : UNRESOLVED;
|
||||
}
|
||||
if (objectValue && typeof objectValue === "object" && propertyValue in objectValue) {
|
||||
return objectValue[propertyValue];
|
||||
}
|
||||
return UNRESOLVED;
|
||||
}
|
||||
|
||||
if (
|
||||
path.isCallExpression() &&
|
||||
path.get("callee").isMemberExpression() &&
|
||||
(
|
||||
path.get("callee.property").isIdentifier({ name: "split" }) ||
|
||||
path.get("callee.property").isStringLiteral({ value: "split" })
|
||||
) &&
|
||||
path.get("arguments.0").isStringLiteral()
|
||||
) {
|
||||
const source = resolveStatic(path.get("callee.object"), seenBindings);
|
||||
if (typeof source === "string") {
|
||||
return source.split(path.node.arguments[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
const evaluated = path.evaluate();
|
||||
return evaluated.confident ? evaluated.value : UNRESOLVED;
|
||||
}
|
||||
|
||||
function getLiteralValue(path) {
|
||||
const resolved = resolveStatic(path);
|
||||
if (typeof resolved === "string" || typeof resolved === "number") {
|
||||
return resolved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeKey(node) {
|
||||
return generator(node, {
|
||||
compact: true,
|
||||
comments: false,
|
||||
jsescOption: { minimal: true }
|
||||
}).code;
|
||||
}
|
||||
|
||||
function getCaseKey(casePath) {
|
||||
const value = getLiteralValue(casePath.get("test"));
|
||||
return value === null ? normalizeKey(casePath.node.test) : value;
|
||||
}
|
||||
|
||||
function getOrderFromInit(initPath) {
|
||||
if (initPath.isArrayExpression()) {
|
||||
const values = [];
|
||||
for (const elementPath of initPath.get("elements")) {
|
||||
if (!elementPath.node) {
|
||||
return null;
|
||||
}
|
||||
const value = getLiteralValue(elementPath);
|
||||
values.push(value === null ? normalizeKey(elementPath.node) : value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
if (
|
||||
initPath.isCallExpression() &&
|
||||
initPath.get("callee").isMemberExpression() &&
|
||||
(
|
||||
initPath.get("callee.property").isIdentifier({ name: "split" }) ||
|
||||
initPath.get("callee.property").isStringLiteral({ value: "split" })
|
||||
) &&
|
||||
initPath.get("arguments.0").isStringLiteral()
|
||||
) {
|
||||
const source = getLiteralValue(initPath.get("callee.object"));
|
||||
if (typeof source === "string") {
|
||||
return source.split(initPath.node.arguments[0].value);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getOrderFromResolvedValue(value) {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const order = [];
|
||||
for (const item of value) {
|
||||
if (typeof item === "string" || typeof item === "number") {
|
||||
order.push(item);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
function getDeclaratorForBinding(binding) {
|
||||
if (!binding || !binding.path) {
|
||||
return null;
|
||||
}
|
||||
if (binding.path.isVariableDeclarator()) {
|
||||
return binding.path;
|
||||
}
|
||||
if (binding.path.parentPath && binding.path.parentPath.isVariableDeclarator()) {
|
||||
return binding.path.parentPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getOrderFromBinding(binding) {
|
||||
const declaratorPath = getDeclaratorForBinding(binding);
|
||||
if (!declaratorPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initPath = declaratorPath.get("init");
|
||||
if (!initPath.node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getOrderFromInit(initPath) || getOrderFromResolvedValue(resolveStatic(initPath));
|
||||
}
|
||||
|
||||
function getOrderFromSplitCallWithObjectTable(initPath, loopPath) {
|
||||
if (
|
||||
!initPath.isCallExpression() ||
|
||||
!initPath.get("callee").isMemberExpression() ||
|
||||
!(
|
||||
initPath.get("callee.property").isIdentifier({ name: "split" }) ||
|
||||
initPath.get("callee.property").isStringLiteral({ value: "split" })
|
||||
) ||
|
||||
!initPath.get("arguments.0").isStringLiteral()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourcePath = initPath.get("callee.object");
|
||||
if (!sourcePath.isMemberExpression() || !sourcePath.get("object").isIdentifier()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectName = sourcePath.get("object").node.name;
|
||||
const propertyKey = sourcePath.node.computed ? getStaticPropertyKey(sourcePath.node.property) : getStaticPropertyKey(sourcePath.node.property);
|
||||
if (propertyKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = collectObjectPipeStringCandidates(loopPath, objectName).find((candidate) => candidate.key === propertyKey);
|
||||
return match ? match.order : null;
|
||||
}
|
||||
|
||||
function getOrderFromDiscriminant(discriminantPath) {
|
||||
if (!discriminantPath.isMemberExpression()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const objectPath = discriminantPath.get("object");
|
||||
const directOrder = getOrderFromResolvedValue(resolveStatic(objectPath));
|
||||
if (directOrder) {
|
||||
return directOrder;
|
||||
}
|
||||
|
||||
if (!objectPath.isIdentifier()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const binding = objectPath.scope.getBinding(objectPath.node.name);
|
||||
return getOrderFromBinding(binding);
|
||||
}
|
||||
|
||||
function findNearbyOrderDeclaration(loopPath, maxSiblings = 3) {
|
||||
let siblingPath = loopPath.getPrevSibling();
|
||||
for (let seen = 0; siblingPath && siblingPath.node && seen < maxSiblings; seen += 1) {
|
||||
if (!siblingPath.isVariableDeclaration()) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const declaratorPath of siblingPath.get("declarations")) {
|
||||
if (!declaratorPath.node || !declaratorPath.get("init").node) {
|
||||
continue;
|
||||
}
|
||||
const order =
|
||||
getOrderFromInit(declaratorPath.get("init")) ||
|
||||
getOrderFromSplitCallWithObjectTable(declaratorPath.get("init"), loopPath) ||
|
||||
getOrderFromResolvedValue(resolveStatic(declaratorPath.get("init")));
|
||||
if (order) {
|
||||
return {
|
||||
order,
|
||||
declarationPath: siblingPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
siblingPath = siblingPath.getPrevSibling();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildBlocksFromSwitchOrder(switchStmt, order) {
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const caseMap = new Map();
|
||||
switchStmt.get("cases").forEach(casePath => {
|
||||
if (!casePath.node.test) {
|
||||
return;
|
||||
}
|
||||
caseMap.set(getCaseKey(casePath), getCaseStatements(casePath));
|
||||
});
|
||||
|
||||
const blocks = [];
|
||||
order.forEach(key => {
|
||||
const stmts = caseMap.get(key);
|
||||
if (stmts) {
|
||||
blocks.push(...stmts);
|
||||
}
|
||||
});
|
||||
|
||||
return blocks.length > 0 ? blocks : null;
|
||||
}
|
||||
|
||||
function getStaticPropertyKey(node) {
|
||||
if (t.isIdentifier(node)) {
|
||||
return node.name;
|
||||
}
|
||||
if (t.isStringLiteral(node) || t.isNumericLiteral(node)) {
|
||||
return String(node.value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveObjectAliasNames(loopPath, objectName) {
|
||||
const names = new Set();
|
||||
let currentName = objectName;
|
||||
|
||||
while (currentName) {
|
||||
if (names.has(currentName)) {
|
||||
break;
|
||||
}
|
||||
names.add(currentName);
|
||||
|
||||
const binding = loopPath.scope.getBinding(currentName);
|
||||
const declaratorPath = getDeclaratorForBinding(binding);
|
||||
if (!declaratorPath || !declaratorPath.get("init").isIdentifier()) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentName = declaratorPath.get("init").node.name;
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
function splitPipeString(value) {
|
||||
if (typeof value !== "string" || !value.includes("|")) {
|
||||
return null;
|
||||
}
|
||||
const parts = value.split("|");
|
||||
return parts.length > 1 ? parts : null;
|
||||
}
|
||||
|
||||
function getPipeOrderFromValuePath(valuePath) {
|
||||
if (!valuePath || !valuePath.node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (valuePath.isStringLiteral()) {
|
||||
return splitPipeString(valuePath.node.value);
|
||||
}
|
||||
|
||||
const resolved = resolveStatic(valuePath);
|
||||
return splitPipeString(resolved);
|
||||
}
|
||||
|
||||
function getCaseKeys(switchStmt) {
|
||||
const keys = [];
|
||||
switchStmt.get("cases").forEach(casePath => {
|
||||
if (!casePath.node.test) {
|
||||
return;
|
||||
}
|
||||
keys.push(getCaseKey(casePath));
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
function sameSequenceKeySet(order, caseKeys) {
|
||||
if (!order || order.length !== caseKeys.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const counts = new Map();
|
||||
caseKeys.forEach((key) => counts.set(String(key), (counts.get(String(key)) || 0) + 1));
|
||||
for (const item of order) {
|
||||
const key = String(item);
|
||||
const count = counts.get(key);
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
if (count === 1) {
|
||||
counts.delete(key);
|
||||
} else {
|
||||
counts.set(key, count - 1);
|
||||
}
|
||||
}
|
||||
return counts.size === 0;
|
||||
}
|
||||
|
||||
function getAncestorBodyStatements(loopPath) {
|
||||
const bodies = [];
|
||||
const seen = new Set();
|
||||
let current = loopPath;
|
||||
|
||||
while (current) {
|
||||
if (current.isProgram()) {
|
||||
const key = `program:${current.node.start || 0}`;
|
||||
if (!seen.has(key)) {
|
||||
bodies.push(current.get("body"));
|
||||
seen.add(key);
|
||||
}
|
||||
} else if (current.isBlockStatement() && Array.isArray(current.node.body)) {
|
||||
const key = `block:${current.node.start || 0}`;
|
||||
if (!seen.has(key)) {
|
||||
bodies.push(current.get("body"));
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
current = current.parentPath;
|
||||
}
|
||||
|
||||
return bodies;
|
||||
}
|
||||
|
||||
function collectObjectPipeStringCandidates(loopPath, objectName) {
|
||||
if (!objectName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const objectNames = resolveObjectAliasNames(loopPath, objectName);
|
||||
const candidates = [];
|
||||
const loopStart = loopPath.node.start || 0;
|
||||
getAncestorBodyStatements(loopPath).forEach((statements) => {
|
||||
statements.forEach((stmtPath) => {
|
||||
if (!stmtPath.node || (stmtPath.node.start || 0) >= loopStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stmtPath.isVariableDeclaration()) {
|
||||
stmtPath.get("declarations").forEach((declPath) => {
|
||||
if (!declPath.get("id").isIdentifier() || !objectNames.has(declPath.get("id").node.name) || !declPath.get("init").isObjectExpression()) {
|
||||
return;
|
||||
}
|
||||
declPath.get("init.properties").forEach((propPath) => {
|
||||
if (!propPath.isObjectProperty()) {
|
||||
return;
|
||||
}
|
||||
const keyPath = propPath.get("key");
|
||||
const key = propPath.node.computed ? getStaticPropertyKey(keyPath.node) : getStaticPropertyKey(keyPath.node);
|
||||
const valuePath = propPath.get("value");
|
||||
const pipeOrder = getPipeOrderFromValuePath(valuePath);
|
||||
if (pipeOrder) {
|
||||
candidates.push({ key, order: pipeOrder, start: stmtPath.node.start || 0 });
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stmtPath.isExpressionStatement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exprPath = stmtPath.get("expression");
|
||||
if (!exprPath.isAssignmentExpression({ operator: "=" })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftPath = exprPath.get("left");
|
||||
const rightPath = exprPath.get("right");
|
||||
if (!leftPath.isMemberExpression() || !leftPath.get("object").isIdentifier() || !objectNames.has(leftPath.get("object").node.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = leftPath.node.computed ? getStaticPropertyKey(leftPath.node.property) : getStaticPropertyKey(leftPath.node.property);
|
||||
if (key === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pipeOrder = getPipeOrderFromValuePath(rightPath);
|
||||
if (pipeOrder) {
|
||||
candidates.push({ key, order: pipeOrder, start: stmtPath.node.start || 0 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
candidates.sort((a, b) => b.start - a.start);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function getOrderFromObjectStringTable(loopPath, switchStmt) {
|
||||
const discriminant = switchStmt.get("discriminant");
|
||||
if (!discriminant.isMemberExpression() || !discriminant.get("object").isIdentifier()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = collectObjectPipeStringCandidates(loopPath, discriminant.get("object").node.name);
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const caseKeys = getCaseKeys(switchStmt);
|
||||
const match = candidates.find((candidate) => sameSequenceKeySet(candidate.order, caseKeys));
|
||||
return match ? match.order : null;
|
||||
}
|
||||
|
||||
function flatten(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
ForStatement(path) {
|
||||
const body = path.get("body");
|
||||
if (!body.isBlockStatement() || body.get("body").length === 0) {
|
||||
return;
|
||||
}
|
||||
const firstStmt = body.get("body.0");
|
||||
if (firstStmt.isSwitchStatement() && path.get("init").isVariableDeclaration() && path.node.update === null) {
|
||||
let order = null;
|
||||
const leftovers = [];
|
||||
path.get("init.declarations").forEach(decl => {
|
||||
if (decl.node.init === null) {
|
||||
leftovers.push(t.variableDeclaration("var", [clone(decl.node)]));
|
||||
return;
|
||||
}
|
||||
const candidate =
|
||||
getOrderFromInit(decl.get("init")) ||
|
||||
getOrderFromSplitCallWithObjectTable(decl.get("init"), path) ||
|
||||
getOrderFromResolvedValue(resolveStatic(decl.get("init")));
|
||||
if (candidate) {
|
||||
order = candidate;
|
||||
}
|
||||
});
|
||||
if (order) {
|
||||
const caseMap = new Map();
|
||||
firstStmt.get("cases").forEach(casePath => {
|
||||
if (!casePath.node.test) {
|
||||
return;
|
||||
}
|
||||
caseMap.set(getCaseKey(casePath), getCaseStatements(casePath));
|
||||
});
|
||||
const blocks = [...leftovers];
|
||||
order.forEach(key => {
|
||||
const stmts = caseMap.get(key);
|
||||
if (stmts) {
|
||||
blocks.push(...stmts);
|
||||
}
|
||||
});
|
||||
if (blocks.length > 0) {
|
||||
path.replaceWithMultiple(blocks);
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceLoopSwitchWithIf(path)) {
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
WhileStatement(path) {
|
||||
if (path.get("test").isUnaryExpression() && path.get("body").isBlockStatement()) {
|
||||
const switchStmt = path.get("body.body.0");
|
||||
if (switchStmt.isSwitchStatement()) {
|
||||
const nearby = findNearbyOrderDeclaration(path);
|
||||
const order =
|
||||
(nearby && nearby.order) ||
|
||||
getOrderFromDiscriminant(switchStmt.get("discriminant")) ||
|
||||
getOrderFromObjectStringTable(path, switchStmt);
|
||||
const blocks = buildBlocksFromSwitchOrder(switchStmt, order);
|
||||
if (blocks) {
|
||||
path.replaceWithMultiple(blocks);
|
||||
if (nearby && nearby.declarationPath && nearby.declarationPath.node) {
|
||||
nearby.declarationPath.remove();
|
||||
}
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceLoopSwitchWithIf(path)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
let changed = false;
|
||||
do {
|
||||
({ ast, changed } = flatten(ast));
|
||||
if (changed) {
|
||||
ast = reparse(ast);
|
||||
}
|
||||
} while (changed);
|
||||
saveAst(ast, outputPath);
|
||||
@@ -0,0 +1,256 @@
|
||||
const generator = require("@babel/generator").default;
|
||||
const { clone, parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
function normalizeCode(node) {
|
||||
return generator(node, {
|
||||
compact: true,
|
||||
comments: false,
|
||||
jsescOption: { minimal: true }
|
||||
}).code;
|
||||
}
|
||||
|
||||
function getCaseInfo(testNode) {
|
||||
if (!t.isBinaryExpression(testNode) || !["==", "==="].includes(testNode.operator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leftLiteral = t.isLiteral(testNode.left);
|
||||
const rightLiteral = t.isLiteral(testNode.right);
|
||||
if (leftLiteral === rightLiteral) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (leftLiteral) {
|
||||
return {
|
||||
discriminant: testNode.right,
|
||||
caseValue: testNode.left
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
discriminant: testNode.left,
|
||||
caseValue: testNode.right
|
||||
};
|
||||
}
|
||||
|
||||
function toCaseStatements(node) {
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
if (t.isBlockStatement(node)) {
|
||||
return node.body.map((stmt) => clone(stmt));
|
||||
}
|
||||
return [clone(node)];
|
||||
}
|
||||
|
||||
function unwrapAlternate(path) {
|
||||
if (!path.node) {
|
||||
return { kind: "empty", path: null, node: null };
|
||||
}
|
||||
if (path.isIfStatement()) {
|
||||
return { kind: "if", path, node: path.node };
|
||||
}
|
||||
if (path.isBlockStatement()) {
|
||||
const body = path.get("body");
|
||||
if (body.length === 1 && body[0].isIfStatement()) {
|
||||
return { kind: "if", path: body[0], node: body[0].node };
|
||||
}
|
||||
return { kind: "default", path, node: path.node };
|
||||
}
|
||||
return { kind: "default", path, node: path.node };
|
||||
}
|
||||
|
||||
function isTerminalStatement(node) {
|
||||
return t.isReturnStatement(node) || t.isThrowStatement(node) || t.isBreakStatement(node) || t.isContinueStatement(node);
|
||||
}
|
||||
|
||||
function collectIfChain(path) {
|
||||
if (path.parentPath.isIfStatement() && path.key === "alternate") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentPath = path;
|
||||
let discriminantCode = null;
|
||||
let discriminantNode = null;
|
||||
const cases = [];
|
||||
let defaultStatements = null;
|
||||
|
||||
while (currentPath && currentPath.isIfStatement()) {
|
||||
const info = getCaseInfo(currentPath.node.test);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentCode = normalizeCode(info.discriminant);
|
||||
if (discriminantCode === null) {
|
||||
discriminantCode = currentCode;
|
||||
discriminantNode = clone(info.discriminant);
|
||||
} else if (currentCode !== discriminantCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cases.push({
|
||||
test: clone(info.caseValue),
|
||||
statements: toCaseStatements(currentPath.node.consequent)
|
||||
});
|
||||
|
||||
const alternate = unwrapAlternate(currentPath.get("alternate"));
|
||||
if (alternate.kind === "empty") {
|
||||
break;
|
||||
}
|
||||
if (alternate.kind === "if") {
|
||||
currentPath = alternate.path;
|
||||
continue;
|
||||
}
|
||||
defaultStatements = toCaseStatements(alternate.node);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cases.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
discriminant: discriminantNode,
|
||||
cases,
|
||||
defaultStatements
|
||||
};
|
||||
}
|
||||
|
||||
function buildSwitchStatementFromChain(chain) {
|
||||
const switchCases = chain.cases.map((entry) => {
|
||||
const statements = entry.statements.slice();
|
||||
if (statements.length === 0 || !isTerminalStatement(statements[statements.length - 1])) {
|
||||
statements.push(t.breakStatement());
|
||||
}
|
||||
return t.switchCase(entry.test, statements);
|
||||
});
|
||||
|
||||
if (chain.defaultStatements) {
|
||||
switchCases.push(t.switchCase(null, chain.defaultStatements));
|
||||
}
|
||||
|
||||
return t.switchStatement(chain.discriminant, switchCases);
|
||||
}
|
||||
|
||||
function collectConsecutiveIfCases(statementPaths, startIndex) {
|
||||
const firstPath = statementPaths[startIndex];
|
||||
if (!firstPath || !firstPath.isIfStatement() || firstPath.node.alternate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstInfo = getCaseInfo(firstPath.node.test);
|
||||
if (!firstInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const discriminantCode = normalizeCode(firstInfo.discriminant);
|
||||
const cases = [{
|
||||
test: clone(firstInfo.caseValue),
|
||||
statements: toCaseStatements(firstPath.node.consequent)
|
||||
}];
|
||||
|
||||
let endIndex = startIndex + 1;
|
||||
while (endIndex < statementPaths.length) {
|
||||
const nextPath = statementPaths[endIndex];
|
||||
if (!nextPath.isIfStatement() || nextPath.node.alternate) {
|
||||
break;
|
||||
}
|
||||
|
||||
const info = getCaseInfo(nextPath.node.test);
|
||||
if (!info || normalizeCode(info.discriminant) !== discriminantCode) {
|
||||
break;
|
||||
}
|
||||
|
||||
cases.push({
|
||||
test: clone(info.caseValue),
|
||||
statements: toCaseStatements(nextPath.node.consequent)
|
||||
});
|
||||
endIndex += 1;
|
||||
}
|
||||
|
||||
if (cases.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
endIndex,
|
||||
switchNode: buildSwitchStatementFromChain({
|
||||
discriminant: clone(firstInfo.discriminant),
|
||||
cases,
|
||||
defaultStatements: null
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function rewriteStatementList(statementPaths) {
|
||||
if (!Array.isArray(statementPaths) || statementPaths.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextBody = [];
|
||||
let changed = false;
|
||||
|
||||
for (let index = 0; index < statementPaths.length;) {
|
||||
const run = collectConsecutiveIfCases(statementPaths, index);
|
||||
if (!run) {
|
||||
nextBody.push(statementPaths[index].node);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextBody.push(run.switchNode);
|
||||
index = run.endIndex;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed ? nextBody : null;
|
||||
}
|
||||
|
||||
function rewrite(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
IfStatement(path) {
|
||||
const chain = collectIfChain(path);
|
||||
if (!chain) {
|
||||
return;
|
||||
}
|
||||
|
||||
path.replaceWith(buildSwitchStatementFromChain(chain));
|
||||
changed = true;
|
||||
},
|
||||
"Program|BlockStatement": {
|
||||
exit(path) {
|
||||
const nextBody = rewriteStatementList(path.get("body"));
|
||||
if (nextBody) {
|
||||
path.node.body = nextBody;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
SwitchCase: {
|
||||
exit(path) {
|
||||
const nextConsequent = rewriteStatementList(path.get("consequent"));
|
||||
if (nextConsequent) {
|
||||
path.node.consequent = nextConsequent;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
let changed = false;
|
||||
do {
|
||||
({ ast, changed } = rewrite(ast));
|
||||
if (changed) {
|
||||
ast = reparse(ast);
|
||||
}
|
||||
} while (changed);
|
||||
saveAst(ast, outputPath);
|
||||
@@ -0,0 +1,130 @@
|
||||
const { clone, parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
function collectDispatcherValue(node, dispatcherMap) {
|
||||
if (t.isStringLiteral(node) || t.isNumericLiteral(node) || t.isBooleanLiteral(node)) {
|
||||
return { type: "literal", value: clone(node) };
|
||||
}
|
||||
if (t.isMemberExpression(node) && t.isIdentifier(node.object) && !node.computed && t.isIdentifier(node.property)) {
|
||||
return { type: "member", objectName: node.object.name, key: node.property.name };
|
||||
}
|
||||
if (t.isFunctionExpression(node) && node.body.body.length === 1 && t.isReturnStatement(node.body.body[0])) {
|
||||
const arg = node.body.body[0].argument;
|
||||
if (t.isBinaryExpression(arg) && t.isIdentifier(arg.left) && t.isIdentifier(arg.right)) {
|
||||
return { type: "binary", operator: arg.operator };
|
||||
}
|
||||
if (t.isLogicalExpression(arg) && t.isIdentifier(arg.left) && t.isIdentifier(arg.right)) {
|
||||
return { type: "logical", operator: arg.operator };
|
||||
}
|
||||
if (t.isCallExpression(arg) && t.isIdentifier(arg.callee)) {
|
||||
return { type: "call" };
|
||||
}
|
||||
if (t.isCallExpression(arg) && t.isMemberExpression(arg.callee) && t.isIdentifier(arg.callee.object) && !arg.callee.computed && t.isIdentifier(arg.callee.property)) {
|
||||
const target = dispatcherMap[arg.callee.object.name] && dispatcherMap[arg.callee.object.name][arg.callee.property.name];
|
||||
if (target && (target.type === "binary" || target.type === "logical" || target.type === "call")) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inlineDispatchers(ast) {
|
||||
let changed = false;
|
||||
const dispatcherMap = Object.create(null);
|
||||
const removableEntries = [];
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclarator(path) {
|
||||
if (!path.get("id").isIdentifier() || !path.get("init").isObjectExpression()) {
|
||||
return;
|
||||
}
|
||||
const name = path.node.id.name;
|
||||
const collected = dispatcherMap[name] || Object.create(null);
|
||||
let localChanged = false;
|
||||
let supportedPropertyCount = 0;
|
||||
let collectedPropertyCount = 0;
|
||||
path.get("init.properties").forEach(propertyPath => {
|
||||
if (!propertyPath.isObjectProperty()) {
|
||||
return;
|
||||
}
|
||||
supportedPropertyCount += 1;
|
||||
const keyNode = propertyPath.node.key;
|
||||
const key = t.isIdentifier(keyNode) ? keyNode.name : keyNode.value;
|
||||
const value = collectDispatcherValue(propertyPath.node.value, dispatcherMap);
|
||||
if (value) {
|
||||
collected[key] = value;
|
||||
localChanged = true;
|
||||
collectedPropertyCount += 1;
|
||||
}
|
||||
});
|
||||
if (localChanged) {
|
||||
dispatcherMap[name] = collected;
|
||||
removableEntries.push({
|
||||
path,
|
||||
removable: supportedPropertyCount > 0 && supportedPropertyCount === collectedPropertyCount
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (!path.get("callee").isMemberExpression()) {
|
||||
return;
|
||||
}
|
||||
const objectName = path.node.callee.object.name;
|
||||
const key = path.node.callee.computed ? path.node.callee.property.value : path.node.callee.property.name;
|
||||
const entry = dispatcherMap[objectName] && dispatcherMap[objectName][key];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const args = path.node.arguments.map(arg => clone(arg));
|
||||
if (entry.type === "binary" && args.length >= 2) {
|
||||
path.replaceWith(t.binaryExpression(entry.operator, args[0], args[1]));
|
||||
changed = true;
|
||||
} else if (entry.type === "logical" && args.length >= 2) {
|
||||
path.replaceWith(t.logicalExpression(entry.operator, args[0], args[1]));
|
||||
changed = true;
|
||||
} else if (entry.type === "call" && args.length >= 1) {
|
||||
path.replaceWith(t.callExpression(args[0], args.slice(1)));
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
MemberExpression(path) {
|
||||
if (!path.get("object").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const objectName = path.node.object.name;
|
||||
const key = path.node.computed ? path.node.property.value : path.node.property.name;
|
||||
const entry = dispatcherMap[objectName] && dispatcherMap[objectName][key];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
if (entry.type === "literal") {
|
||||
path.replaceWith(clone(entry.value));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
removableEntries.forEach(({ path, removable }) => {
|
||||
if (removable && !path.removed) {
|
||||
path.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
let changed = false;
|
||||
do {
|
||||
({ ast, changed } = inlineDispatchers(ast));
|
||||
if (changed) {
|
||||
ast = reparse(ast);
|
||||
}
|
||||
} while (changed);
|
||||
saveAst(ast, outputPath);
|
||||
@@ -0,0 +1,190 @@
|
||||
const { clone, isPrimitiveNode, parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
function getReplacementTarget(path) {
|
||||
let current = path;
|
||||
while (current.parentPath.isMemberExpression() && current.key === "object") {
|
||||
current = current.parentPath;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function isUnsafeReplacementTarget(path) {
|
||||
const target = getReplacementTarget(path);
|
||||
if (target.parentPath.isUpdateExpression() && target.key === "argument") {
|
||||
return true;
|
||||
}
|
||||
if ((target.parentPath.isAssignmentExpression() || target.parentPath.isAssignmentPattern()) && target.key === "left") {
|
||||
return true;
|
||||
}
|
||||
if ((target.parentPath.isForInStatement() || target.parentPath.isForOfStatement()) && target.key === "left") {
|
||||
return true;
|
||||
}
|
||||
if (target.parentPath.isUnaryExpression({ operator: "delete" }) && target.key === "argument") {
|
||||
return true;
|
||||
}
|
||||
if (target.parentPath.isObjectProperty() && target.key === "key" && !target.parent.computed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function replaceSafely(path, node) {
|
||||
if (isUnsafeReplacementTarget(path)) {
|
||||
return false;
|
||||
}
|
||||
path.replaceWith(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
function allRefsAfter(binding, position) {
|
||||
return binding.referencePaths.every(refPath => typeof refPath.node.start !== "number" || refPath.node.start > position);
|
||||
}
|
||||
|
||||
function inlineLiterals(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
MemberExpression(path) {
|
||||
if (t.isArrayExpression(path.node.object) && path.node.object.elements.length === 0 && t.isArrayExpression(path.node.property) && path.node.property.elements.length === 0) {
|
||||
if (replaceSafely(path, t.identifier("undefined"))) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
VariableDeclarator(path) {
|
||||
if (!path.get("id").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const name = path.node.id.name;
|
||||
const init = path.node.init;
|
||||
const binding = path.scope.getBinding(name);
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
if (t.isStringLiteral(init) || t.isNumericLiteral(init) || t.isBooleanLiteral(init)) {
|
||||
if (!binding.constant) {
|
||||
return;
|
||||
}
|
||||
let replacedAll = binding.referencePaths.length > 0;
|
||||
let replacedAny = false;
|
||||
binding.referencePaths.forEach(refPath => {
|
||||
if (!replaceSafely(refPath, clone(init))) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
replacedAny = true;
|
||||
});
|
||||
if (replacedAny) {
|
||||
changed = true;
|
||||
}
|
||||
if (replacedAll) {
|
||||
path.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (t.isSequenceExpression(init) && init.expressions.length > 0 && init.expressions.every(expr => t.isStringLiteral(expr))) {
|
||||
path.get("init").replaceWith(clone(init.expressions[init.expressions.length - 1]));
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
if (t.isArrayExpression(init) && init.elements.length > 0 && init.elements.every(el => el && isPrimitiveNode(el))) {
|
||||
let replacedAll = binding.referencePaths.length > 0;
|
||||
let replacedCount = 0;
|
||||
binding.referencePaths.forEach(refPath => {
|
||||
const parent = refPath.parentPath;
|
||||
if (!parent.isMemberExpression({ object: refPath.node }) || !parent.get("property").isNumericLiteral()) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
const index = parent.node.property.value;
|
||||
if (!init.elements[index]) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
if (!replaceSafely(parent, clone(init.elements[index]))) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
replacedCount += 1;
|
||||
changed = true;
|
||||
});
|
||||
if (replacedAll && replacedCount > 0) {
|
||||
path.remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
AssignmentExpression(path) {
|
||||
if (!path.get("left").isIdentifier() || path.node.operator !== "=") {
|
||||
return;
|
||||
}
|
||||
if (!path.parentPath.isExpressionStatement()) {
|
||||
return;
|
||||
}
|
||||
const binding = path.scope.getBinding(path.node.left.name);
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
if (typeof path.node.start === "number" && !allRefsAfter(binding, path.node.start)) {
|
||||
return;
|
||||
}
|
||||
if (path.get("right").isStringLiteral() || path.get("right").isNumericLiteral() || path.get("right").isBooleanLiteral()) {
|
||||
const right = clone(path.node.right);
|
||||
let replacedAll = binding.referencePaths.length > 0;
|
||||
let replacedAny = false;
|
||||
binding.referencePaths.forEach(refPath => {
|
||||
if (!replaceSafely(refPath, clone(right))) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
replacedAny = true;
|
||||
});
|
||||
if (replacedAny) {
|
||||
changed = true;
|
||||
}
|
||||
if (replacedAll) {
|
||||
path.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path.get("right").isArrayExpression() && path.node.right.elements.every(el => el && isPrimitiveNode(el))) {
|
||||
const elements = path.node.right.elements;
|
||||
let replacedAll = binding.referencePaths.length > 0;
|
||||
let replacedCount = 0;
|
||||
binding.referencePaths.forEach(refPath => {
|
||||
const parent = refPath.parentPath;
|
||||
if (!parent.isMemberExpression({ object: refPath.node }) || !parent.get("property").isNumericLiteral()) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
const index = parent.node.property.value;
|
||||
if (!elements[index]) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
if (!replaceSafely(parent, clone(elements[index]))) {
|
||||
replacedAll = false;
|
||||
return;
|
||||
}
|
||||
replacedCount += 1;
|
||||
changed = true;
|
||||
});
|
||||
if (replacedAll && replacedCount > 0) {
|
||||
path.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
let changed = false;
|
||||
do {
|
||||
({ ast, changed } = inlineLiterals(ast));
|
||||
if (changed) {
|
||||
ast = reparse(ast);
|
||||
}
|
||||
} while (changed);
|
||||
saveAst(ast, outputPath);
|
||||
@@ -0,0 +1,211 @@
|
||||
const { parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
function toStatements(expressions) {
|
||||
return expressions.map(expr => t.expressionStatement(expr));
|
||||
}
|
||||
|
||||
function hasUnsafeContinue(bodyPath) {
|
||||
let found = false;
|
||||
bodyPath.traverse({
|
||||
Function(path) {
|
||||
path.skip();
|
||||
},
|
||||
ContinueStatement(path) {
|
||||
found = true;
|
||||
path.stop();
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function ensureBlock(path) {
|
||||
if (path.isBlockStatement()) {
|
||||
return path;
|
||||
}
|
||||
path.replaceWith(t.blockStatement([path.node]));
|
||||
return path.parentPath.get(path.key);
|
||||
}
|
||||
|
||||
function replaceStatement(path, node) {
|
||||
if (path.inList) {
|
||||
path.replaceWith(node);
|
||||
return;
|
||||
}
|
||||
path.replaceWith(t.blockStatement([node]));
|
||||
}
|
||||
|
||||
function replaceStatementWithStatements(path, statements) {
|
||||
if (statements.length === 0) {
|
||||
path.remove();
|
||||
return;
|
||||
}
|
||||
if (path.inList) {
|
||||
path.replaceWithMultiple(statements);
|
||||
return;
|
||||
}
|
||||
if (statements.length === 1) {
|
||||
path.replaceWith(statements[0]);
|
||||
return;
|
||||
}
|
||||
path.replaceWith(t.blockStatement(statements));
|
||||
}
|
||||
|
||||
function normalize(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
UnaryExpression(path) {
|
||||
if (path.node.operator === "void" && path.parentPath.isExpressionStatement()) {
|
||||
path.replaceWith(path.node.argument);
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
SequenceExpression(path) {
|
||||
if (path.parentPath.isExpressionStatement()) {
|
||||
replaceStatementWithStatements(path.parentPath, toStatements(path.node.expressions));
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isReturnStatement()) {
|
||||
if (!path.parentPath.inList) {
|
||||
return;
|
||||
}
|
||||
const exprs = path.node.expressions.slice();
|
||||
const returnExpr = exprs.pop();
|
||||
path.parentPath.insertBefore(toStatements(exprs));
|
||||
path.replaceWith(returnExpr);
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isIfStatement() && path.key === "test") {
|
||||
if (!path.parentPath.inList) {
|
||||
return;
|
||||
}
|
||||
const exprs = path.node.expressions.slice();
|
||||
const testExpr = exprs.pop();
|
||||
path.parentPath.insertBefore(toStatements(exprs));
|
||||
path.replaceWith(testExpr);
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isForStatement() && path.key === "init") {
|
||||
if (!path.parentPath.inList) {
|
||||
return;
|
||||
}
|
||||
path.parentPath.insertBefore(toStatements(path.node.expressions));
|
||||
path.remove();
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isForStatement() && path.key === "test") {
|
||||
const exprs = path.node.expressions.slice();
|
||||
const testExpr = exprs.pop();
|
||||
const forPath = path.parentPath;
|
||||
const bodyPath = ensureBlock(forPath.get("body"));
|
||||
bodyPath.unshiftContainer(
|
||||
"body",
|
||||
t.ifStatement(t.unaryExpression("!", testExpr, true), t.blockStatement([t.breakStatement()]))
|
||||
);
|
||||
bodyPath.unshiftContainer("body", toStatements(exprs));
|
||||
path.remove();
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isForStatement() && path.key === "update") {
|
||||
const forPath = path.parentPath;
|
||||
const bodyPath = ensureBlock(forPath.get("body"));
|
||||
if (hasUnsafeContinue(bodyPath)) {
|
||||
return;
|
||||
}
|
||||
bodyPath.pushContainer("body", toStatements(path.node.expressions));
|
||||
path.remove();
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
ConditionalExpression(path) {
|
||||
if (path.parentPath.isExpressionStatement()) {
|
||||
replaceStatement(
|
||||
path.parentPath,
|
||||
t.ifStatement(
|
||||
path.node.test,
|
||||
t.blockStatement([t.expressionStatement(path.node.consequent)]),
|
||||
t.blockStatement([t.expressionStatement(path.node.alternate)])
|
||||
)
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
LogicalExpression(path) {
|
||||
if (!path.parentPath.isExpressionStatement()) {
|
||||
return;
|
||||
}
|
||||
const body = t.blockStatement([t.expressionStatement(path.node.right)]);
|
||||
if (path.node.operator === "&&") {
|
||||
replaceStatement(path.parentPath, t.ifStatement(path.node.left, body, null));
|
||||
} else if (path.node.operator === "||") {
|
||||
replaceStatement(path.parentPath, t.ifStatement(t.unaryExpression("!", path.node.left, true), body, null));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
changed = true;
|
||||
},
|
||||
AssignmentExpression(path) {
|
||||
if (!path.parentPath.isExpressionStatement() || !t.isConditionalExpression(path.node.right)) {
|
||||
return;
|
||||
}
|
||||
const test = path.node.right.test;
|
||||
const consequent = t.expressionStatement(
|
||||
t.assignmentExpression(path.node.operator, t.cloneNode(path.node.left, true), path.node.right.consequent)
|
||||
);
|
||||
const alternate = t.expressionStatement(
|
||||
t.assignmentExpression(path.node.operator, t.cloneNode(path.node.left, true), path.node.right.alternate)
|
||||
);
|
||||
replaceStatement(path.parentPath, t.ifStatement(test, t.blockStatement([consequent]), t.blockStatement([alternate])));
|
||||
changed = true;
|
||||
},
|
||||
CallExpression(path) {
|
||||
if (!path.parentPath.isExpressionStatement()) {
|
||||
return;
|
||||
}
|
||||
if (path.get("callee").isFunctionExpression() && path.node.arguments.length === 0) {
|
||||
replaceStatementWithStatements(path.parentPath, path.node.callee.body.body);
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
path.get("callee").isMemberExpression() &&
|
||||
path.get("callee.object").isFunctionExpression() &&
|
||||
path.get("callee.property").isIdentifier({ name: "call" }) &&
|
||||
path.node.arguments.length === 1 &&
|
||||
path.get("arguments.0").isThisExpression()
|
||||
) {
|
||||
replaceStatementWithStatements(path.parentPath, path.node.callee.object.body.body);
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
BlockStatement(path) {
|
||||
if (path.parentPath.isSwitchCase()) {
|
||||
path.replaceWithMultiple(path.node.body);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
let changed = false;
|
||||
do {
|
||||
({ ast, changed } = normalize(ast));
|
||||
if (changed) {
|
||||
ast = reparse(ast);
|
||||
}
|
||||
} while (changed);
|
||||
saveAst(ast, outputPath);
|
||||
@@ -0,0 +1,81 @@
|
||||
const { clone } = require("../shared");
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const { getTopLevelIifeInfo, traverse, t } = require("./pattern-utils");
|
||||
|
||||
function dingxiangArrayPass(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
Program(programPath) {
|
||||
const info = getTopLevelIifeInfo(programPath);
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actualArgs = info.argPaths.map((argPath) => argPath.node);
|
||||
const formalParams = info.calleePath.node.params;
|
||||
const paramElementMap = Object.create(null);
|
||||
|
||||
formalParams.forEach((paramNode, index) => {
|
||||
if (!t.isIdentifier(paramNode)) {
|
||||
return;
|
||||
}
|
||||
const actualNode = actualArgs[index];
|
||||
if (t.isArrayExpression(actualNode)) {
|
||||
paramElementMap[paramNode.name] = actualNode.elements.map((element) => (element ? clone(element) : t.identifier("undefined")));
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(paramElementMap).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
info.statementPath.traverse({
|
||||
UnaryExpression(path) {
|
||||
const callPath = path.get("argument");
|
||||
if (
|
||||
!callPath.isCallExpression() ||
|
||||
!callPath.get("callee").isFunctionExpression() ||
|
||||
callPath.node.arguments.length !== 2 ||
|
||||
!callPath.get("arguments").every((argPath) => argPath.isArrayExpression())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
callPath.get("callee.params").forEach((paramPath) => {
|
||||
if (!paramPath.isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const elements = paramElementMap[paramPath.node.name];
|
||||
if (!elements) {
|
||||
return;
|
||||
}
|
||||
const binding = paramPath.scope.getBinding(paramPath.node.name);
|
||||
if (!binding || !binding.constant) {
|
||||
return;
|
||||
}
|
||||
binding.referencePaths.forEach((refPath) => {
|
||||
const parent = refPath.parentPath;
|
||||
if (!parent.isMemberExpression({ object: refPath.node }) || !parent.get("property").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
const index = parent.node.property.value;
|
||||
if (!Number.isInteger(index) || index < 0 || index >= elements.length) {
|
||||
return;
|
||||
}
|
||||
parent.replaceWith(clone(elements[index]));
|
||||
changed = true;
|
||||
});
|
||||
});
|
||||
|
||||
path.replaceWithMultiple(callPath.get("callee.body").node.body.map((node) => clone(node)));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
runPatternPass(dingxiangArrayPass);
|
||||
@@ -0,0 +1,113 @@
|
||||
const { clone } = require("../shared");
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const { createSandbox, evaluateExpression, evaluateNodes, generateCode, traverse, t } = require("./pattern-utils");
|
||||
|
||||
function geetest4GuardedPass(ast) {
|
||||
let changed = false;
|
||||
const body = ast.program.body;
|
||||
|
||||
if (body.length >= 5) {
|
||||
const sandbox = createSandbox();
|
||||
const decriptPropName = body[2] && body[2].expression && body[2].expression.left ? generateCode(body[2].expression.left) : "";
|
||||
const ctrlVarName = body[3] && body[3].expression && body[3].expression.left ? generateCode(body[3].expression.left) : "";
|
||||
|
||||
if (decriptPropName && evaluateNodes(body.slice(0, 5), sandbox)) {
|
||||
traverse(ast, {
|
||||
VariableDeclaration(path) {
|
||||
if (
|
||||
path.node.declarations.length !== 3 ||
|
||||
!path.get("declarations.0.init").node ||
|
||||
generateCode(path.get("declarations.0.init").node) !== decriptPropName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next1 = path.getNextSibling();
|
||||
const next2 = next1 && next1.getNextSibling();
|
||||
if (!next1 || !next2 || !next1.node || !next2.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidateNames = [0, 2]
|
||||
.map((index) => path.get(`declarations.${index}.id`))
|
||||
.filter((idPath) => idPath && idPath.isIdentifier())
|
||||
.map((idPath) => idPath.node.name);
|
||||
|
||||
candidateNames.forEach((name) => {
|
||||
const binding = path.scope.getBinding(name);
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
binding.referencePaths.forEach((refPath) => {
|
||||
const parent = refPath.parentPath;
|
||||
if (!parent.isCallExpression() || parent.node.arguments.length !== 1 || !parent.get("arguments.0").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
const argValue = parent.node.arguments[0].value;
|
||||
const result = evaluateExpression(`${decriptPropName}(${argValue})`, sandbox);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
parent.replaceWith(t.valueToNode(result.value));
|
||||
changed = true;
|
||||
});
|
||||
});
|
||||
|
||||
path.remove();
|
||||
next1.remove();
|
||||
next2.remove();
|
||||
changed = true;
|
||||
},
|
||||
ForStatement(path) {
|
||||
if (
|
||||
path.node.init !== null ||
|
||||
path.node.update !== null ||
|
||||
!path.get("test").isBinaryExpression() ||
|
||||
!generateCode(path.get("test.right").node).includes(ctrlVarName) ||
|
||||
!path.get("body").isBlockStatement() ||
|
||||
path.get("body.body").length !== 1 ||
|
||||
!path.get("body.body.0").isSwitchStatement()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const testNamePath = path.get("test.left");
|
||||
if (!testNamePath.isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const testName = testNamePath.node.name;
|
||||
const statements = [];
|
||||
path.get("body.body.0.cases").forEach((casePath) => {
|
||||
casePath.get("consequent").forEach((stmtPath) => {
|
||||
if (stmtPath.isBreakStatement()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
stmtPath.isExpressionStatement() &&
|
||||
stmtPath.get("expression").isAssignmentExpression() &&
|
||||
stmtPath.get("expression.left").isIdentifier({ name: testName })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
statements.push(clone(stmtPath.node));
|
||||
});
|
||||
});
|
||||
|
||||
if (statements.length > 0) {
|
||||
path.replaceWithMultiple(statements);
|
||||
const prev = path.getPrevSibling();
|
||||
if (prev && prev.node) {
|
||||
prev.remove();
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
runPatternPass(geetest4GuardedPass);
|
||||
@@ -0,0 +1,352 @@
|
||||
const vm = require("vm");
|
||||
const { clone } = require("../shared");
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const { createSandbox, evaluateExpression, evaluateNodes, evaluatePreludeStatements, foldWrapperObjects, generateCode, literalNodeFromValue, replaceCallsFromSandbox, replaceMembersFromSandbox, replacePrimitiveIdentifiers, seedSandboxFromAliases, traverse, t } = require("./pattern-utils");
|
||||
|
||||
function inlineAssignmentObjects(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclaration(path) {
|
||||
if (path.node.declarations.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const declaration = path.get("declarations.0");
|
||||
if (!declaration.get("id").isIdentifier() || !declaration.get("init").isObjectExpression({ properties: [] })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = declaration.node.id.name;
|
||||
const binding = path.scope.getBinding(name);
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectEntries = Object.create(null);
|
||||
binding.referencePaths.forEach((refPath) => {
|
||||
const memberPath = refPath.parentPath;
|
||||
const assignmentPath = memberPath && memberPath.parentPath;
|
||||
if (
|
||||
!memberPath ||
|
||||
!memberPath.isMemberExpression({ object: refPath.node }) ||
|
||||
!assignmentPath ||
|
||||
!assignmentPath.isAssignmentExpression({ left: memberPath.node, operator: "=" })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = memberPath.node.computed ? String(memberPath.node.property.value) : memberPath.node.property.name;
|
||||
objectEntries[key] = clone(assignmentPath.node.right);
|
||||
assignmentPath.remove();
|
||||
changed = true;
|
||||
});
|
||||
|
||||
const properties = Object.entries(objectEntries).map(([key, valueNode]) => t.objectProperty(t.stringLiteral(key), valueNode));
|
||||
if (properties.length > 0) {
|
||||
declaration.get("init").replaceWith(t.objectExpression(properties));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function getLeadingPreludeNodes(ast, maxStatements = 12) {
|
||||
const collected = [];
|
||||
for (const node of ast.program.body) {
|
||||
if (collected.length >= maxStatements) {
|
||||
break;
|
||||
}
|
||||
if (t.isFunctionDeclaration(node) || t.isVariableDeclaration(node)) {
|
||||
collected.push(node);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
function findRotationIifeNode(ast, preludeCount) {
|
||||
const node = ast.program.body[preludeCount];
|
||||
if (!node || !t.isExpressionStatement(node)) {
|
||||
return null;
|
||||
}
|
||||
const expr = node.expression;
|
||||
if (t.isCallExpression(expr) && (t.isFunctionExpression(expr.callee) || t.isArrowFunctionExpression(expr.callee))) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findPxSetterFunctionName(ast) {
|
||||
for (const node of ast.program.body) {
|
||||
if (!t.isFunctionDeclaration(node) || !node.id) {
|
||||
continue;
|
||||
}
|
||||
let matched = false;
|
||||
traverse(t.file(t.program([clone(node)])), {
|
||||
AssignmentExpression(path) {
|
||||
if (path.get("left").isIdentifier({ name: "PX" })) {
|
||||
matched = true;
|
||||
path.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (matched) {
|
||||
return node.id.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findDecoderFunctionName(ast) {
|
||||
for (const node of ast.program.body) {
|
||||
if (t.isFunctionDeclaration(node) && node.id && node.id.name) {
|
||||
return node.id.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectValidationTargets(ast) {
|
||||
const targets = [];
|
||||
const seen = new Set();
|
||||
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const firstArgPath = path.get("arguments.0");
|
||||
if (
|
||||
!path.get("callee").isMemberExpression() ||
|
||||
!firstArgPath ||
|
||||
!firstArgPath.node ||
|
||||
!firstArgPath.isStringLiteral({ value: "|" })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyPath = path.get("callee.property");
|
||||
if (!propertyPath.isCallExpression() || !propertyPath.get("arguments.0").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const argValue = propertyPath.get("arguments.0").node.value;
|
||||
const objectPath = path.get("callee.object");
|
||||
if (!objectPath.isCallExpression() || !objectPath.get("arguments.0").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderArgValue = objectPath.get("arguments.0").node.value;
|
||||
const token = `split:${argValue}:order:${orderArgValue}`;
|
||||
if (seen.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seen.add(token);
|
||||
targets.push({
|
||||
argValue,
|
||||
expected: "split",
|
||||
orderArgValue
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
function runCodeInSandbox(code, sandbox, timeoutMs) {
|
||||
try {
|
||||
vm.runInNewContext(code, sandbox, { timeout: timeoutMs });
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildDynamicDecoderSandbox(preludeCode, rotationCode, pxSetterName, pxValue) {
|
||||
const sandbox = createSandbox();
|
||||
if (!runCodeInSandbox(preludeCode, sandbox, 5000)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sandbox[pxSetterName] = function patchedPxSetter() {
|
||||
sandbox.PX = pxValue;
|
||||
return "nBfa";
|
||||
};
|
||||
|
||||
// OB variants can spend noticeably longer in the bootstrap rotation loop.
|
||||
if (!runCodeInSandbox(rotationCode, sandbox, 3000)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
function validateDynamicDecoderSandbox(sandbox, decoderName, targets) {
|
||||
if (!sandbox || typeof sandbox[decoderName] !== "function" || targets.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return targets.every((target) => {
|
||||
if (sandbox[decoderName](target.argValue) !== target.expected) {
|
||||
return false;
|
||||
}
|
||||
if (typeof target.orderArgValue !== "number") {
|
||||
return true;
|
||||
}
|
||||
const orderValue = sandbox[decoderName](target.orderArgValue);
|
||||
return typeof orderValue === "string" && orderValue.includes("|");
|
||||
});
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapDynamicDecoderSandbox(ast) {
|
||||
const preludeNodes = getLeadingPreludeNodes(ast);
|
||||
const rotationNode = findRotationIifeNode(ast, preludeNodes.length);
|
||||
const pxSetterName = findPxSetterFunctionName(ast);
|
||||
const decoderName = findDecoderFunctionName(ast);
|
||||
const targets = collectValidationTargets(ast);
|
||||
const preludeCode = preludeNodes.map((node) => generateCode(node)).join(";\n");
|
||||
const rotationCode = rotationNode ? generateCode(rotationNode) : "";
|
||||
|
||||
if (!rotationNode || !pxSetterName || !decoderName || targets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pxCandidates = [];
|
||||
for (let pxValue = 256; pxValue <= 512; pxValue += 1) {
|
||||
pxCandidates.push(pxValue);
|
||||
}
|
||||
for (let pxValue = 0; pxValue < 256; pxValue += 1) {
|
||||
pxCandidates.push(pxValue);
|
||||
}
|
||||
|
||||
for (const pxValue of pxCandidates) {
|
||||
const sandbox = buildDynamicDecoderSandbox(preludeCode, rotationCode, pxSetterName, pxValue);
|
||||
if (!validateDynamicDecoderSandbox(sandbox, decoderName, targets)) {
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
sandbox,
|
||||
decoderName,
|
||||
pxValue
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function obVariantPass(ast) {
|
||||
let changed = false;
|
||||
const body = ast.program.body;
|
||||
|
||||
if (body.length >= 3 && body[2].type === "VariableDeclaration" && body[2].declarations.length > 0 && t.isIdentifier(body[2].declarations[0].id)) {
|
||||
const sandbox = createSandbox();
|
||||
const decoderName = body[2].declarations[0].id.name;
|
||||
if (evaluateNodes(body.slice(0, 3), sandbox) && typeof sandbox[decoderName] === "function") {
|
||||
ast.program.body = body.slice(3).map((node) => clone(node));
|
||||
changed = true;
|
||||
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (!path.get("callee").isIdentifier({ name: decoderName })) {
|
||||
return;
|
||||
}
|
||||
const result = evaluateExpression(path.toString(), sandbox);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
path.replaceWith(t.valueToNode(result.value));
|
||||
changed = true;
|
||||
},
|
||||
BinaryExpression: {
|
||||
exit(path) {
|
||||
const evaluated = path.evaluate();
|
||||
if (evaluated.confident) {
|
||||
path.replaceWith(t.valueToNode(evaluated.value));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dynamicDecoder = bootstrapDynamicDecoderSandbox(ast);
|
||||
const prelude = dynamicDecoder ? null : evaluatePreludeStatements(ast, {
|
||||
sandbox: createSandbox(),
|
||||
maxStatements: 8,
|
||||
timeoutMs: 1500
|
||||
});
|
||||
const sandbox = dynamicDecoder ? dynamicDecoder.sandbox : prelude.sandbox;
|
||||
if (dynamicDecoder || prelude.executedCount > 0) {
|
||||
if (seedSandboxFromAliases(ast, sandbox)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (replaceCallsFromSandbox(ast, sandbox, {
|
||||
timeoutMs: 800,
|
||||
allowPredicate: (path) => path.node.arguments.every((arg) => t.isLiteral(arg))
|
||||
})) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (replaceMembersFromSandbox(ast, sandbox)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclarator(path) {
|
||||
const initPath = path.get("init");
|
||||
if (!initPath.node) {
|
||||
return;
|
||||
}
|
||||
const result = evaluateExpression(generateCode(initPath.node), sandbox, 800);
|
||||
const node = result.ok ? literalNodeFromValue(result.value) : null;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
initPath.replaceWith(node);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (replacePrimitiveIdentifiers(ast, sandbox)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (replaceMembersFromSandbox(ast, sandbox)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (replaceCallsFromSandbox(ast, sandbox, {
|
||||
timeoutMs: 800,
|
||||
allowPredicate: (path) => path.node.arguments.every((arg) => t.isLiteral(arg))
|
||||
})) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (inlineAssignmentObjects(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
let localChanged = false;
|
||||
do {
|
||||
({ changed: localChanged } = foldWrapperObjects(ast, {
|
||||
sandbox,
|
||||
timeoutMs: 800
|
||||
}));
|
||||
if (localChanged) {
|
||||
changed = true;
|
||||
}
|
||||
} while (localChanged);
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
runPatternPass(obVariantPass);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const { inlineStringMethodCalls, traverse, t } = require("./pattern-utils");
|
||||
|
||||
function pruneUnusedTopLevel(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
Program(path) {
|
||||
path.get("body").forEach((stmtPath) => {
|
||||
if (stmtPath.isVariableDeclaration()) {
|
||||
const removable = [];
|
||||
stmtPath.get("declarations").forEach((declPath) => {
|
||||
if (!declPath.get("id").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const binding = declPath.scope.getBinding(declPath.node.id.name);
|
||||
if (binding && !binding.referenced) {
|
||||
removable.push(declPath);
|
||||
}
|
||||
});
|
||||
if (removable.length === stmtPath.node.declarations.length && removable.length > 0) {
|
||||
stmtPath.remove();
|
||||
changed = true;
|
||||
} else if (removable.length > 0) {
|
||||
removable.forEach((declPath) => declPath.remove());
|
||||
changed = true;
|
||||
}
|
||||
} else if (stmtPath.isFunctionDeclaration() && stmtPath.node.id) {
|
||||
const binding = stmtPath.scope.getBinding(stmtPath.node.id.name);
|
||||
if (binding && !binding.referenced) {
|
||||
stmtPath.remove();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function reese84HeavyPass(ast) {
|
||||
let changed = false;
|
||||
if (inlineStringMethodCalls(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
traverse(ast, {
|
||||
BinaryExpression: {
|
||||
exit(path) {
|
||||
if (path.get("left").isStringLiteral() && path.get("right").isStringLiteral()) {
|
||||
const evaluated = path.evaluate();
|
||||
if (evaluated.confident) {
|
||||
path.replaceWith(t.valueToNode(evaluated.value));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (pruneUnusedTopLevel(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
runPatternPass(reese84HeavyPass);
|
||||
@@ -0,0 +1,17 @@
|
||||
const { parseArgs, parseFile, reparse, saveAst } = require("../shared");
|
||||
|
||||
function runPatternPass(transform) {
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
const result = typeof transform === "function" ? transform(ast) || { ast, changed: false } : { ast, changed: false };
|
||||
if (result.changed) {
|
||||
ast = reparse(result.ast || ast);
|
||||
} else if (result.ast) {
|
||||
ast = result.ast;
|
||||
}
|
||||
saveAst(ast, outputPath);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runPatternPass
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const {
|
||||
createSandbox,
|
||||
evaluateNodes,
|
||||
foldWrapperObjects,
|
||||
inlineStringMethodCalls,
|
||||
seedSandboxFromAliases,
|
||||
traverse,
|
||||
t
|
||||
} = require("./pattern-utils");
|
||||
|
||||
function getDecoderPreludeNodes(ast) {
|
||||
const topLevelDeclarator = ast.program.body
|
||||
.filter((node) => t.isVariableDeclaration(node))
|
||||
.flatMap((node) => node.declarations)
|
||||
.find((decl) =>
|
||||
t.isIdentifier(decl.id) &&
|
||||
(
|
||||
t.isFunctionExpression(decl.init) ||
|
||||
(t.isCallExpression(decl.init) && t.isFunctionExpression(decl.init.callee))
|
||||
)
|
||||
);
|
||||
|
||||
const rootFunction = !topLevelDeclarator ? null :
|
||||
t.isFunctionExpression(topLevelDeclarator.init) ? topLevelDeclarator.init :
|
||||
t.isCallExpression(topLevelDeclarator.init) && t.isFunctionExpression(topLevelDeclarator.init.callee) ? topLevelDeclarator.init.callee :
|
||||
null;
|
||||
|
||||
if (!rootFunction || !t.isBlockStatement(rootFunction.body)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes = [];
|
||||
for (const stmt of rootFunction.body.body) {
|
||||
if (t.isFunctionDeclaration(stmt) && stmt.id && (stmt.id.name === "t" || stmt.id.name === "u")) {
|
||||
nodes.push(stmt);
|
||||
continue;
|
||||
}
|
||||
if (t.isVariableDeclaration(stmt) && stmt.declarations.every((decl) => t.isIdentifier(decl.id) && decl.init == null)) {
|
||||
nodes.push(stmt);
|
||||
continue;
|
||||
}
|
||||
if (t.isExpressionStatement(stmt)) {
|
||||
nodes.push(stmt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function tonghuashunOrderPass(ast) {
|
||||
let changed = false;
|
||||
let localChanged = false;
|
||||
const sandbox = createSandbox();
|
||||
const preludeNodes = getDecoderPreludeNodes(ast);
|
||||
if (preludeNodes.length > 0 && evaluateNodes(preludeNodes, sandbox, 2000)) {
|
||||
while (seedSandboxFromAliases(ast, sandbox)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
const decoderAliasNames = collectDecoderAliasNames(ast, ["t"]);
|
||||
do {
|
||||
localChanged = false;
|
||||
|
||||
if (replaceDecoderAliasCalls(ast, sandbox, decoderAliasNames)) {
|
||||
localChanged = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (inlineStringMethodCalls(ast)) {
|
||||
localChanged = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const foldResult = foldWrapperObjects(ast, {
|
||||
ignoreDynamicUsage: true
|
||||
});
|
||||
if (foldResult.changed) {
|
||||
localChanged = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (inlineStringMethodCalls(ast)) {
|
||||
localChanged = true;
|
||||
changed = true;
|
||||
}
|
||||
} while (localChanged);
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
function collectDecoderAliasNames(ast, seedNames) {
|
||||
const aliasNames = new Set(seedNames);
|
||||
let expanded = false;
|
||||
|
||||
do {
|
||||
expanded = false;
|
||||
traverse(ast, {
|
||||
VariableDeclarator(path) {
|
||||
if (!path.get("id").isIdentifier() || !path.get("init").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const sourceName = path.get("init").node.name;
|
||||
const aliasName = path.get("id").node.name;
|
||||
if (!aliasNames.has(sourceName) || aliasNames.has(aliasName)) {
|
||||
return;
|
||||
}
|
||||
aliasNames.add(aliasName);
|
||||
expanded = true;
|
||||
},
|
||||
AssignmentExpression(path) {
|
||||
if (!path.get("left").isIdentifier() || !path.get("right").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const sourceName = path.get("right").node.name;
|
||||
const aliasName = path.get("left").node.name;
|
||||
if (!aliasNames.has(sourceName) || aliasNames.has(aliasName)) {
|
||||
return;
|
||||
}
|
||||
aliasNames.add(aliasName);
|
||||
expanded = true;
|
||||
}
|
||||
});
|
||||
} while (expanded);
|
||||
|
||||
return aliasNames;
|
||||
}
|
||||
|
||||
function nodeFromPrimitiveValue(value) {
|
||||
if (typeof value === "undefined") {
|
||||
return t.identifier("undefined");
|
||||
}
|
||||
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
return t.valueToNode(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function replaceDecoderAliasCalls(ast, sandbox, aliasNames) {
|
||||
if (typeof sandbox.t !== "function" || aliasNames.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleePath = path.get("callee");
|
||||
let isDecoderAliasCall = false;
|
||||
|
||||
if (calleePath.isIdentifier()) {
|
||||
isDecoderAliasCall = aliasNames.has(calleePath.node.name);
|
||||
} else if (
|
||||
calleePath.isAssignmentExpression({ operator: "=" }) &&
|
||||
calleePath.get("left").isIdentifier() &&
|
||||
calleePath.get("right").isIdentifier()
|
||||
) {
|
||||
isDecoderAliasCall = aliasNames.has(calleePath.get("right").node.name);
|
||||
}
|
||||
|
||||
if (!isDecoderAliasCall) {
|
||||
return;
|
||||
}
|
||||
if (path.node.arguments.length !== 1 || !path.get("arguments.0").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
try {
|
||||
value = sandbox.t(path.get("arguments.0").node.value);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const literalNode = nodeFromPrimitiveValue(value);
|
||||
if (!literalNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
path.replaceWith(literalNode);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
runPatternPass(tonghuashunOrderPass);
|
||||
@@ -0,0 +1,257 @@
|
||||
const { clone } = require("../shared");
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const { createSandbox, evaluateExpression, evaluateNodes, foldWrapperObjects, traverse, t } = require("./pattern-utils");
|
||||
|
||||
function getOpcodeTestInfo(testPath) {
|
||||
if (!testPath.isBinaryExpression()) {
|
||||
return null;
|
||||
}
|
||||
if (!["===", "=="].includes(testPath.node.operator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (testPath.get("left").isNumericLiteral() && testPath.get("right").isIdentifier()) {
|
||||
return {
|
||||
discriminant: clone(testPath.get("right").node),
|
||||
discriminantName: testPath.get("right").node.name,
|
||||
caseValue: clone(testPath.get("left").node)
|
||||
};
|
||||
}
|
||||
|
||||
if (testPath.get("right").isNumericLiteral() && testPath.get("left").isIdentifier()) {
|
||||
return {
|
||||
discriminant: clone(testPath.get("left").node),
|
||||
discriminantName: testPath.get("left").node.name,
|
||||
caseValue: clone(testPath.get("right").node)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function blockify(node) {
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
if (t.isBlockStatement(node)) {
|
||||
return node.body.map((stmt) => clone(stmt));
|
||||
}
|
||||
return [clone(node)];
|
||||
}
|
||||
|
||||
function withTrailingBreak(statements) {
|
||||
if (statements.length === 0) {
|
||||
return [t.breakStatement()];
|
||||
}
|
||||
const last = statements[statements.length - 1];
|
||||
if (t.isBreakStatement(last) || t.isReturnStatement(last) || t.isThrowStatement(last) || t.isContinueStatement(last)) {
|
||||
return statements;
|
||||
}
|
||||
return [...statements, t.breakStatement()];
|
||||
}
|
||||
|
||||
function collectOpcodeCasesFromAlternate(node, discriminantName, cases) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (t.isIfStatement(node)) {
|
||||
const info = getOpcodeTestInfo({
|
||||
isBinaryExpression: () => t.isBinaryExpression(node.test),
|
||||
node: node.test,
|
||||
get(key) {
|
||||
if (key === "left") {
|
||||
return {
|
||||
isNumericLiteral: () => t.isNumericLiteral(node.test.left),
|
||||
isIdentifier: () => t.isIdentifier(node.test.left),
|
||||
node: node.test.left
|
||||
};
|
||||
}
|
||||
return {
|
||||
isNumericLiteral: () => t.isNumericLiteral(node.test.right),
|
||||
isIdentifier: () => t.isIdentifier(node.test.right),
|
||||
node: node.test.right
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (info && info.discriminantName === discriminantName) {
|
||||
cases.push(t.switchCase(info.caseValue, withTrailingBreak(blockify(node.consequent))));
|
||||
collectOpcodeCasesFromAlternate(node.alternate, discriminantName, cases);
|
||||
return;
|
||||
}
|
||||
|
||||
cases.push(t.switchCase(null, withTrailingBreak(blockify(node))));
|
||||
return;
|
||||
}
|
||||
|
||||
if (t.isBlockStatement(node)) {
|
||||
node.body.forEach((stmt) => {
|
||||
if (t.isIfStatement(stmt)) {
|
||||
const info = getOpcodeTestInfo({
|
||||
isBinaryExpression: () => t.isBinaryExpression(stmt.test),
|
||||
node: stmt.test,
|
||||
get(key) {
|
||||
if (key === "left") {
|
||||
return {
|
||||
isNumericLiteral: () => t.isNumericLiteral(stmt.test.left),
|
||||
isIdentifier: () => t.isIdentifier(stmt.test.left),
|
||||
node: stmt.test.left
|
||||
};
|
||||
}
|
||||
return {
|
||||
isNumericLiteral: () => t.isNumericLiteral(stmt.test.right),
|
||||
isIdentifier: () => t.isIdentifier(stmt.test.right),
|
||||
node: stmt.test.right
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (info && info.discriminantName === discriminantName) {
|
||||
cases.push(t.switchCase(info.caseValue, withTrailingBreak(blockify(stmt.consequent))));
|
||||
collectOpcodeCasesFromAlternate(stmt.alternate, discriminantName, cases);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cases.push(t.switchCase(null, withTrailingBreak([clone(stmt)])));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
cases.push(t.switchCase(null, withTrailingBreak([clone(node)])));
|
||||
}
|
||||
|
||||
function convertOpcodeIfChains(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
IfStatement(path) {
|
||||
const info = getOpcodeTestInfo(path.get("test"));
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
t.switchCase(info.caseValue, withTrailingBreak(blockify(path.node.consequent)))
|
||||
];
|
||||
collectOpcodeCasesFromAlternate(path.node.alternate, info.discriminantName, cases);
|
||||
path.replaceWith(t.switchStatement(info.discriminant, cases));
|
||||
path.skip();
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function stripPseudoForLoops(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
ForStatement(path) {
|
||||
const testPath = path.get("test");
|
||||
if (
|
||||
!testPath.isBinaryExpression() ||
|
||||
path.node.update !== null ||
|
||||
!(
|
||||
testPath.get("left").isNumericLiteral() ||
|
||||
testPath.get("right").isNumericLiteral()
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.node.init) {
|
||||
path.replaceWith(clone(path.node.init));
|
||||
} else {
|
||||
path.remove();
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function xiaohongshuWrapperPass(ast) {
|
||||
let changed = false;
|
||||
const body = ast.program.body;
|
||||
|
||||
if (
|
||||
body.length >= 3 &&
|
||||
body[1].type === "VariableDeclaration" &&
|
||||
body[1].declarations.length > 0 &&
|
||||
t.isIdentifier(body[1].declarations[0].id)
|
||||
) {
|
||||
const sandbox = createSandbox();
|
||||
const decoderName = body[1].declarations[0].id.name;
|
||||
|
||||
if (evaluateNodes(body.slice(0, 3), sandbox) && typeof sandbox[decoderName] === "function") {
|
||||
ast.program.body = body.slice(3).map((node) => clone(node));
|
||||
changed = true;
|
||||
|
||||
const aliasNames = new Set([decoderName]);
|
||||
let progress = false;
|
||||
do {
|
||||
progress = false;
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclarator(path) {
|
||||
if (!path.get("id").isIdentifier() || !path.get("init").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
if (aliasNames.has(path.node.init.name) && !aliasNames.has(path.node.id.name)) {
|
||||
aliasNames.add(path.node.id.name);
|
||||
progress = true;
|
||||
}
|
||||
},
|
||||
CallExpression(path) {
|
||||
if (!path.get("callee").isIdentifier() || !aliasNames.has(path.node.callee.name) || path.node.arguments.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const result = evaluateExpression(`${decoderName}(${path.get("arguments.0").toString()})`, sandbox);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
path.replaceWith(t.valueToNode(result.value));
|
||||
progress = true;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
} while (progress);
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclarator(path) {
|
||||
if (!path.get("id").isIdentifier() || !path.get("init").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
if (aliasNames.has(path.node.id.name) && aliasNames.has(path.node.init.name)) {
|
||||
path.remove();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let localChanged = false;
|
||||
do {
|
||||
({ changed: localChanged } = foldWrapperObjects(ast));
|
||||
if (localChanged) {
|
||||
changed = true;
|
||||
}
|
||||
} while (localChanged);
|
||||
|
||||
if (convertOpcodeIfChains(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (stripPseudoForLoops(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
runPatternPass(xiaohongshuWrapperPass);
|
||||
@@ -0,0 +1,202 @@
|
||||
const { clone } = require("../shared");
|
||||
const { runPatternPass } = require("./shared-pattern-pass");
|
||||
const { createSandbox, evaluateExpression, evaluateNodes, generateCode, traverse, t } = require("./pattern-utils");
|
||||
|
||||
function unwrapTopLevelIife(ast) {
|
||||
const first = ast.program.body[0];
|
||||
if (!first || first.type !== "ExpressionStatement" || first.expression.type !== "CallExpression") {
|
||||
return false;
|
||||
}
|
||||
const callee = first.expression.callee;
|
||||
if (callee.type !== "FunctionExpression") {
|
||||
return false;
|
||||
}
|
||||
ast.program.body = callee.body.body.map((node) => clone(node));
|
||||
return true;
|
||||
}
|
||||
|
||||
function expandSequenceExpressions(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
SequenceExpression(path) {
|
||||
if (path.parentPath.isExpressionStatement()) {
|
||||
path.replaceWithMultiple(path.node.expressions.map((expr) => t.expressionStatement(clone(expr))));
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isReturnStatement()) {
|
||||
const expressions = path.node.expressions.map((expr) => clone(expr));
|
||||
const last = expressions.pop();
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
path.parentPath.replaceWithMultiple([
|
||||
...expressions.map((expr) => t.expressionStatement(expr)),
|
||||
t.returnStatement(last)
|
||||
]);
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.parentPath.isVariableDeclarator()) {
|
||||
const declaratorPath = path.parentPath;
|
||||
const declarationPath = declaratorPath.parentPath;
|
||||
if (!declarationPath || !declarationPath.isVariableDeclaration()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expressions = path.node.expressions.map((expr) => clone(expr));
|
||||
const last = expressions.pop();
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inserted = [
|
||||
...expressions.map((expr) => t.expressionStatement(expr)),
|
||||
t.variableDeclaration(declarationPath.node.kind, [
|
||||
t.variableDeclarator(clone(declaratorPath.node.id), last)
|
||||
])
|
||||
];
|
||||
|
||||
declarationPath.insertAfter(inserted);
|
||||
declaratorPath.remove();
|
||||
if (declarationPath.get("declarations").length === 0) {
|
||||
declarationPath.remove();
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function unwrapUnaryIifes(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
UnaryExpression(path) {
|
||||
if (
|
||||
path.node.operator !== "!" ||
|
||||
!path.get("argument").isCallExpression() ||
|
||||
path.get("argument.arguments").length !== 0 ||
|
||||
!path.get("argument.callee").isFunctionExpression() ||
|
||||
path.get("argument.callee.params").length !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
path.replaceWithMultiple(path.get("argument.callee.body").node.body.map((node) => clone(node)));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function yidunDispatcherPass(ast) {
|
||||
let changed = false;
|
||||
|
||||
if (unwrapTopLevelIife(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (expandSequenceExpressions(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (unwrapUnaryIifes(ast)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const body = ast.program.body;
|
||||
if (body.length >= 2 && body[1].type === "FunctionDeclaration" && body[1].id && body[1].params.length === 2) {
|
||||
const sandbox = createSandbox();
|
||||
const decoderName = body[1].id.name;
|
||||
if (evaluateNodes(body.slice(0, 2), sandbox) && typeof sandbox[decoderName] === "function") {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
if (!path.get("callee").isIdentifier({ name: decoderName }) || path.node.arguments.length !== 1 || !path.get("arguments.0").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
const result = evaluateExpression(path.toString(), sandbox);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
path.replaceWith(t.valueToNode(result.value));
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
ast.program.body = ast.program.body.slice(2).map((node) => clone(node));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclaration(path) {
|
||||
if (
|
||||
path.node.declarations.length !== 5 ||
|
||||
!path.get("declarations").every((declarationPath) => {
|
||||
return (
|
||||
declarationPath.get("init").isMemberExpression() &&
|
||||
declarationPath.get("init.object").isNewExpression() &&
|
||||
declarationPath.get("init.property").isStringLiteral()
|
||||
);
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = path.getPrevSibling();
|
||||
const next1 = path.getNextSibling();
|
||||
const next2 = next1 && next1.getNextSibling();
|
||||
if (!prev || !next1 || !next2 || !prev.node || !next1.node || !next2.node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sandbox = createSandbox();
|
||||
if (!evaluateNodes([prev.node, path.node, next1.node, next2.node], sandbox)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const names = []
|
||||
.concat(next1.node.declarations || [])
|
||||
.concat(next2.node.declarations || [])
|
||||
.map((declaration) => declaration.id)
|
||||
.filter((idNode) => t.isIdentifier(idNode))
|
||||
.map((idNode) => idNode.name);
|
||||
|
||||
names.forEach((name) => {
|
||||
const binding = path.scope.getBinding(name);
|
||||
if (!binding || !binding.constant) {
|
||||
return;
|
||||
}
|
||||
binding.referencePaths.forEach((refPath) => {
|
||||
const parent = refPath.parentPath;
|
||||
if (!parent.isMemberExpression({ object: refPath.node }) || !parent.get("property").isNumericLiteral()) {
|
||||
return;
|
||||
}
|
||||
const result = evaluateExpression(generateCode(parent.node), sandbox);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
parent.replaceWith(t.valueToNode(result.value));
|
||||
changed = true;
|
||||
});
|
||||
});
|
||||
|
||||
prev.remove();
|
||||
path.remove();
|
||||
next1.remove();
|
||||
next2.remove();
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
runPatternPass(yidunDispatcherPass);
|
||||
@@ -0,0 +1,304 @@
|
||||
const path = require("path");
|
||||
|
||||
function step(id, script, options = {}) {
|
||||
return {
|
||||
id,
|
||||
script,
|
||||
timeoutMs: 120000,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
const STEP_LIBRARY = {
|
||||
normalize: step("normalize", "normalize-structure.js", { timeoutMs: 60000 }),
|
||||
prune: step("prune", "prune-fake-branches.js", { timeoutMs: 60000 }),
|
||||
inlineDispatchers: step("inline_dispatchers", "inline-dispatchers.js", { timeoutMs: 120000 }),
|
||||
flatten: step("flatten", "flatten-array-control-flow.js", { timeoutMs: 180000 }),
|
||||
ifToSwitch: step("if_to_switch", "if-chain-to-switch.js", { timeoutMs: 90000 }),
|
||||
inlineLiterals: step("inline_literals", "inline-literals.js", { timeoutMs: 90000 }),
|
||||
rename: step("rename_identifiers", "rename-identifiers.js", { timeoutMs: 120000 }),
|
||||
reese84HeavyPass: step("reese84_heavy_pass", "patterns/reese84-heavy-pass.js", { timeoutMs: 30000 }),
|
||||
dingxiangArrayPass: step("dingxiang_array_pass", "patterns/dingxiang-array-pass.js", { timeoutMs: 30000 }),
|
||||
geetest4GuardedPass: step("geetest4_guarded_pass", "patterns/geetest4-guarded-pass.js", { timeoutMs: 30000 }),
|
||||
tonghuashunOrderPass: step("tonghuashun_order_pass", "patterns/tonghuashun-order-pass.js", { timeoutMs: 30000 }),
|
||||
yidunDispatcherPass: step("yidun_dispatcher_pass", "patterns/yidun-dispatcher-pass.js", { timeoutMs: 30000 }),
|
||||
xiaohongshuWrapperPass: step("xiaohongshu_wrapper_pass", "patterns/xiaohongshu-wrapper-pass.js", { timeoutMs: 30000 }),
|
||||
obVariantPass: step("ob_variant_pass", "patterns/ob-variant-pass.js", { timeoutMs: 360000 })
|
||||
};
|
||||
|
||||
const BASE_PIPELINE = [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize
|
||||
];
|
||||
|
||||
const PATTERNS = [
|
||||
{
|
||||
id: "generic",
|
||||
displayName: "generic layered pipeline",
|
||||
reference: null,
|
||||
hintTokens: [],
|
||||
contentRegexes: [],
|
||||
families: ["generic"],
|
||||
notes: [
|
||||
"Use only the generic passes.",
|
||||
"Escalate to a site adapter when residue is stable and reproducible."
|
||||
],
|
||||
steps: BASE_PIPELINE
|
||||
},
|
||||
{
|
||||
id: "reese84",
|
||||
displayName: "reese84",
|
||||
reference: "references/patterns/reese84.md",
|
||||
hintTokens: ["reese84"],
|
||||
contentRegexes: [/reese84/i],
|
||||
families: ["vm-heavy", "dispatcher-heavy"],
|
||||
notes: [
|
||||
"Skip late inline-literals by default because large reese84 samples can stall there.",
|
||||
"Prefer comparing dispatcher residue and flattened handlers against the reference decode."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.reese84HeavyPass,
|
||||
STEP_LIBRARY.rename
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "dingxiang",
|
||||
displayName: "dingxiang",
|
||||
reference: "references/patterns/dingxiang.md",
|
||||
hintTokens: ["\u9876\u8c61", "dingxiang", "dx"],
|
||||
contentRegexes: [/dingxiang/i],
|
||||
families: ["param-array", "dispatcher-heavy"],
|
||||
notes: [
|
||||
"Resolve IIFE parameter backed arrays before judging the flattener result.",
|
||||
"Do not force a second inline-literals pass unless the file stays small."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.dingxiangArrayPass,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "geetest4",
|
||||
displayName: "geetest4",
|
||||
reference: "references/patterns/geetest4.md",
|
||||
hintTokens: ["\u6781\u9a8c4", "geetest4", "geetest"],
|
||||
contentRegexes: [/geetest/i],
|
||||
families: ["guarded-switch", "vm-heavy"],
|
||||
notes: [
|
||||
"Keep the guarded-if adapter outside the generic flattener unless the pattern becomes broadly reusable.",
|
||||
"Current focus is reducing direct loop-switch residue before touching naming."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.geetest4GuardedPass,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "tonghuashun",
|
||||
displayName: "tonghuashun",
|
||||
reference: "references/patterns/tonghuashun.md",
|
||||
hintTokens: ["\u540c\u82b1\u987a", "tonghuashun", "10jqka"],
|
||||
contentRegexes: [/10jqka/i, /split\(\s*["']\|["']\s*\)/],
|
||||
families: ["split-order-source", "dispatcher-wrapper"],
|
||||
notes: [
|
||||
"Add order-source adapters around nested wrapper objects instead of bloating the generic flattener.",
|
||||
"This pipeline intentionally skips inline-literals because it is not the main blocker here."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.tonghuashunOrderPass,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "yidun",
|
||||
displayName: "yidun",
|
||||
reference: "references/patterns/yidun.md",
|
||||
hintTokens: ["\u7f51\u6613\u6613\u76fe", "yidun", "163"],
|
||||
contentRegexes: [/yidun/i],
|
||||
families: ["dispatcher-heavy"],
|
||||
notes: [
|
||||
"One inline-literals pass can still help here, but keep it early enough to avoid blowups.",
|
||||
"Compare remaining dispatcher wrappers against the reference decode."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.yidunDispatcherPass,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.inlineLiterals,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.rename
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "xiaohongshu",
|
||||
displayName: "xiaohongshu",
|
||||
reference: "references/patterns/xiaohongshu.md",
|
||||
hintTokens: ["\u5c0f\u7ea2\u4e66", "xiaohongshu", "xhs"],
|
||||
contentRegexes: [/xiaohongshu/i, /\bxhs\b/i],
|
||||
families: ["wrapper-light"],
|
||||
notes: [
|
||||
"Treat this as a lighter wrapper cleanup path.",
|
||||
"Prefer readability and structural cleanup over aggressive evaluation."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.xiaohongshuWrapperPass,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.xiaohongshuWrapperPass,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.rename
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "cn-bidding-ob",
|
||||
displayName: "cn-bidding-ob",
|
||||
reference: "references/patterns/cn-bidding-ob.md",
|
||||
hintTokens: ["\u4e2d\u56fd\u62db\u6807\u6295\u6807\u516c\u5171\u670d\u52a1\u5e73\u53f0", "\u62db\u6807\u6295\u6807", "ob"],
|
||||
contentRegexes: [/\b_0x[a-f0-9]+\b/i],
|
||||
families: ["ob-variant"],
|
||||
notes: [
|
||||
"Keep OB-family tweaks in a dedicated adapter so the common pipeline stays conservative.",
|
||||
"Track remaining loop-switch residue after the OB adapter stage."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.obVariantPass,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.obVariantPass,
|
||||
STEP_LIBRARY.normalize
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "mps-ob",
|
||||
displayName: "mps-ob",
|
||||
reference: "references/patterns/mps-ob.md",
|
||||
hintTokens: ["\u4e2d\u534e\u4eba\u6c11\u5171\u548c\u56fd\u516c\u5b89\u90e8", "\u516c\u5b89\u90e8", "mps", "ob"],
|
||||
contentRegexes: [/\b_0x[a-f0-9]+\b/i],
|
||||
families: ["ob-variant"],
|
||||
notes: [
|
||||
"Share the OB-family adapter but keep a separate rule document because the residue profile is different from the bidding sample.",
|
||||
"Compare remaining loop-switch blocks directly against the reference decode."
|
||||
],
|
||||
steps: [
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.inlineDispatchers,
|
||||
STEP_LIBRARY.obVariantPass,
|
||||
STEP_LIBRARY.flatten,
|
||||
STEP_LIBRARY.ifToSwitch,
|
||||
STEP_LIBRARY.prune,
|
||||
STEP_LIBRARY.obVariantPass,
|
||||
STEP_LIBRARY.normalize,
|
||||
STEP_LIBRARY.rename
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function normalizeHint(value) {
|
||||
return String(value || "").toLowerCase();
|
||||
}
|
||||
|
||||
function collectDetection(pattern, inputPath, hint, sourceText) {
|
||||
let score = 0;
|
||||
const reasons = [];
|
||||
const haystacks = [normalizeHint(inputPath), normalizeHint(hint)];
|
||||
for (const token of pattern.hintTokens) {
|
||||
const normalized = normalizeHint(token);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
if (haystacks.some((value) => value.includes(normalized))) {
|
||||
score += 10;
|
||||
reasons.push(`hint:${token}`);
|
||||
}
|
||||
}
|
||||
for (const regex of pattern.contentRegexes) {
|
||||
if (regex.test(sourceText)) {
|
||||
score += 3;
|
||||
reasons.push(`content:${regex}`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: pattern.id,
|
||||
score,
|
||||
reasons
|
||||
};
|
||||
}
|
||||
|
||||
function detectPatterns({ inputPath, hint = "", sourceText }) {
|
||||
const detections = PATTERNS
|
||||
.filter((pattern) => pattern.id !== "generic")
|
||||
.map((pattern) => collectDetection(pattern, inputPath, hint, sourceText))
|
||||
.filter((entry) => entry.score > 0)
|
||||
.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
|
||||
|
||||
const bestId = detections.length > 0 ? detections[0].id : "generic";
|
||||
return {
|
||||
bestId,
|
||||
detections,
|
||||
selected: getPattern(bestId)
|
||||
};
|
||||
}
|
||||
|
||||
function getPattern(id) {
|
||||
return PATTERNS.find((pattern) => pattern.id === id) || PATTERNS[0];
|
||||
}
|
||||
|
||||
function buildPipeline(patternId) {
|
||||
const pattern = getPattern(patternId);
|
||||
return pattern.steps.map((entry) => ({
|
||||
...entry,
|
||||
scriptPath: path.join(__dirname, entry.script)
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BASE_PIPELINE,
|
||||
PATTERNS,
|
||||
STEP_LIBRARY,
|
||||
buildPipeline,
|
||||
detectPatterns,
|
||||
getPattern
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
const { parseArgs, parseFile, reparse, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
function isLiteralLike(node) {
|
||||
return (
|
||||
t.isStringLiteral(node) ||
|
||||
t.isNumericLiteral(node) ||
|
||||
t.isBooleanLiteral(node) ||
|
||||
t.isNullLiteral(node) ||
|
||||
t.isIdentifier(node, { name: "undefined" })
|
||||
);
|
||||
}
|
||||
|
||||
function evaluateBinary(node) {
|
||||
if (!t.isBinaryExpression(node) || !["==", "===", "!=", "!=="].includes(node.operator)) {
|
||||
return null;
|
||||
}
|
||||
if (!isLiteralLike(node.left) || !isLiteralLike(node.right)) {
|
||||
return null;
|
||||
}
|
||||
const left = t.isIdentifier(node.left, { name: "undefined" }) ? undefined : node.left.value;
|
||||
const right = t.isIdentifier(node.right, { name: "undefined" }) ? undefined : node.right.value;
|
||||
switch (node.operator) {
|
||||
case "==":
|
||||
return left == right;
|
||||
case "===":
|
||||
return left === right;
|
||||
case "!=":
|
||||
return left != right;
|
||||
case "!==":
|
||||
return left !== right;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceStatement(path, node) {
|
||||
if (!node) {
|
||||
path.remove();
|
||||
return;
|
||||
}
|
||||
if (t.isBlockStatement(node)) {
|
||||
path.replaceWithMultiple(node.body);
|
||||
return;
|
||||
}
|
||||
path.replaceWith(node);
|
||||
}
|
||||
|
||||
function simplify(ast) {
|
||||
let changed = false;
|
||||
|
||||
traverse(ast, {
|
||||
IfStatement: {
|
||||
exit(path) {
|
||||
let decision = evaluateBinary(path.node.test);
|
||||
if (decision === null) {
|
||||
const evaluated = path.get("test").evaluate();
|
||||
if (evaluated.confident && typeof evaluated.value === "boolean") {
|
||||
decision = evaluated.value;
|
||||
}
|
||||
}
|
||||
if (decision === null) {
|
||||
return;
|
||||
}
|
||||
replaceStatement(path, decision ? path.node.consequent : path.node.alternate);
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
ConditionalExpression(path) {
|
||||
let decision = evaluateBinary(path.node.test);
|
||||
if (decision === null) {
|
||||
const evaluated = path.get("test").evaluate();
|
||||
if (evaluated.confident && typeof evaluated.value === "boolean") {
|
||||
decision = evaluated.value;
|
||||
}
|
||||
}
|
||||
if (decision === null) {
|
||||
return;
|
||||
}
|
||||
path.replaceWith(decision ? path.node.consequent : path.node.alternate);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
return { ast, changed };
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
let ast = parseFile(inputPath);
|
||||
let changed = false;
|
||||
do {
|
||||
({ ast, changed } = simplify(ast));
|
||||
if (changed) {
|
||||
ast = reparse(ast);
|
||||
}
|
||||
} while (changed);
|
||||
saveAst(ast, outputPath);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
const { parseArgs, parseFile, saveAst, t, traverse } = require("./shared");
|
||||
|
||||
const typeAlias = {
|
||||
ArrayExpression: "array",
|
||||
AssignmentExpression: "assign",
|
||||
AwaitExpression: "await",
|
||||
BinaryExpression: "bin",
|
||||
BooleanLiteral: "bool",
|
||||
CallExpression: "call",
|
||||
ConditionalExpression: "cond",
|
||||
FunctionExpression: "func",
|
||||
Identifier: "id",
|
||||
LogicalExpression: "logic",
|
||||
MemberExpression: "member",
|
||||
NewExpression: "new",
|
||||
NullLiteral: "null",
|
||||
NumericLiteral: "num",
|
||||
ObjectExpression: "obj",
|
||||
SequenceExpression: "seq",
|
||||
StringLiteral: "str",
|
||||
ThisExpression: "this",
|
||||
UnaryExpression: "unary"
|
||||
};
|
||||
|
||||
function shouldRename(name) {
|
||||
return /^_0x/i.test(name) || /^[a-zA-Z]$/.test(name);
|
||||
}
|
||||
|
||||
function buildName(scopeTag, binding, init) {
|
||||
const fromInit = init && typeAlias[init.type];
|
||||
const fromRef = binding && binding.referencePaths[0] && typeAlias[binding.referencePaths[0].parentPath.type];
|
||||
const base = fromInit || fromRef || "value";
|
||||
return `${scopeTag}_${base}`;
|
||||
}
|
||||
|
||||
const { inputPath, outputPath } = parseArgs();
|
||||
const ast = parseFile(inputPath);
|
||||
|
||||
traverse(ast, {
|
||||
VariableDeclarator(path) {
|
||||
if (!path.get("id").isIdentifier()) {
|
||||
return;
|
||||
}
|
||||
const name = path.node.id.name;
|
||||
if (!shouldRename(name)) {
|
||||
return;
|
||||
}
|
||||
const binding = path.scope.getBinding(name);
|
||||
const scopeTag = path.scope.block.type === "Program" ? "glb" : "loc";
|
||||
const nextName = path.scope.generateUidIdentifier(buildName(scopeTag, binding, path.node.init)).name;
|
||||
path.scope.rename(name, nextName);
|
||||
},
|
||||
CatchClause(path) {
|
||||
if (path.node.param && t.isIdentifier(path.node.param)) {
|
||||
path.scope.rename(path.node.param.name, path.scope.generateUidIdentifier("error_msg").name);
|
||||
}
|
||||
},
|
||||
"FunctionDeclaration|FunctionExpression|ArrowFunctionExpression": {
|
||||
exit(path) {
|
||||
if (path.node.id && t.isIdentifier(path.node.id) && shouldRename(path.node.id.name)) {
|
||||
path.scope.rename(path.node.id.name, path.scope.generateUidIdentifier("fn").name);
|
||||
}
|
||||
path.node.params.forEach((param, index) => {
|
||||
if (t.isIdentifier(param) && shouldRename(param.name)) {
|
||||
path.scope.rename(param.name, `arg_${index + 1}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
saveAst(ast, outputPath);
|
||||
@@ -0,0 +1,122 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { buildPipeline, detectPatterns, getPattern } = require("./pipeline-config");
|
||||
const { collectMetricsFromFile } = require("./collect-residue-metrics");
|
||||
|
||||
function sanitizeStepName(name) {
|
||||
return String(name).replace(/[^a-z0-9_]+/gi, "_");
|
||||
}
|
||||
|
||||
function runStep(nodePath, step, currentPath, nextPath) {
|
||||
const start = Date.now();
|
||||
const result = spawnSync(nodePath, [step.scriptPath, currentPath, nextPath], {
|
||||
encoding: "utf8",
|
||||
env: process.env,
|
||||
timeout: step.timeoutMs
|
||||
});
|
||||
const durationMs = Date.now() - start;
|
||||
const stdout = result.stdout ? result.stdout.trim() : "";
|
||||
const stderr = result.stderr ? result.stderr.trim() : "";
|
||||
const timedOut = Boolean(result.error && result.error.code === "ETIMEDOUT");
|
||||
return {
|
||||
id: step.id,
|
||||
script: step.script,
|
||||
outputFile: path.basename(nextPath),
|
||||
timeoutMs: step.timeoutMs,
|
||||
durationMs,
|
||||
status: timedOut ? "timed_out" : result.status === 0 ? "ok" : "failed",
|
||||
exitCode: typeof result.status === "number" ? result.status : null,
|
||||
stdout,
|
||||
stderr,
|
||||
error: result.error ? String(result.error.message || result.error) : ""
|
||||
};
|
||||
}
|
||||
|
||||
function copyFile(fromPath, toPath) {
|
||||
fs.copyFileSync(fromPath, toPath);
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [, , inputPath, outputDir, hint = ""] = process.argv;
|
||||
if (!inputPath || !outputDir) {
|
||||
console.error("Usage: node run-pipeline.js <input.js> <output-dir> [hint]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const absoluteInput = path.resolve(inputPath);
|
||||
const absoluteOutputDir = path.resolve(outputDir);
|
||||
ensureDir(absoluteOutputDir);
|
||||
|
||||
const sourceText = fs.readFileSync(absoluteInput, "utf8");
|
||||
const detection = detectPatterns({
|
||||
inputPath: absoluteInput,
|
||||
hint,
|
||||
sourceText
|
||||
});
|
||||
const pattern = getPattern(detection.bestId);
|
||||
const pipeline = buildPipeline(pattern.id);
|
||||
|
||||
const report = {
|
||||
inputPath: absoluteInput,
|
||||
outputDir: absoluteOutputDir,
|
||||
hint,
|
||||
selectedPattern: {
|
||||
id: pattern.id,
|
||||
displayName: pattern.displayName,
|
||||
reference: pattern.reference,
|
||||
families: pattern.families,
|
||||
notes: pattern.notes
|
||||
},
|
||||
detections: detection.detections,
|
||||
steps: [],
|
||||
status: "ok",
|
||||
failedStep: null,
|
||||
lastGoodFile: "00_source.js",
|
||||
finalMetrics: null
|
||||
};
|
||||
|
||||
const sourceCopyPath = path.join(absoluteOutputDir, "00_source.js");
|
||||
copyFile(absoluteInput, sourceCopyPath);
|
||||
|
||||
let currentPath = sourceCopyPath;
|
||||
for (let index = 0; index < pipeline.length; index += 1) {
|
||||
const step = pipeline[index];
|
||||
const stepName = `${String(index + 1).padStart(2, "0")}_${sanitizeStepName(step.id)}.js`;
|
||||
const nextPath = path.join(absoluteOutputDir, stepName);
|
||||
const outcome = runStep(process.execPath, step, currentPath, nextPath);
|
||||
report.steps.push(outcome);
|
||||
if (outcome.status !== "ok") {
|
||||
report.status = outcome.status;
|
||||
report.failedStep = step.id;
|
||||
break;
|
||||
}
|
||||
currentPath = nextPath;
|
||||
report.lastGoodFile = stepName;
|
||||
}
|
||||
|
||||
const finalPath = path.join(absoluteOutputDir, "final.js");
|
||||
copyFile(currentPath, finalPath);
|
||||
report.finalMetrics = collectMetricsFromFile(finalPath);
|
||||
|
||||
fs.writeFileSync(path.join(absoluteOutputDir, "selected-patterns.json"), JSON.stringify({
|
||||
bestId: detection.bestId,
|
||||
detections: detection.detections,
|
||||
selectedPattern: report.selectedPattern
|
||||
}, null, 2), "utf8");
|
||||
fs.writeFileSync(path.join(absoluteOutputDir, "pipeline-report.json"), JSON.stringify(report, null, 2), "utf8");
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
status: report.status,
|
||||
selectedPattern: report.selectedPattern.id,
|
||||
failedStep: report.failedStep,
|
||||
lastGoodFile: report.lastGoodFile
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
const fs = require("fs");
|
||||
const parser = require("@babel/parser");
|
||||
const traverse = require("@babel/traverse").default;
|
||||
const generator = require("@babel/generator").default;
|
||||
const t = require("@babel/types");
|
||||
|
||||
function parseFile(filePath) {
|
||||
return parser.parse(fs.readFileSync(filePath, "utf8"), {
|
||||
sourceType: "unambiguous",
|
||||
plugins: ["jsx"]
|
||||
});
|
||||
}
|
||||
|
||||
function printAst(ast) {
|
||||
return generator(ast, {
|
||||
compact: false,
|
||||
comments: false,
|
||||
jsescOption: { minimal: true }
|
||||
}).code;
|
||||
}
|
||||
|
||||
function saveAst(ast, filePath) {
|
||||
fs.writeFileSync(filePath, printAst(ast), "utf8");
|
||||
}
|
||||
|
||||
function reparse(ast) {
|
||||
return parser.parse(printAst(ast), {
|
||||
sourceType: "unambiguous",
|
||||
plugins: ["jsx"]
|
||||
});
|
||||
}
|
||||
|
||||
function clone(node) {
|
||||
return t.cloneNode(node, true);
|
||||
}
|
||||
|
||||
function isPrimitiveNode(node) {
|
||||
return t.isStringLiteral(node) || t.isNumericLiteral(node) || t.isBooleanLiteral(node) || t.isNullLiteral(node) || t.isIdentifier(node, { name: "undefined" });
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const [, , inputPath, outputPath] = process.argv;
|
||||
if (!inputPath || !outputPath) {
|
||||
console.error("Usage: node <script> <input.js> <output.js>");
|
||||
process.exit(1);
|
||||
}
|
||||
return { inputPath, outputPath };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clone,
|
||||
isPrimitiveNode,
|
||||
parseArgs,
|
||||
parseFile,
|
||||
printAst,
|
||||
reparse,
|
||||
saveAst,
|
||||
t,
|
||||
traverse
|
||||
};
|
||||
Reference in New Issue
Block a user