Skip to main content

js_deobfuscator/ast/
create.rs

1//! AST expression constructors. Allocates in the arena via AstBuilder.
2
3use oxc::ast::AstBuilder;
4use oxc::ast::ast::{Expression, NumberBase, UnaryOperator};
5use oxc::span::SPAN;
6
7use crate::value::JsValue;
8
9// ============================================================================
10// Primitive constructors
11// ============================================================================
12
13/// Create a number literal expression.
14///
15/// Handles negative numbers by wrapping in unary negation.
16/// Handles negative zero correctly.
17pub fn make_number<'a>(value: f64, ast: &AstBuilder<'a>) -> Expression<'a> {
18    if value == 0.0 && value.is_sign_negative() {
19        // -0: must be unary negation of 0
20        let inner = ast.expression_numeric_literal(SPAN, 0.0, None, NumberBase::Decimal);
21        return ast.expression_unary(SPAN, UnaryOperator::UnaryNegation, inner);
22    }
23    if value.is_sign_negative() && !value.is_nan() {
24        let inner = ast.expression_numeric_literal(SPAN, value.abs(), None, NumberBase::Decimal);
25        return ast.expression_unary(SPAN, UnaryOperator::UnaryNegation, inner);
26    }
27    ast.expression_numeric_literal(SPAN, value, None, NumberBase::Decimal)
28}
29
30/// Create a string literal expression.
31#[inline]
32pub fn make_string<'a>(value: &str, ast: &AstBuilder<'a>) -> Expression<'a> {
33    ast.expression_string_literal(SPAN, ast.str(value), None)
34}
35
36/// Create a boolean literal expression.
37#[inline]
38pub fn make_boolean<'a>(value: bool, ast: &AstBuilder<'a>) -> Expression<'a> {
39    ast.expression_boolean_literal(SPAN, value)
40}
41
42/// Create a null literal expression.
43#[inline]
44pub fn make_null<'a>(ast: &AstBuilder<'a>) -> Expression<'a> {
45    ast.expression_null_literal(SPAN)
46}
47
48/// Create an `undefined` expression (identifier reference).
49#[inline]
50pub fn make_undefined<'a>(ast: &AstBuilder<'a>) -> Expression<'a> {
51    ast.expression_identifier(SPAN, "undefined")
52}
53
54// ============================================================================
55// JsValue → Expression
56// ============================================================================
57
58/// Convert a JsValue to an AST Expression. Allocates in the arena.
59pub fn from_js_value<'a>(value: &JsValue, ast: &AstBuilder<'a>) -> Expression<'a> {
60    match value {
61        JsValue::Number(n) => make_number(*n, ast),
62        JsValue::String(s) => make_string(s, ast),
63        JsValue::Boolean(b) => make_boolean(*b, ast),
64        JsValue::Null => make_null(ast),
65        JsValue::Undefined => make_undefined(ast),
66    }
67}
68
69// ============================================================================
70// Tests
71// ============================================================================
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::ast::extract;
77    use oxc::allocator::Allocator;
78
79    #[test]
80    fn test_make_number_positive() {
81        let alloc = Allocator::default();
82        let ast = AstBuilder::new(&alloc);
83        let expr = make_number(42.0, &ast);
84        assert_eq!(extract::number(&expr), Some(42.0));
85    }
86
87    #[test]
88    fn test_make_number_negative() {
89        let alloc = Allocator::default();
90        let ast = AstBuilder::new(&alloc);
91        let expr = make_number(-5.0, &ast);
92        assert_eq!(extract::number(&expr), Some(-5.0));
93    }
94
95    #[test]
96    fn test_make_number_negative_zero() {
97        let alloc = Allocator::default();
98        let ast = AstBuilder::new(&alloc);
99        let expr = make_number(-0.0, &ast);
100        let val = extract::number(&expr).unwrap();
101        assert!(val.is_sign_negative());
102        assert_eq!(val, 0.0);
103    }
104
105    #[test]
106    fn test_make_string() {
107        let alloc = Allocator::default();
108        let ast = AstBuilder::new(&alloc);
109        let expr = make_string("hello", &ast);
110        assert_eq!(extract::string(&expr), Some("hello"));
111    }
112
113    #[test]
114    fn test_make_boolean() {
115        let alloc = Allocator::default();
116        let ast = AstBuilder::new(&alloc);
117        assert_eq!(extract::boolean(&make_boolean(true, &ast)), Some(true));
118        assert_eq!(extract::boolean(&make_boolean(false, &ast)), Some(false));
119    }
120
121    #[test]
122    fn test_roundtrip_js_value() {
123        let alloc = Allocator::default();
124        let ast = AstBuilder::new(&alloc);
125
126        let cases = [
127            JsValue::Number(42.0),
128            JsValue::Number(-7.0),
129            JsValue::String("test".into()),
130            JsValue::Boolean(true),
131            JsValue::Null,
132        ];
133
134        for val in &cases {
135            let expr = from_js_value(val, &ast);
136            let extracted = extract::js_value(&expr);
137            assert_eq!(extracted.as_ref(), Some(val), "roundtrip failed for {val:?}");
138        }
139    }
140}