use oxc::allocator::Allocator;
use oxc::ast::ast::{Program, Statement};
use oxc::semantic::{Scoping, SymbolId};
use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
use crate::ast::query;
use crate::engine::error::Result;
use crate::engine::module::{Module, TransformResult};
use crate::scope::{query as scope_query, resolve};
pub struct DeadCodeEliminator;
impl Module for DeadCodeEliminator {
fn name(&self) -> &'static str {
"DeadCodeEliminator"
}
fn changes_symbols(&self) -> bool {
true
}
fn transform<'a>(
&mut self,
allocator: &'a Allocator,
program: &mut Program<'a>,
scoping: Scoping,
) -> Result<TransformResult> {
let mut visitor = DeadCodeVisitor { modifications: 0 };
let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
Ok(TransformResult { modifications: visitor.modifications, scoping })
}
}
struct DeadCodeVisitor {
modifications: usize,
}
impl<'a> Traverse<'a, ()> for DeadCodeVisitor {
fn exit_statements(
&mut self,
stmts: &mut oxc::allocator::Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a, ()>,
) {
let allocator = ctx.ast.allocator;
let mut new_stmts = ctx.ast.vec();
let mut removed = 0;
for stmt in stmts.drain(..) {
if should_remove(&stmt, ctx.scoping()) {
removed += 1;
continue;
}
new_stmts.push(stmt);
}
*stmts = new_stmts;
let _ = allocator;
self.modifications += removed;
}
}
#[inline]
fn is_module_scope(scoping: &Scoping, symbol_id: SymbolId) -> bool {
scoping.symbol_scope_id(symbol_id) == scoping.root_scope_id()
}
fn should_remove(stmt: &Statement, scoping: &Scoping) -> bool {
match stmt {
Statement::VariableDeclaration(decl) => {
decl.declarations.iter().all(|d| {
let Some(symbol_id) = resolve::get_declarator_symbol(d) else {
return false;
};
if is_module_scope(scoping, symbol_id) {
return false;
}
if scope_query::has_reads(scoping, symbol_id) {
return false;
}
d.init.as_ref().is_none_or(|init| query::is_side_effect_free(init))
})
}
Statement::FunctionDeclaration(_) => false,
Statement::EmptyStatement(_) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxc::codegen::Codegen;
use oxc::parser::Parser;
use oxc::semantic::SemanticBuilder;
use oxc::span::SourceType;
fn eliminate(source: &str) -> (String, usize) {
let allocator = Allocator::default();
let mut program = Parser::new(&allocator, source, SourceType::mjs())
.parse()
.program;
let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
let mut module = DeadCodeEliminator;
let result = module.transform(&allocator, &mut program, scoping).unwrap();
(Codegen::new().build(&program).code, result.modifications)
}
#[test]
fn test_keep_module_scope_unused_var() {
let (code, mods) = eliminate("var x = 1; var y = 2; console.log(y);");
assert_eq!(mods, 0, "module-scope vars must be preserved");
assert!(code.contains("var x"), "should keep module-scope x: {code}");
assert!(code.contains("var y"), "should keep module-scope y: {code}");
}
#[test]
fn test_keep_unused_function_declaration() {
let (code, mods) = eliminate("function unused() {} console.log(1);");
assert_eq!(mods, 0, "function declarations must never be removed");
assert!(code.contains("function unused"), "must preserve unused: {code}");
}
#[test]
fn test_keep_used_function() {
let (code, _) = eliminate("function used() {} used();");
assert!(code.contains("function used"), "should keep used function: {code}");
}
#[test]
fn test_keep_side_effectful_module_var() {
let (code, mods) = eliminate("var x = foo(); console.log(1);");
assert_eq!(mods, 0, "should not remove side-effectful init");
assert!(code.contains("foo()"), "got: {code}");
}
#[test]
fn test_remove_function_scope_unused_var() {
let (code, mods) = eliminate(
"function f() { var x = 1; var y = 2; return y; }"
);
assert!(mods > 0, "function-scope unused var should be removed");
assert!(!code.contains("var x"), "should remove function-scope x: {code}");
assert!(code.contains("var y"), "should keep used y: {code}");
}
#[test]
fn test_keep_function_scope_side_effect_var() {
let (code, mods) = eliminate(
"function f() { var x = foo(); return 1; }"
);
assert_eq!(mods, 0, "side-effectful init must be preserved");
assert!(code.contains("foo()"), "got: {code}");
}
#[test]
fn test_keep_nested_function_declaration() {
let (code, mods) = eliminate(
"function outer() { function helper() {} return 1; }"
);
assert_eq!(mods, 0, "nested function declarations must be preserved");
assert!(code.contains("function helper"), "got: {code}");
}
#[test]
fn test_remove_empty_statement() {
let (code, mods) = eliminate("function f() { ;;; return 1; }");
assert!(mods > 0, "empty statements always removable");
assert!(!code.contains(";;"), "got: {code}");
}
}