use rspack_core::ImportAttributes;
use rspack_error::{Label, Result};
use rspack_regex::RspackRegex;
use rspack_util::SpanExt;
use swc_experimental_ecma_ast::{Expr, GetSpan, Lit, ObjectLit, PropName};
use crate::visitors::static_string_from_expr;
pub fn get_value_by_obj_prop<'r, 'ast>(
obj: &'r ObjectLit<'ast>,
field: &str,
) -> Option<&'r Expr<'ast>> {
obj.props.iter().rev().find_map(|p| {
let prop = p.as_prop()?;
let kv = prop.as_key_value()?;
let matched = kv.key.as_ident().is_some_and(|key| key.sym == field)
|| kv
.key
.as_str()
.is_some_and(|key| key.value.as_str() == Some(field));
matched.then_some(&kv.value)
})
}
pub fn get_attributes(obj: &ObjectLit<'_>) -> ImportAttributes {
obj
.props
.iter()
.filter_map(|p| {
p.as_prop().and_then(|p| p.as_key_value()).and_then(|kv| {
kv.key
.as_ident()
.map(|k| k.sym.as_str())
.or_else(|| kv.key.as_str().and_then(|k| k.value.as_str()))
.map(|s| s.to_string())
.zip(kv.value.as_lit().and_then(|lit| match lit {
Lit::Str(s) => Some(s.value.to_string_lossy().to_string()),
_ => None,
}))
})
})
.collect()
}
pub fn get_property<'r, 'ast>(obj: &'r ObjectLit<'ast>, key: &str) -> Option<&'r Expr<'ast>> {
get_value_by_obj_prop(obj, key)
}
pub fn get_from_object<'r, 'ast>(
object: &'r ObjectLit<'ast>,
path: &[&str],
) -> Option<&'r Expr<'ast>> {
let (key, remaining) = path.split_first()?;
get(get_property(object, key)?, remaining)
}
pub fn get_value_from_object<'r, 'ast, T: FromAstExpr<'ast>>(
object: &'r ObjectLit<'ast>,
path: &[&str],
) -> Result<Option<T>> {
let Some(expr) = get_from_object(object, path) else {
return Ok(None);
};
extract_value(expr)
}
pub fn get<'r, 'ast>(expr: &'r Expr<'ast>, path: &[&str]) -> Option<&'r Expr<'ast>> {
let mut current = expr;
for key in path {
current = get_property(current.as_object()?, key)?;
}
Some(current)
}
pub fn get_value<'r, 'ast, T: FromAstExpr<'ast>>(
expr: &'r Expr<'ast>,
path: &[&str],
) -> Result<Option<T>> {
let Some(expr) = get(expr, path) else {
return Ok(None);
};
extract_value(expr)
}
fn extract_value<'ast, T: FromAstExpr<'ast>>(expr: &Expr<'ast>) -> Result<Option<T>> {
T::from_ast_expr(expr).map_err(|mut error| {
if error.labels.is_none() {
let span = expr.span();
error.labels = Some(vec![Label {
name: None,
offset: span.real_lo() as usize,
len: span.real_hi().saturating_sub(span.real_lo()) as usize,
}]);
}
error
})
}
pub trait FromAstExpr<'a>: Sized {
fn from_ast_expr(expr: &Expr<'a>) -> Result<Option<Self>>;
}
impl FromAstExpr<'_> for bool {
fn from_ast_expr(expr: &Expr<'_>) -> Result<Option<Self>> {
Ok(match expr.as_lit() {
Some(Lit::Bool(bool)) => Some(bool.value),
_ => None,
})
}
}
impl FromAstExpr<'_> for String {
fn from_ast_expr(expr: &Expr<'_>) -> Result<Option<Self>> {
Ok(static_string_from_expr(expr))
}
}
impl FromAstExpr<'_> for f64 {
fn from_ast_expr(expr: &Expr<'_>) -> Result<Option<Self>> {
Ok(match expr.as_lit() {
Some(Lit::Num(num)) => Some(num.value),
_ => None,
})
}
}
impl<'a, T: FromAstExpr<'a>> FromAstExpr<'a> for Option<T> {
fn from_ast_expr(expr: &Expr<'a>) -> Result<Option<Self>> {
T::from_ast_expr(expr).map(|value| value.map(Some))
}
}
impl FromAstExpr<'_> for RspackRegex {
fn from_ast_expr(expr: &Expr<'_>) -> Result<Option<Self>> {
let Some(Lit::Regex(regex)) = expr.as_lit() else {
return Ok(None);
};
RspackRegex::with_flags(regex.exp.as_ref(), regex.flags.as_ref()).map(Some)
}
}
impl<'a, T: FromAstExpr<'a>> FromAstExpr<'a> for Vec<(String, T)> {
fn from_ast_expr(expr: &Expr<'a>) -> Result<Option<Self>> {
let Some(obj) = expr.as_object() else {
return Ok(None);
};
obj
.props
.iter()
.map(|prop| -> Result<Option<(String, T)>> {
let Some(kv) = prop.as_prop().and_then(|prop| prop.as_key_value()) else {
return Ok(None);
};
let Some(key) = static_prop_name(&kv.key) else {
return Ok(None);
};
let Some(value) = T::from_ast_expr(&kv.value)? else {
return Ok(None);
};
Ok(Some((key, value)))
})
.collect::<Result<Option<Vec<_>>>>()
}
}
fn static_prop_name(key: &PropName) -> Option<String> {
match key {
PropName::Ident(ident) => Some(ident.sym.to_string()),
PropName::Str(str) => Some(str.value.to_string_lossy().into_owned()),
PropName::Num(num) => Some(num.value.to_string()),
PropName::Computed(computed) => {
if let Some(key) = static_string_from_expr(&computed.expr) {
return Some(key);
}
match computed.expr.as_lit()? {
Lit::Num(num) => Some(num.value.to_string()),
Lit::Bool(bool) => Some(bool.value.to_string()),
Lit::Null(_) => Some("null".to_string()),
_ => None,
}
}
PropName::BigInt(_) => None,
}
}
#[cfg(test)]
mod tests {
use rspack_macros::AstObject;
use swc_experimental_allocator::Allocator;
use swc_experimental_ecma_ast::EsVersion;
use swc_experimental_ecma_parser::{
EsSyntax, Lexer, Parser, StringSource, Syntax, unstable::Capturing,
};
use super::*;
fn parse_expr<'a>(allocator: &'a Allocator, source: &'a str) -> Expr<'a> {
let lexer = Lexer::new(
allocator,
Syntax::Es(EsSyntax::default()),
EsVersion::EsNext,
StringSource::new(source),
None,
);
let lexer = Capturing::new(lexer);
let mut parser = Parser::new_from(allocator, lexer);
parser
.parse_expr()
.expect("failed to parse test expression")
}
fn default_true() -> bool {
true
}
#[derive(Debug, Default, PartialEq, AstObject)]
#[ast_object(rename_all = "camelCase")]
struct TestOptions {
eager: bool,
case_sensitive: Option<bool>,
import: Option<String>,
#[ast_object(skip, default = "default_true")]
skipped: bool,
}
#[test]
fn extracts_camel_case_fields_and_defaults() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ eager: true, caseSensitive: false }");
let options = TestOptions::from_ast_object(expr.as_object().unwrap()).unwrap();
assert_eq!(
options,
TestOptions {
eager: true,
case_sensitive: Some(false),
import: None,
skipped: true,
}
);
let expr = parse_expr(&allocator, "{}");
assert_eq!(
TestOptions::from_ast_object(expr.as_object().unwrap()).unwrap(),
TestOptions {
skipped: true,
..Default::default()
}
);
}
#[test]
fn unrecognized_values_fall_back_to_default() {
let allocator = Allocator::new();
let expr = parse_expr(
&allocator,
"{ eager: \"yes\", import: 1, caseSensitive: null }",
);
assert_eq!(
TestOptions::from_ast_object(expr.as_object().unwrap()).unwrap(),
TestOptions {
skipped: true,
..Default::default()
}
);
let expr = parse_expr(&allocator, "true");
assert_eq!(TestOptions::from_ast_expr(&expr).unwrap(), None);
}
#[test]
fn resolves_template_literals() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ import: `default` }");
assert_eq!(
TestOptions::from_ast_object(expr.as_object().unwrap())
.unwrap()
.import,
Some("default".to_string())
);
}
#[derive(Debug, Default, PartialEq, AstObject)]
struct TestOuter {
inner: Option<TestOptions>,
}
#[test]
fn extracts_nested_options_objects() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ inner: { eager: true } }");
let options = TestOuter::from_ast_object(expr.as_object().unwrap()).unwrap();
assert_eq!(
options.inner,
Some(TestOptions {
eager: true,
skipped: true,
..Default::default()
})
);
}
#[test]
fn extracts_records_in_source_order() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ b: '1', a: `2`, [\"c\"]: '3' }");
let record = Vec::<(String, String)>::from_ast_expr(&expr)
.unwrap()
.unwrap();
assert_eq!(
record,
vec![
("b".to_string(), "1".to_string()),
("a".to_string(), "2".to_string()),
("c".to_string(), "3".to_string()),
]
);
}
#[test]
fn record_extraction_is_all_or_nothing() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ a: '1', [Symbol()]: '2' }");
assert_eq!(Vec::<(String, String)>::from_ast_expr(&expr).unwrap(), None);
}
#[derive(Debug, Default, AstObject)]
#[ast_object(rename_all = "camelCase")]
struct TestRegexOptions {
reg_exp: Option<RspackRegex>,
recursive: Option<bool>,
}
#[test]
fn extracts_regex_literals() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ regExp: /abc/gi }");
let regex = TestRegexOptions::from_ast_object(expr.as_object().unwrap())
.unwrap()
.reg_exp
.expect("expected a regex");
assert_eq!(regex.source(), "abc");
assert_eq!(regex.flags(), "gi");
let expr = parse_expr(&allocator, "{ regExp: \"nope\" }");
assert!(
TestRegexOptions::from_ast_object(expr.as_object().unwrap())
.unwrap()
.reg_exp
.is_none()
);
}
#[test]
fn preserves_regex_conversion_errors() {
let allocator = Allocator::new();
let expr = parse_expr(
&allocator,
"{ regExp: /(?<name>a)(?<name>b)/, recursive: false }",
);
let error = TestRegexOptions::from_ast_object(expr.as_object().unwrap())
.expect_err("regex conversion failures should not be treated as an absent option");
assert!(error.to_string().contains("/(?<name>a)(?<name>b)/"));
}
#[test]
fn preserves_valid_fields_when_collecting_conversion_diagnostics() {
let allocator = Allocator::new();
let expr = parse_expr(
&allocator,
"{ regExp: /(?<name>a)(?<name>b)/, recursive: false }",
);
let (options, diagnostics) =
TestRegexOptions::from_ast_object_with_diagnostics(expr.as_object().unwrap());
assert!(options.reg_exp.is_none());
assert_eq!(options.recursive, Some(false));
assert_eq!(diagnostics.len(), 1);
assert!(
diagnostics[0]
.to_string()
.contains("/(?<name>a)(?<name>b)/")
);
}
}
#[cfg(test)]
mod get_tests {
use swc_experimental_allocator::Allocator;
use swc_experimental_ecma_ast::EsVersion;
use swc_experimental_ecma_parser::{
EsSyntax, Lexer, Parser, StringSource, Syntax, unstable::Capturing,
};
use super::*;
fn parse_expr<'a>(allocator: &'a Allocator, source: &'a str) -> Expr<'a> {
let lexer = Lexer::new(
allocator,
Syntax::Es(EsSyntax::default()),
EsVersion::EsNext,
StringSource::new(source),
None,
);
let lexer = Capturing::new(lexer);
let mut parser = Parser::new_from(allocator, lexer);
parser
.parse_expr()
.expect("failed to parse test expression")
}
#[test]
fn gets_nested_values_by_key_path() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ a: { b: { c: 42 } } }");
assert_eq!(
get_value_from_object::<f64>(expr.as_object().unwrap(), &["a"]).unwrap(),
None
);
assert_eq!(
get_value::<f64>(&expr, &["a", "b", "c"]).unwrap(),
Some(42.0)
);
assert!(get(&expr, &["a", "b"]).is_some_and(|expr| expr.as_object().is_some()));
assert!(get(&expr, &[]).is_some());
}
#[test]
fn gets_typed_values_from_object_paths() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ enabled: true, label: 'test' }");
let object = expr.as_object().unwrap();
let dynamic_key = "enabled".to_string();
assert_eq!(
get_value_from_object::<bool>(object, &[dynamic_key.as_str()]).unwrap(),
Some(true)
);
assert_eq!(
get_value_from_object::<String>(object, &["label"]).unwrap(),
Some("test".to_string())
);
assert_eq!(
get_value_from_object::<bool>(object, &["missing"]).unwrap(),
None
);
assert_eq!(get_value_from_object::<bool>(object, &[]).unwrap(), None);
}
#[test]
fn later_duplicate_properties_take_precedence() {
let allocator = Allocator::new();
let expr = parse_expr(
&allocator,
"{ enabled: false, enabled: true, nested: { value: 1, value: 2 } }",
);
let object = expr.as_object().unwrap();
assert_eq!(
get_value_from_object::<bool>(object, &["enabled"]).unwrap(),
Some(true)
);
assert_eq!(
get_value::<f64>(&expr, &["nested", "value"]).unwrap(),
Some(2.0)
);
}
#[test]
fn get_returns_none_for_missing_or_non_object_segments() {
let allocator = Allocator::new();
let expr = parse_expr(&allocator, "{ a: { b: 1 } }");
assert_eq!(get_value::<f64>(&expr, &["a", "x"]).unwrap(), None);
assert_eq!(get_value::<f64>(&expr, &["x"]).unwrap(), None);
assert_eq!(get_value::<f64>(&expr, &["a", "b", "c"]).unwrap(), None);
assert_eq!(get_value::<String>(&expr, &["a", "b"]).unwrap(), None);
}
}