js_deobfuscator/transform/
constant.rs1use 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
24pub struct ConstantPropagator;
26
27impl Module for ConstantPropagator {
28 fn name(&self) -> &'static str {
29 "ConstantPropagator"
30 }
31
32 fn changes_symbols(&self) -> bool {
33 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 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 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#[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 if query::has_writes(ctx.scoping(), symbol_id) {
80 return;
81 }
82
83 self.constants.insert(symbol_id, value);
84 }
85}
86
87struct 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#[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}