Skip to main content

stylex_ast/ast/
convertors.rs

1use anyhow::anyhow;
2use stylex_macros::stylex_panic;
3use stylex_utils::swc::get_default_expr_ctx;
4use swc_core::{
5  atoms::{Atom, Wtf8Atom},
6  ecma::{
7    ast::{
8      BigInt, Bool, CallExpr, Expr, Ident, KeyValueProp, Lit, Prop, PropName, Str, Tpl, TplElement,
9    },
10    parser::Context,
11    utils::{ExprExt, quote_ident, quote_str},
12  },
13};
14
15use stylex_constants::constants::messages::INVALID_UTF8;
16
17use super::factories::{
18  create_big_int_lit, create_boolean_lit, create_ident, create_null_lit, create_number_lit,
19  create_string_lit,
20};
21
22/// Helper function to convert a Lit to a number
23/// # Arguments
24/// * `lit_num` - The literal to convert
25/// # Returns
26/// * `Result<f64, anyhow::Error>` - The number value of the literal
27///
28/// # Example
29/// ```javascript
30/// Input: Lit::Num(1.0)
31/// Output: 1.0
32/// ```
33pub fn convert_lit_to_number(lit_num: &Lit) -> Result<f64, anyhow::Error> {
34  let result = match &lit_num {
35    Lit::Bool(Bool { value, .. }) => {
36      if value == &true {
37        1.0
38      } else {
39        0.0
40      }
41    },
42    Lit::Num(num) => num.value,
43    Lit::Str(strng) => {
44      let Result::Ok(num) = convert_atom_to_string(&strng.value).parse::<f64>() else {
45        return Err(anyhow!(
46          "Value in not a number: {}",
47          convert_atom_to_string(&strng.value)
48        ));
49      };
50
51      num
52    },
53    _ => {
54      return Err(anyhow!(
55        "Value in not a number: {:?}",
56        Expr::from(lit_num.clone()).get_type(get_default_expr_ctx())
57      ));
58    },
59  };
60
61  Result::Ok(result)
62}
63
64/// Helper function to convert a Tpl to a string literal
65/// # Arguments
66/// * `tpl` - The template literal to convert
67/// # Returns
68/// * `Option<Lit>` - The string literal if the template is simple (no interpolations)
69/// * `None` - If the template is not simple (has interpolations)
70/// # Example
71/// ```javascript
72/// Input: Tpl { exprs: [], quasis: [TplElement { cooked: Some("hello"), raw: "hello" }] }
73/// Output: Some(Lit::Str("hello"))
74/// ```
75pub fn convert_tpl_to_string_lit(tpl: &Tpl) -> Option<Lit> {
76  // Check if it's a simple template (no expressions)
77  if tpl.exprs.is_empty() && tpl.quasis.len() == 1 {
78    let quasi = &tpl.quasis[0];
79
80    // Get the string value (prefer cooked if available, otherwise use raw)
81    let value = match quasi.cooked.as_ref() {
82      Some(cooked) => match cooked.as_str() {
83        Some(s) => s,
84        None => stylex_panic!("Failed to extract a string value from the expression."),
85      },
86      None => stylex_panic!("Failed to extract cooked value from template literal element."),
87    };
88
89    return Some(create_string_lit(value));
90  }
91
92  None
93}
94
95/// Converts a simple template literal expression to a regular string literal expression.
96/// This is a convenience wrapper around `convert_tpl_to_string_lit` that works with `Expr::Tpl`.
97///
98/// # Arguments
99/// * `expr` - The expression to check and potentially convert
100///
101/// # Returns
102/// * `Expr` - The original expression if it's not a simple template literal
103/// * A string literal expression if the template is simple (no interpolations)
104#[inline]
105pub fn convert_simple_tpl_to_str_expr(expr: Expr) -> Expr {
106  match expr {
107    Expr::Tpl(ref tpl) => {
108      if let Some(str_lit) = convert_tpl_to_string_lit(tpl) {
109        return Expr::Lit(str_lit);
110      }
111      expr
112    },
113    _ => expr,
114  }
115}
116
117/// Converts a string `.concat()` call expression to a template literal expression.
118///
119/// # Arguments
120/// * `expr` - The expression to check and potentially convert
121///
122/// # Returns
123/// * The original expression if it's not a concat call
124/// * A template literal expression if the expression is a valid concat call
125///
126/// # Example
127/// ```javascript
128/// Input: "hello".concat(world, "!")
129/// Output: `hello${world}!`
130/// ```
131#[inline]
132pub fn convert_concat_to_tpl_expr(expr: Expr) -> Expr {
133  match expr {
134    Expr::Call(ref call_expr) => {
135      if let Some(tpl_expr) = concat_call_to_template_literal(call_expr) {
136        return tpl_expr;
137      }
138      expr
139    },
140    _ => expr,
141  }
142}
143
144/// Helper function that converts a CallExpr representing `.concat()` to a template literal.
145///
146/// # Arguments
147/// * `call_expr` - The call expression to convert
148///
149/// # Returns
150/// * `Some(Expr)` - Template literal expression if conversion is successful
151/// * `None` - If the call expression is not a valid concat call
152fn concat_call_to_template_literal(call_expr: &CallExpr) -> Option<Expr> {
153  use swc_core::common::DUMMY_SP;
154
155  // Check if this is a member expression with a "concat" property
156  let member_expr = call_expr.callee.as_expr()?.as_member()?;
157  let prop_ident = member_expr.prop.as_ident()?;
158
159  if prop_ident.sym.as_ref() != "concat" {
160    return None;
161  }
162
163  // Get the base string from the object being called
164  let base_string = extract_str_lit_ref(member_expr.obj.as_lit()?).map(|s| s.to_string())?;
165
166  let mut exprs = Vec::new();
167  let mut quasis = Vec::new();
168
169  // Add the base string as the first quasi
170  quasis.push(TplElement {
171    span: DUMMY_SP,
172    tail: false,
173    cooked: Some(base_string.clone().into()),
174    raw: base_string.into(),
175  });
176
177  // Process each argument
178  for (i, arg) in call_expr.args.iter().enumerate() {
179    // Skip spread arguments
180    if arg.spread.is_some() {
181      continue;
182    }
183
184    exprs.push(arg.expr.clone());
185
186    let is_last = i == call_expr.args.len() - 1;
187    quasis.push(TplElement {
188      span: DUMMY_SP,
189      tail: is_last,
190      cooked: Some("".into()),
191      raw: "".into(),
192    });
193  }
194
195  let template_literal = Tpl {
196    span: DUMMY_SP,
197    exprs,
198    quasis,
199  };
200
201  Some(Expr::Tpl(template_literal))
202}
203
204#[inline]
205pub fn create_number_expr(value: f64) -> Expr {
206  Expr::from(create_number_lit(value))
207}
208
209#[inline]
210pub fn create_big_int_expr(value: BigInt) -> Expr {
211  Expr::from(create_big_int_lit(value))
212}
213
214#[inline]
215pub fn create_string_expr(value: &str) -> Expr {
216  Expr::Lit(create_string_lit(value))
217}
218
219#[inline]
220pub fn create_bool_expr(value: bool) -> Expr {
221  Expr::Lit(create_boolean_lit(value))
222}
223
224#[inline]
225pub fn create_ident_expr(value: &str) -> Expr {
226  Expr::Ident(create_ident(value))
227}
228
229#[inline]
230pub fn create_null_expr() -> Expr {
231  Expr::Lit(create_null_lit())
232}
233
234fn should_wrap_prop_name_key_with_quotes(key: &str) -> bool {
235  Ident::verify_symbol(key).is_err() && {
236    let ctx = Context::default();
237
238    !ctx.is_reserved_word(&key.into())
239  }
240}
241#[inline]
242pub fn convert_string_to_prop_name(value: &str) -> Option<PropName> {
243  if should_wrap_prop_name_key_with_quotes(value) {
244    Some(PropName::Str(quote_str!(value)))
245  } else {
246    Some(PropName::Ident(quote_ident!(value)))
247  }
248}
249
250pub fn expand_shorthand_prop(prop: &mut Box<Prop>) {
251  if let Some(ident) = prop.as_shorthand() {
252    **prop = Prop::from(KeyValueProp {
253      key: match convert_string_to_prop_name(ident.sym.as_ref()) {
254        Some(k) => k,
255        None => stylex_panic!("Failed to convert string to a valid property name."),
256      },
257      value: Box::new(Expr::Ident(ident.clone())),
258    });
259  }
260}
261
262/// Helper function to convert Wtf8Atom to String
263/// Note: `.as_str()` returns an `Option<&str>` that only fails when the string contains invalid UTF-8
264#[inline]
265pub fn convert_atom_to_string(atom: &Wtf8Atom) -> String {
266  match atom.as_str() {
267    Some(s) => s.to_string(),
268    None => stylex_panic!("{}", INVALID_UTF8),
269  }
270}
271
272pub fn convert_wtf8_to_atom(atom: &Wtf8Atom) -> Atom {
273  match atom.as_atom() {
274    Some(a) => a.clone(),
275    None => stylex_panic!("{}", INVALID_UTF8),
276  }
277}
278
279/// Helper function to safely get string from Lit::Str
280#[inline]
281pub fn convert_str_lit_to_string(str_lit: &Str) -> String {
282  match str_lit.value.as_str() {
283    Some(s) => s.to_string(),
284    None => stylex_panic!("{}", INVALID_UTF8),
285  }
286}
287
288/// Helper function to safely get Atom from Lit::Str
289pub fn convert_str_lit_to_atom(str_lit: &Str) -> Atom {
290  match str_lit.value.as_atom() {
291    Some(a) => a.clone(),
292    None => stylex_panic!("{}", INVALID_UTF8),
293  }
294}
295
296/// Helper function to safely get cooked string from TplElement
297#[inline]
298pub fn extract_tpl_cooked_value(elem: &TplElement) -> String {
299  match elem.cooked.as_ref() {
300    Some(cooked) => match cooked.as_str() {
301      Some(s) => s.to_string(),
302      None => stylex_panic!("{}", INVALID_UTF8),
303    },
304    None => stylex_panic!(
305      "Template literal element has no cooked value (contains an invalid escape sequence)."
306    ),
307  }
308}
309
310/// Helper function to convert Atom to &str (reference, not owned String)
311/// Useful when you need a reference instead of an owned String
312#[inline]
313pub fn convert_atom_to_str_ref(atom: &swc_core::atoms::Wtf8Atom) -> &str {
314  match atom.as_str() {
315    Some(s) => s,
316    None => stylex_panic!("Failed to convert SWC Atom to string (invalid WTF-8 encoding)."),
317  }
318}
319
320#[inline]
321pub fn convert_lit_to_string(value: &Lit) -> Option<String> {
322  match value {
323    Lit::Str(strng) => Some(convert_str_lit_to_string(strng)),
324    Lit::Num(num) => Some(format!("{}", num.value)),
325    Lit::BigInt(big_int) => Some(format!("{}", big_int.value)),
326    _ => None,
327  }
328}
329
330/// Helper function to safely extract string from Lit::Str using Option pattern
331#[inline]
332pub fn extract_str_lit_ref(lit: &Lit) -> Option<&str> {
333  match lit {
334    Lit::Str(s) => Some(convert_atom_to_str_ref(&s.value)),
335    _ => None,
336  }
337}