Skip to main content

js_deobfuscator/format/
split.rs

1//! Statement splitting: `var a = 1, b = 2` → `var a = 1; var b = 2;`
2
3use oxc::allocator::Allocator;
4use oxc::ast::ast::{Program, Statement};
5use oxc::semantic::Scoping;
6use oxc::span::SPAN;
7
8use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
9
10use crate::engine::error::Result;
11use crate::engine::module::{Module, TransformResult};
12
13/// Statement splitting module.
14pub struct StatementSplitter;
15
16impl Module for StatementSplitter {
17    fn name(&self) -> &'static str {
18        "StatementSplitter"
19    }
20
21    fn changes_symbols(&self) -> bool {
22        true
23    }
24
25    fn transform<'a>(
26        &mut self,
27        allocator: &'a Allocator,
28        program: &mut Program<'a>,
29        scoping: Scoping,
30    ) -> Result<TransformResult> {
31        let mut visitor = SplitVisitor { modifications: 0 };
32        let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
33        Ok(TransformResult { modifications: visitor.modifications, scoping })
34    }
35}
36
37struct SplitVisitor {
38    modifications: usize,
39}
40
41impl<'a> Traverse<'a, ()> for SplitVisitor {
42    fn exit_statements(
43        &mut self,
44        stmts: &mut oxc::allocator::Vec<'a, Statement<'a>>,
45        ctx: &mut TraverseCtx<'a, ()>,
46    ) {
47        let mut new_stmts = ctx.ast.vec();
48        let mut split_count = 0;
49
50        for stmt in stmts.drain(..) {
51            match &stmt {
52                Statement::VariableDeclaration(decl) if decl.declarations.len() > 1 => {
53                    // Split into individual declarations
54                    let Statement::VariableDeclaration(decl) = stmt else { unreachable!() };
55                    let kind = decl.kind;
56                    for declarator in decl.unbox().declarations.into_iter() {
57                        let mut decls = ctx.ast.vec();
58                        decls.push(declarator);
59                        new_stmts.push(Statement::VariableDeclaration(
60                            ctx.ast.alloc_variable_declaration(SPAN, kind, decls, false),
61                        ));
62                    }
63                    split_count += 1;
64                }
65                _ => new_stmts.push(stmt),
66            }
67        }
68
69        *stmts = new_stmts;
70        self.modifications += split_count;
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use oxc::codegen::Codegen;
78    use oxc::parser::Parser;
79    use oxc::semantic::SemanticBuilder;
80    use oxc::span::SourceType;
81
82    fn split(source: &str) -> (String, usize) {
83        let allocator = Allocator::default();
84        let mut program = Parser::new(&allocator, source, SourceType::mjs()).parse().program;
85        let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
86        let mut module = StatementSplitter;
87        let result = module.transform(&allocator, &mut program, scoping).unwrap();
88        (Codegen::new().build(&program).code, result.modifications)
89    }
90
91    #[test]
92    fn test_split_var() {
93        let (code, mods) = split("var a = 1, b = 2;");
94        assert!(mods > 0);
95        assert!(code.contains("var a = 1;"), "got: {code}");
96        assert!(code.contains("var b = 2;"), "got: {code}");
97    }
98
99    #[test]
100    fn test_split_const() {
101        let (code, mods) = split("const x = 1, y = 2;");
102        assert!(mods > 0);
103        assert!(code.contains("const x = 1;"), "got: {code}");
104        assert!(code.contains("const y = 2;"), "got: {code}");
105    }
106
107    #[test]
108    fn test_single_not_split() {
109        let (_, mods) = split("var a = 1;");
110        assert_eq!(mods, 0);
111    }
112}