Skip to main content

js_deobfuscator/transform/
member.rs

1//! Member simplification: `obj["property"]` → `obj.property`.
2//!
3//! Converts computed member access with string literal keys to static
4//! member access when the key is a valid JavaScript identifier.
5
6use oxc::allocator::Allocator;
7use oxc::ast::ast::{Expression, Program};
8use oxc::semantic::Scoping;
9use oxc::span::SPAN;
10
11use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
12
13use crate::ast::extract;
14use crate::engine::error::Result;
15use crate::engine::module::{Module, TransformResult};
16
17/// Member simplification module.
18pub struct MemberSimplifier;
19
20impl Module for MemberSimplifier {
21    fn name(&self) -> &'static str {
22        "MemberSimplifier"
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 = MemberVisitor { modifications: 0 };
32        let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
33        Ok(TransformResult { modifications: visitor.modifications, scoping })
34    }
35}
36
37struct MemberVisitor {
38    modifications: usize,
39}
40
41impl<'a> Traverse<'a, ()> for MemberVisitor {
42    fn exit_expression(
43        &mut self,
44        expr: &mut Expression<'a>,
45        ctx: &mut TraverseCtx<'a, ()>,
46    ) {
47        // Check eligibility immutably first
48        let key_str = {
49            let Expression::ComputedMemberExpression(computed) = &*expr else { return };
50            let Some(key) = extract::string(&computed.expression) else { return };
51            if !is_valid_identifier(key) { return; }
52            key.to_string() // own the string before we mutate
53        };
54
55        // Allocate the key in the arena so it has lifetime 'a
56        let arena_key = ctx.ast.str(&key_str);
57
58        // Now mutate
59        let Expression::ComputedMemberExpression(computed) = expr else { return };
60        let object = std::mem::replace(
61            &mut computed.object,
62            ctx.ast.expression_null_literal(SPAN),
63        );
64
65        let property = ctx.ast.identifier_name(SPAN, arena_key);
66        let member = ctx.ast.alloc_static_member_expression(SPAN, object, property, false);
67        *expr = Expression::StaticMemberExpression(member);
68        self.modifications += 1;
69    }
70}
71
72/// Check if a string is a valid JavaScript identifier.
73fn is_valid_identifier(s: &str) -> bool {
74    if s.is_empty() { return false; }
75    let mut chars = s.chars();
76    let first = chars.next().unwrap();
77    if !first.is_ascii_alphabetic() && first != '_' && first != '$' {
78        return false;
79    }
80    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use oxc::codegen::Codegen;
87    use oxc::parser::Parser;
88    use oxc::semantic::SemanticBuilder;
89    use oxc::span::SourceType;
90
91    fn simplify(source: &str) -> (String, usize) {
92        let allocator = Allocator::default();
93        let mut program = Parser::new(&allocator, source, SourceType::mjs())
94            .parse()
95            .program;
96        let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
97
98        let mut module = MemberSimplifier;
99        let result = module.transform(&allocator, &mut program, scoping).unwrap();
100        (Codegen::new().build(&program).code, result.modifications)
101    }
102
103    #[test]
104    fn test_bracket_to_dot() {
105        let (code, mods) = simplify("obj[\"property\"];");
106        assert!(mods > 0);
107        assert!(code.contains("obj.property"), "got: {code}");
108        assert!(!code.contains("[\"property\"]"), "got: {code}");
109    }
110
111    #[test]
112    fn test_keep_numeric_key() {
113        let (code, mods) = simplify("arr[0];");
114        assert_eq!(mods, 0, "should not convert numeric key");
115        assert!(code.contains("[0]"), "got: {code}");
116    }
117
118    #[test]
119    fn test_keep_invalid_identifier() {
120        let (code, mods) = simplify("obj[\"has-dash\"];");
121        assert_eq!(mods, 0);
122        assert!(code.contains("[\"has-dash\"]"), "got: {code}");
123    }
124
125    #[test]
126    fn test_keep_keyword_like() {
127        let (code, mods) = simplify("obj[\"valid_key\"];");
128        assert!(mods > 0);
129        assert!(code.contains("obj.valid_key"), "got: {code}");
130    }
131}