Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support optional chaining in environment variable #1730

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 181 additions & 1 deletion crates/mako/src/visitors/env_replacer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use serde_json::Value;
use swc_core::common::{Mark, Span, DUMMY_SP};
use swc_core::ecma::ast::{
ArrayLit, Bool, ComputedPropName, Expr, ExprOrSpread, Ident, IdentName, KeyValueProp, Lit,
MemberExpr, MemberProp, ModuleItem, Null, Number, ObjectLit, Prop, PropOrSpread, Stmt, Str,
MemberExpr, MemberProp, ModuleItem, Null, Number, ObjectLit, OptChainBase, OptChainExpr, Prop,
PropOrSpread, Stmt, Str,
};
use swc_core::ecma::utils::{quote_ident, ExprExt};
use swc_core::ecma::visit::{VisitMut, VisitMutWith};
Expand All @@ -33,7 +34,48 @@ impl EnvReplacer {
fn get_define_env(&self, key: &str) -> Option<Expr> {
self.define.get(key).cloned()
}

fn extract_prop_name(&self, prop: &MemberProp) -> Option<String> {
match prop {
// handle obj.property
MemberProp::Ident(ident) => Some(ident.sym.to_string()),
// handle obj.['property'] or obj[1]
MemberProp::Computed(computed) => match computed.expr.as_ref() {
Expr::Lit(Lit::Str(str_lit)) => Some(str_lit.value.to_string()),
Expr::Lit(Lit::Num(num_lit)) => Some(num_lit.value.to_string()),
_ => None,
},
_ => None,
}
}

fn process_chain_expr(&self, expr: &Expr, parts: &mut Vec<String>) -> bool {
match expr {
Expr::Member(member_expr) => {
if let Some(prop_name) = self.extract_prop_name(&member_expr.prop) {
parts.push(prop_name);
return self.process_chain_expr(&member_expr.obj, parts);
}
}
Expr::OptChain(OptChainExpr {
base: box OptChainBase::Member(member_expr),
..
}) => {
if let Some(prop_name) = self.extract_prop_name(&member_expr.prop) {
parts.push(prop_name);
return self.process_chain_expr(&member_expr.obj, parts);
}
}
Expr::Ident(ident) if ident.ctxt.outer() == self.unresolved_mark => {
xusd320 marked this conversation as resolved.
Show resolved Hide resolved
parts.push(ident.sym.to_string());
return true;
}
_ => (),
}
false
}
}

impl VisitMut for EnvReplacer {
fn visit_mut_expr(&mut self, expr: &mut Expr) {
if let Expr::Ident(Ident { ctxt, .. }) = expr {
Expand All @@ -45,6 +87,25 @@ impl VisitMut for EnvReplacer {
}

match expr {
Expr::OptChain(OptChainExpr { base, .. }) => {
if let OptChainBase::Member(member) = base.as_ref() {
let mut parts = Vec::new();

if let Some(prop_name) = self.extract_prop_name(&member.prop) {
parts.push(prop_name);

if self.process_chain_expr(&member.obj, &mut parts) {
parts.reverse();
let full_path = parts.join(".");

if let Some(env) = self.get_define_env(&full_path) {
*expr = env
}
}
}
}
}

Expr::Member(MemberExpr { obj, prop, .. }) => {
let mut member_visit_path = match prop {
MemberProp::Ident(IdentName { sym, .. }) => sym.to_string(),
Expand Down Expand Up @@ -484,6 +545,125 @@ mod tests {
);
}

#[test]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

需要补充两类用例:

  1. optional chain 和 非 optional chain 混用的情况,入 A.B?.C;
  2. optional chain 作为 computed prop 的情况,入 A[x?.y]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已更新

fn test_optional_chaining_basic() {
assert_eq!(
run(
r#"log(A?.B)"#,
hashmap! {
"A.B".to_string() => json!(1)
}
),
"log(1);"
);
}

#[test]
fn test_optional_chaining_nested() {
assert_eq!(
run(
r#"log(A?.B?.C)"#,
hashmap! {
"A.B.C".to_string() => json!("\"test\"")
}
),
"log(\"test\");"
);
}

#[test]
fn test_optional_chaining_with_computed() {
assert_eq!(
run(
r#"log(A?.["B"]?.C)"#,
hashmap! {
"A.B.C".to_string() => json!(true)
}
),
"log(true);"
);
}

#[test]
fn test_optional_chaining_with_number_computed() {
assert_eq!(
run(
r#"log(A?.[1]?.B)"#,
hashmap! {
"A.1.B".to_string() => json!(42)
}
),
"log(42);"
);
}

#[test]
fn test_optional_chaining_mixed() {
assert_eq!(
run(
r#"log(A?.B?.["C"]?.D)"#,
hashmap! {
"A.B.C.D".to_string() => json!({"\"value\"": true})
}
),
"log({\"value\": true});"
);
}

#[test]
fn test_optional_chaining_not_defined() {
assert_eq!(
run(
r#"log(A?.B?.C)"#,
hashmap! {
"X.Y.Z".to_string() => json!(1)
}
),
"log(A?.B?.C);"
);
}

#[test]
fn test_mixed_optional_and_normal_chain() {
assert_eq!(
run(
r#"log(A.B?.C)"#,
hashmap! {
"A.B.C".to_string() => json!(42)
}
),
"log(42);"
);
}

#[test]
fn test_optional_chain_in_computed_prop() {
assert_eq!(
run(
r#"log(A[x?.y])"#,
hashmap! {
"x.y".to_string() => json!("\"prop\""),
"A.prop".to_string() => json!(123)
}
),
r#"log(A["prop"]);"#
);
}

#[test]
fn test_nested_optional_chain_in_computed_prop() {
assert_eq!(
run(
r#"log(A[x?.y?.z])"#,
hashmap! {
"x.y.z".to_string() => json!("\"test\""),
"A.test".to_string() => json!(true)
}
),
r#"log(A["test"]);"#
);
}

fn run(js_code: &str, envs: HashMap<String, Value>) -> String {
let mut test_utils = TestUtils::gen_js_ast(js_code);
let envs = build_env_map(envs, &test_utils.context).unwrap();
Expand Down
Loading