js_deobfuscator/transform/
alias.rs1use rustc_hash::FxHashMap;
8
9use oxc::allocator::Allocator;
10use oxc::ast::ast::{Expression, Program};
11use oxc::semantic::{Scoping, SymbolId};
12use oxc::span::SPAN;
13
14use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
15
16use crate::engine::error::Result;
17use crate::engine::module::{Module, TransformResult};
18use crate::scope::{query, resolve};
19
20pub struct AliasInliner;
22
23impl Module for AliasInliner {
24 fn name(&self) -> &'static str {
25 "AliasInliner"
26 }
27
28 fn changes_symbols(&self) -> bool {
29 true
31 }
32
33 fn transform<'a>(
34 &mut self,
35 allocator: &'a Allocator,
36 program: &mut Program<'a>,
37 scoping: Scoping,
38 ) -> Result<TransformResult> {
39 let mut collector = AliasCollector::default();
40 let scoping = traverse_mut(&mut collector, allocator, program, scoping, ());
41
42 if collector.aliases.is_empty() {
43 return Ok(TransformResult { modifications: 0, scoping });
44 }
45
46 let mut inliner = Inliner { aliases: collector.aliases, modifications: 0 };
47 let scoping = traverse_mut(&mut inliner, allocator, program, scoping, ());
48
49 Ok(TransformResult { modifications: inliner.modifications, scoping })
50 }
51}
52
53#[derive(Default)]
54struct AliasCollector {
55 aliases: FxHashMap<SymbolId, String>,
56}
57
58impl<'a> Traverse<'a, ()> for AliasCollector {
59 fn enter_variable_declarator(
60 &mut self,
61 node: &mut oxc::ast::ast::VariableDeclarator<'a>,
62 ctx: &mut TraverseCtx<'a, ()>,
63 ) {
64 let Some(Expression::Identifier(target)) = &node.init else { return };
65
66 if matches!(target.name.as_str(), "undefined" | "NaN" | "Infinity") {
68 return;
69 }
70
71 let target_name = target.name.to_string();
72 let Some(symbol_id) = resolve::get_declarator_symbol(node) else { return };
73
74 if query::has_writes(ctx.scoping(), symbol_id) {
75 return;
76 }
77
78 self.aliases.insert(symbol_id, target_name);
79 }
80}
81
82struct Inliner {
83 aliases: FxHashMap<SymbolId, String>,
84 modifications: usize,
85}
86
87impl<'a> Traverse<'a, ()> for Inliner {
88 fn exit_expression(
89 &mut self,
90 expr: &mut Expression<'a>,
91 ctx: &mut TraverseCtx<'a, ()>,
92 ) {
93 let Expression::Identifier(ident) = &*expr else { return };
94 let Some(symbol_id) = resolve::get_reference_symbol(ctx.scoping(), ident) else { return };
95 let Some(target_name) = self.aliases.get(&symbol_id) else { return };
96
97 let arena_name = ctx.ast.str(target_name);
98 let new_ident = ctx.ast.alloc_identifier_reference(SPAN, arena_name);
99 *expr = Expression::Identifier(new_ident);
100 self.modifications += 1;
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use oxc::codegen::Codegen;
108 use oxc::parser::Parser;
109 use oxc::semantic::SemanticBuilder;
110 use oxc::span::SourceType;
111
112 fn inline(source: &str) -> (String, usize) {
113 let allocator = Allocator::default();
114 let mut program = Parser::new(&allocator, source, SourceType::mjs())
115 .parse().program;
116 let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
117 let mut module = AliasInliner;
118 let result = module.transform(&allocator, &mut program, scoping).unwrap();
119 (Codegen::new().build(&program).code, result.modifications)
120 }
121
122 #[test]
123 fn test_simple_alias() {
124 let (code, mods) = inline("var e = Yp; console.log(e(445));");
125 assert!(mods > 0);
126 assert!(code.contains("Yp(445)"), "e → Yp: {code}");
127 }
128
129 #[test]
130 fn test_no_inline_with_writes() {
131 let (_, mods) = inline("var e = Yp; e = other; console.log(e(445));");
132 assert_eq!(mods, 0);
133 }
134
135 #[test]
136 fn test_no_inline_literal() {
137 let (_, mods) = inline("var e = 42; console.log(e);");
138 assert_eq!(mods, 0, "literal not an alias");
139 }
140
141 #[test]
142 fn test_multiple() {
143 let (code, mods) = inline("var a = X; var b = Y; console.log(a(1), b(2));");
144 assert!(mods >= 2);
145 assert!(code.contains("X(1)"), "got: {code}");
146 assert!(code.contains("Y(2)"), "got: {code}");
147 }
148
149 #[test]
150 fn test_skip_undefined() {
151 let (_, mods) = inline("var x = undefined; console.log(x);");
152 assert_eq!(mods, 0, "undefined is not an alias");
153 }
154}