Skip to main content

js_deobfuscator/transform/
constant.rs

1//! Constant propagation: inline literal values into their usage sites.
2//!
3//! `var x = 5; return x;` → `var x = 5; return 5;`
4//!
5//! Two-pass:
6//!   Pass 1 (Collect): Find var/let/const with literal init and 0 writes.
7//!   Pass 2 (Inline): Replace all read references with the literal.
8
9use rustc_hash::FxHashMap;
10
11use oxc::allocator::Allocator;
12use oxc::ast::ast::{Expression, Program};
13use oxc::semantic::{Scoping, SymbolId};
14
15use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
16
17use crate::ast::extract;
18use crate::ast::create;
19use crate::engine::error::Result;
20use crate::engine::module::{Module, TransformResult};
21use crate::scope::{query, resolve};
22use crate::value::JsValue;
23
24/// Constant propagation module.
25pub struct ConstantPropagator;
26
27impl Module for ConstantPropagator {
28    fn name(&self) -> &'static str {
29        "ConstantPropagator"
30    }
31
32    fn changes_symbols(&self) -> bool {
33        // Replaces identifier references with literals — need scoping rebuild
34        true
35    }
36
37    fn transform<'a>(
38        &mut self,
39        allocator: &'a Allocator,
40        program: &mut Program<'a>,
41        scoping: Scoping,
42    ) -> Result<TransformResult> {
43        // Pass 1: Collect
44        let mut collector = Collector::default();
45        let scoping = traverse_mut(&mut collector, allocator, program, scoping, ());
46
47        if collector.constants.is_empty() {
48            return Ok(TransformResult { modifications: 0, scoping });
49        }
50
51        // Pass 2: Inline
52        let mut inliner = Inliner { constants: collector.constants, modifications: 0 };
53        let scoping = traverse_mut(&mut inliner, allocator, program, scoping, ());
54
55        Ok(TransformResult { modifications: inliner.modifications, scoping })
56    }
57}
58
59// ============================================================================
60// Collector
61// ============================================================================
62
63#[derive(Default)]
64struct Collector {
65    constants: FxHashMap<SymbolId, JsValue>,
66}
67
68impl<'a> Traverse<'a, ()> for Collector {
69    fn enter_variable_declarator(
70        &mut self,
71        node: &mut oxc::ast::ast::VariableDeclarator<'a>,
72        ctx: &mut TraverseCtx<'a, ()>,
73    ) {
74        let Some(init) = &node.init else { return };
75        let Some(symbol_id) = resolve::get_declarator_symbol(node) else { return };
76        let Some(value) = extract::js_value(init) else { return };
77
78        // Must have no write references
79        if query::has_writes(ctx.scoping(), symbol_id) {
80            return;
81        }
82
83        self.constants.insert(symbol_id, value);
84    }
85}
86
87// ============================================================================
88// Inliner
89// ============================================================================
90
91struct Inliner {
92    constants: FxHashMap<SymbolId, JsValue>,
93    modifications: usize,
94}
95
96impl<'a> Traverse<'a, ()> for Inliner {
97    fn exit_expression(
98        &mut self,
99        expr: &mut Expression<'a>,
100        ctx: &mut TraverseCtx<'a, ()>,
101    ) {
102        let Expression::Identifier(ident) = &*expr else { return };
103        let Some(symbol_id) = resolve::get_reference_symbol(ctx.scoping(), ident) else { return };
104        let Some(value) = self.constants.get(&symbol_id) else { return };
105
106        *expr = create::from_js_value(value, &ctx.ast);
107        self.modifications += 1;
108    }
109}
110
111// ============================================================================
112// Tests
113// ============================================================================
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use oxc::codegen::Codegen;
119    use oxc::parser::Parser;
120    use oxc::semantic::SemanticBuilder;
121    use oxc::span::SourceType;
122
123    fn propagate(source: &str) -> (String, usize) {
124        let allocator = Allocator::default();
125        let mut program = Parser::new(&allocator, source, SourceType::mjs())
126            .parse()
127            .program;
128        let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
129
130        let mut module = ConstantPropagator;
131        let result = module.transform(&allocator, &mut program, scoping).unwrap();
132        (Codegen::new().build(&program).code, result.modifications)
133    }
134
135    #[test]
136    fn test_number() {
137        let (code, mods) = propagate("var x = 42; console.log(x);");
138        assert!(mods > 0);
139        assert!(code.contains("console.log(42)"), "got: {code}");
140    }
141
142    #[test]
143    fn test_string() {
144        let (code, mods) = propagate("var msg = \"hello\"; alert(msg);");
145        assert!(mods > 0);
146        assert!(code.contains("alert(\"hello\")"), "got: {code}");
147    }
148
149    #[test]
150    fn test_boolean() {
151        let (code, mods) = propagate("const flag = true; if (flag) {}");
152        assert!(mods > 0);
153        assert!(code.contains("if (true)"), "got: {code}");
154    }
155
156    #[test]
157    fn test_no_propagate_with_writes() {
158        let (_, mods) = propagate("var x = 1; x = 2; console.log(x);");
159        assert_eq!(mods, 0, "should not propagate reassigned var");
160    }
161
162    #[test]
163    fn test_no_propagate_non_literal() {
164        let (_, mods) = propagate("var x = foo(); console.log(x);");
165        assert_eq!(mods, 0, "should not propagate call result");
166    }
167
168    #[test]
169    fn test_multiple_refs() {
170        let (code, mods) = propagate("var x = 5; f(x); g(x);");
171        assert_eq!(mods, 2, "should inline both references");
172        assert!(code.contains("f(5)"), "got: {code}");
173        assert!(code.contains("g(5)"), "got: {code}");
174    }
175}