Skip to main content

js_deobfuscator/transform/
alias.rs

1//! Alias inlining: `var e = Yp; e(445)` → `Yp(445)`.
2//!
3//! Two-pass: collect identifier-to-identifier aliases with 0 writes,
4//! then replace all reads of the alias with the target identifier.
5//! Transitive chains resolve through convergence loop iterations.
6
7use 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
20/// Alias inlining module.
21pub struct AliasInliner;
22
23impl Module for AliasInliner {
24    fn name(&self) -> &'static str {
25        "AliasInliner"
26    }
27
28    fn changes_symbols(&self) -> bool {
29        // Creates new IdentifierReferences that need scoping rebuild
30        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        // Skip well-known globals that aren't aliases
67        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}