Skip to main content

js_deobfuscator/fold/
mod.rs

1//! Layer 3: Static expression folding.
2//!
3//! One file per AST Expression/Statement variant. Pure Rust evaluation —
4//! no scope analysis, no Node.js. Depends on value/ + ast/.
5//!
6//! The visitor dispatches by Expression variant to sub-modules.
7//! Each sub-module's `try_fold` returns `Option<usize>`:
8//!   - `None` — can't fold this expression
9//!   - `Some(n)` — folded, n modifications made (usually 1)
10
11pub mod binary;
12pub mod unary;
13pub mod logical;
14pub mod conditional;
15pub mod call;
16pub mod sequence;
17pub mod statement;
18pub mod template;
19
20// ============================================================================
21// Imports
22// ============================================================================
23
24use oxc::allocator::Allocator;
25use oxc::ast::ast::{Expression, Statement};
26use oxc::semantic::Scoping;
27
28use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
29
30use crate::engine::error::Result;
31use crate::engine::module::{Module, TransformResult};
32
33// ============================================================================
34// Module implementation
35// ============================================================================
36
37/// Layer 3 module: folds all statically-known expressions in pure Rust.
38pub struct StaticFolder;
39
40impl Module for StaticFolder {
41    fn name(&self) -> &'static str {
42        "StaticFolder"
43    }
44
45    fn transform<'a>(
46        &mut self,
47        allocator: &'a Allocator,
48        program: &mut oxc::ast::ast::Program<'a>,
49        scoping: Scoping,
50    ) -> Result<TransformResult> {
51        let mut visitor = FoldVisitor::default();
52        let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
53        Ok(TransformResult {
54            modifications: visitor.modifications,
55            scoping,
56        })
57    }
58}
59
60// ============================================================================
61// Visitor
62// ============================================================================
63
64#[derive(Default)]
65struct FoldVisitor {
66    modifications: usize,
67}
68
69impl<'a> Traverse<'a, ()> for FoldVisitor {
70    fn exit_expression(
71        &mut self,
72        expr: &mut Expression<'a>,
73        ctx: &mut TraverseCtx<'a, ()>,
74    ) {
75        // Skip leaf literals — already fully reduced.
76        if is_leaf_literal(expr) {
77            return;
78        }
79
80        // Dispatch to appropriate folder based on expression type.
81        // Each folder returns Some(n) if it made n modifications.
82        // We dispatch by type first to avoid unnecessary work.
83        let folded = match expr {
84            Expression::BinaryExpression(_) => binary::try_fold(expr, ctx),
85            Expression::UnaryExpression(_) => unary::try_fold(expr, ctx),
86            Expression::LogicalExpression(_) => logical::try_fold(expr, ctx),
87            Expression::ConditionalExpression(_) => conditional::try_fold(expr, ctx),
88            Expression::CallExpression(_) => call::try_fold(expr, ctx),
89            Expression::SequenceExpression(_) => sequence::try_fold(expr, ctx),
90            Expression::TemplateLiteral(_) => template::try_fold(expr, ctx),
91            _ => None,
92        };
93
94        if let Some(n) = folded {
95            self.modifications += n;
96        }
97    }
98
99    fn exit_statement(
100        &mut self,
101        stmt: &mut Statement<'a>,
102        ctx: &mut TraverseCtx<'a, ()>,
103    ) {
104        self.modifications += statement::try_fold(stmt, ctx);
105    }
106
107    fn exit_statements(
108        &mut self,
109        stmts: &mut oxc::allocator::Vec<'a, Statement<'a>>,
110        ctx: &mut TraverseCtx<'a, ()>,
111    ) {
112        self.modifications += statement::clean(stmts, ctx);
113    }
114}
115
116/// Check if an expression is a leaf literal that needs no folding.
117fn is_leaf_literal(expr: &Expression) -> bool {
118    match expr {
119        Expression::NumericLiteral(_)
120        | Expression::StringLiteral(_)
121        | Expression::BooleanLiteral(_)
122        | Expression::NullLiteral(_) => true,
123        Expression::Identifier(id) => {
124            matches!(id.name.as_str(), "undefined" | "Infinity" | "NaN")
125        }
126        _ => false,
127    }
128}
129
130// ============================================================================
131// Tests
132// ============================================================================
133
134#[cfg(test)]
135pub(crate) mod test_utils {
136    use oxc::allocator::Allocator;
137    use oxc::codegen::Codegen;
138    use oxc::parser::Parser;
139    use oxc::semantic::SemanticBuilder;
140    use oxc::span::SourceType;
141
142    /// Parse → fold → codegen. Returns the output source.
143    pub fn fold(source: &str) -> String {
144        let allocator = Allocator::default();
145        let mut program = Parser::new(&allocator, source, SourceType::mjs())
146            .parse()
147            .program;
148        let scoping = SemanticBuilder::new()
149            .build(&program)
150            .semantic
151            .into_scoping();
152
153        let mut folder = super::FoldVisitor::default();
154        oxc_traverse::traverse_mut(&mut folder, &allocator, &mut program, scoping, ());
155
156        Codegen::new().build(&program).code
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::test_utils::fold;
163
164    #[test]
165    fn test_leaf_literals_unchanged() {
166        assert_eq!(fold("42;\n").trim(), "42;");
167        assert_eq!(fold("\"hello\";\n").trim(), "\"hello\";");
168        assert_eq!(fold("true;\n").trim(), "true;");
169    }
170}