js_deobfuscator/transform/
dead.rs1use oxc::allocator::Allocator;
33use oxc::ast::ast::{Program, Statement};
34use oxc::semantic::{Scoping, SymbolId};
35
36use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
37
38use crate::ast::query;
39use crate::engine::error::Result;
40use crate::engine::module::{Module, TransformResult};
41use crate::scope::{query as scope_query, resolve};
42
43pub struct DeadCodeEliminator;
45
46impl Module for DeadCodeEliminator {
47 fn name(&self) -> &'static str {
48 "DeadCodeEliminator"
49 }
50
51 fn changes_symbols(&self) -> bool {
52 true
53 }
54
55 fn transform<'a>(
56 &mut self,
57 allocator: &'a Allocator,
58 program: &mut Program<'a>,
59 scoping: Scoping,
60 ) -> Result<TransformResult> {
61 let mut visitor = DeadCodeVisitor { modifications: 0 };
62 let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
63 Ok(TransformResult { modifications: visitor.modifications, scoping })
64 }
65}
66
67struct DeadCodeVisitor {
68 modifications: usize,
69}
70
71impl<'a> Traverse<'a, ()> for DeadCodeVisitor {
72 fn exit_statements(
73 &mut self,
74 stmts: &mut oxc::allocator::Vec<'a, Statement<'a>>,
75 ctx: &mut TraverseCtx<'a, ()>,
76 ) {
77 let allocator = ctx.ast.allocator;
78 let mut new_stmts = ctx.ast.vec();
79 let mut removed = 0;
80
81 for stmt in stmts.drain(..) {
82 if should_remove(&stmt, ctx.scoping()) {
83 removed += 1;
84 continue;
85 }
86 new_stmts.push(stmt);
87 }
88
89 *stmts = new_stmts;
90 let _ = allocator;
91 self.modifications += removed;
92 }
93}
94
95#[inline]
97fn is_module_scope(scoping: &Scoping, symbol_id: SymbolId) -> bool {
98 scoping.symbol_scope_id(symbol_id) == scoping.root_scope_id()
99}
100
101fn should_remove(stmt: &Statement, scoping: &Scoping) -> bool {
105 match stmt {
106 Statement::VariableDeclaration(decl) => {
107 decl.declarations.iter().all(|d| {
110 let Some(symbol_id) = resolve::get_declarator_symbol(d) else {
111 return false;
112 };
113 if is_module_scope(scoping, symbol_id) {
115 return false;
116 }
117 if scope_query::has_reads(scoping, symbol_id) {
118 return false;
119 }
120 d.init.as_ref().is_none_or(|init| query::is_side_effect_free(init))
121 })
122 }
123 Statement::FunctionDeclaration(_) => false,
129 Statement::EmptyStatement(_) => true,
130 _ => false,
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use oxc::codegen::Codegen;
138 use oxc::parser::Parser;
139 use oxc::semantic::SemanticBuilder;
140 use oxc::span::SourceType;
141
142 fn eliminate(source: &str) -> (String, usize) {
143 let allocator = Allocator::default();
144 let mut program = Parser::new(&allocator, source, SourceType::mjs())
145 .parse()
146 .program;
147 let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
148
149 let mut module = DeadCodeEliminator;
150 let result = module.transform(&allocator, &mut program, scoping).unwrap();
151 (Codegen::new().build(&program).code, result.modifications)
152 }
153
154 #[test]
157 fn test_keep_module_scope_unused_var() {
158 let (code, mods) = eliminate("var x = 1; var y = 2; console.log(y);");
161 assert_eq!(mods, 0, "module-scope vars must be preserved");
162 assert!(code.contains("var x"), "should keep module-scope x: {code}");
163 assert!(code.contains("var y"), "should keep module-scope y: {code}");
164 }
165
166 #[test]
167 fn test_keep_unused_function_declaration() {
168 let (code, mods) = eliminate("function unused() {} console.log(1);");
171 assert_eq!(mods, 0, "function declarations must never be removed");
172 assert!(code.contains("function unused"), "must preserve unused: {code}");
173 }
174
175 #[test]
176 fn test_keep_used_function() {
177 let (code, _) = eliminate("function used() {} used();");
178 assert!(code.contains("function used"), "should keep used function: {code}");
179 }
180
181 #[test]
182 fn test_keep_side_effectful_module_var() {
183 let (code, mods) = eliminate("var x = foo(); console.log(1);");
184 assert_eq!(mods, 0, "should not remove side-effectful init");
185 assert!(code.contains("foo()"), "got: {code}");
186 }
187
188 #[test]
191 fn test_remove_function_scope_unused_var() {
192 let (code, mods) = eliminate(
193 "function f() { var x = 1; var y = 2; return y; }"
194 );
195 assert!(mods > 0, "function-scope unused var should be removed");
196 assert!(!code.contains("var x"), "should remove function-scope x: {code}");
197 assert!(code.contains("var y"), "should keep used y: {code}");
198 }
199
200 #[test]
201 fn test_keep_function_scope_side_effect_var() {
202 let (code, mods) = eliminate(
203 "function f() { var x = foo(); return 1; }"
204 );
205 assert_eq!(mods, 0, "side-effectful init must be preserved");
206 assert!(code.contains("foo()"), "got: {code}");
207 }
208
209 #[test]
210 fn test_keep_nested_function_declaration() {
211 let (code, mods) = eliminate(
214 "function outer() { function helper() {} return 1; }"
215 );
216 assert_eq!(mods, 0, "nested function declarations must be preserved");
217 assert!(code.contains("function helper"), "got: {code}");
218 }
219
220 #[test]
221 fn test_remove_empty_statement() {
222 let (code, mods) = eliminate("function f() { ;;; return 1; }");
223 assert!(mods > 0, "empty statements always removable");
224 assert!(!code.contains(";;"), "got: {code}");
225 }
226}