Skip to content

Commit

Permalink
Support for optional chaining (#102)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Igor Unanua <[email protected]>
  • Loading branch information
uurien and iunanua authored Nov 26, 2024
1 parent 1ee2193 commit 936ff4e
Show file tree
Hide file tree
Showing 5 changed files with 649 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/transform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ pub(crate) mod binary_add_transform;
pub(crate) mod call_expr_transform;
pub(crate) mod function_prototype_transform;
pub(crate) mod operand_handler;
pub(crate) mod opt_chain_transform;
pub(crate) mod template_transform;
pub(crate) mod transform_status;
307 changes: 307 additions & 0 deletions src/transform/opt_chain_transform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
/**
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License.
* This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
**/
use crate::visitor::{
csi_methods::CsiMethods,
ident_provider::{IdentKind, IdentProvider},
};
use swc::atoms::JsWord;
use swc_common::{util::take::Take, SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_visit::{Visit, VisitMut, VisitMutWith};

use super::transform_status::TransformResult;

struct OptChainVisitor<'a> {
pub assignment: Option<Expr>,
pub assignments: Vec<Expr>,
pub new_ident: Option<Ident>,
pub ident_provider: &'a mut dyn IdentProvider,
pub csi_methods: &'a CsiMethods,
pub found: bool,
}

impl OptChainVisitor<'_> {
pub fn default<'a>(
ident_provider: &'a mut dyn IdentProvider,
csi_methods: &'a CsiMethods,
) -> OptChainVisitor<'a> {
OptChainVisitor {
assignment: None,
assignments: Vec::new(),
new_ident: None,
ident_provider,
csi_methods,
found: false,
}
}

fn get_call_from_base_call(&mut self, call_expr: &OptCall, optional: bool) -> Option<CallExpr> {
if optional {
/*
* if expression is like obj.b?(arg1, arg2), to prevent double calling to the b getter
* we should extract obj.b to a variable, and call to the new variable maintaining
* the expected this property:
*
* (_1 = obj, _2 = _1.b, _2 == null ? undefined : _2.call(_1, arg1, arg2))
*/
if let Expr::Member(mut member_expr) = *call_expr.callee.clone() {
let mut member_obj_arguments = Vec::new();
let span = DUMMY_SP;
let member_obj_ident_opt = self.ident_provider.get_ident_used_in_assignation(
&member_expr.obj.clone(),
&mut self.assignments,
&mut member_obj_arguments,
&span,
IdentKind::Expr,
);

if let Some(member_obj_ident) = member_obj_ident_opt {
let new_member_expr = MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(member_obj_ident.clone())),
prop: member_expr.prop.clone(),
};

member_expr.map_with_mut(|_| new_member_expr);

let mut member_expr_args = Vec::new();
let member_expr_ident_opt = self.ident_provider.get_ident_used_in_assignation(
&Expr::Member(member_expr.clone()),
&mut self.assignments,
&mut member_expr_args,
&span,
IdentKind::Expr,
);

if let Some(member_expr_ident) = member_expr_ident_opt {
self.new_ident = Some(member_expr_ident.clone());
let call_ident = Ident {
span: DUMMY_SP,
sym: "call".into(),
optional: false,
ctxt: SyntaxContext::empty(),
};
let callee = MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(member_expr_ident)),
prop: MemberProp::Ident(IdentName::from(call_ident)),
};

let mut args = Vec::new();
args.push(ExprOrSpread {
expr: Box::new(Expr::Ident(member_obj_ident)),
spread: None,
});
for arg in call_expr.clone().args {
args.push(arg)
}
let call_expr_new = CallExpr {
span: DUMMY_SP,
callee: Callee::Expr(Box::new(Expr::Member(callee))),
args,
ctxt: call_expr.ctxt,
type_args: call_expr.type_args.clone(),
};

return Some(call_expr_new);
}
}
} else {
let mut arguments = Vec::new();
let span = DUMMY_SP;
let new_ident_opt = self.ident_provider.get_ident_used_in_assignation(
&call_expr.callee.clone(),
&mut self.assignments,
&mut arguments,
&span,
IdentKind::Expr,
);

if let Some(new_ident) = new_ident_opt {
if let Some(fist_arg) = self.assignments.first() {
self.assignment = Some(fist_arg.clone());
self.new_ident = Some(new_ident.clone());
let call_expr_new = CallExpr {
span: DUMMY_SP,
callee: Callee::Expr(Box::new(Expr::Ident(new_ident))),
args: call_expr.args.clone(),
ctxt: call_expr.ctxt,
type_args: call_expr.type_args.clone(),
};

return Some(call_expr_new);
}
}
}
} else {
return Some(CallExpr {
span: DUMMY_SP,
callee: call_expr.callee.clone().into(),
args: call_expr.args.clone(),
ctxt: call_expr.ctxt,
type_args: call_expr.type_args.clone(),
});
}
None
}

fn get_member_from_base_member(
&mut self,
member_expr: &MemberExpr,
optional: bool,
) -> Option<MemberExpr> {
if optional {
let mut arguments = Vec::new();
let span = DUMMY_SP;
let new_ident_opt = self.ident_provider.get_ident_used_in_assignation(
&member_expr.obj.clone(),
&mut self.assignments,
&mut arguments,
&span,
IdentKind::Expr,
);

if let Some(new_ident) = new_ident_opt {
self.new_ident = Some(new_ident.clone());

let member_expr_new = MemberExpr {
span: DUMMY_SP,
obj: Box::new(Expr::Ident(new_ident)),
prop: member_expr.prop.clone(),
};

return Some(member_expr_new);
}
} else {
return Some(member_expr.clone());
}

None
}
}

impl Visit for OptChainVisitor<'_> {}

impl VisitMut for OptChainVisitor<'_> {
/*
* Iterates the OptChain finding a method to replace.
* If the expression contains method to rewrite, all the OptCall or OptMembers are converted
* normal Member or Call expressions, and the optional part of the expression is extracted
* to a new variable.
*/
fn visit_mut_expr(&mut self, expr: &mut Expr) {
match expr {
Expr::OptChain(opt_chain_expr) => {
if self.found {
let base = &*opt_chain_expr.base;
let optional = opt_chain_expr.optional;
match base {
OptChainBase::Call(call_expr) => {
let call_opt = self.get_call_from_base_call(call_expr, optional);
if let Some(call) = call_opt {
expr.map_with_mut(|_| Expr::Call(call))
}
}

OptChainBase::Member(member_expr) => {
let call_opt = self.get_member_from_base_member(member_expr, optional);
if let Some(call) = call_opt {
expr.map_with_mut(|_| Expr::Member(call))
}
}
};

if optional {
// Do not call to visit_mut_children_with
return;
}
} else if !opt_chain_expr.optional {
if let OptChainBase::Call(opt_call) = &*opt_chain_expr.base {
if let Expr::OptChain(opt_chain_expr) = *opt_call.clone().callee {
if let OptChainBase::Member(member_expr) = &*opt_chain_expr.base {
if let MemberProp::Ident(method_ident) = &member_expr.prop {
let prop_name = &method_ident.sym;

if self.csi_methods.get(prop_name).is_some() {
self.found = true;

expr.visit_mut_with(self);
return;
}
}
}
}
}
}

expr.visit_mut_children_with(self);
}

_ => {
expr.visit_mut_children_with(self);
}
};
}
}

pub struct OptChainTransform {}

impl OptChainTransform {
pub fn to_dd_cond_expr(
opt_chain_expr: &mut Expr,
csi_methods: &CsiMethods,
ident_provider: &mut dyn IdentProvider,
) -> TransformResult<Expr> {
let visitor = &mut OptChainVisitor::default(ident_provider, csi_methods);
opt_chain_expr.visit_mut_with(visitor);

// If the optional chaining contains a method to rewrite, we should modify the expression
// by a paren expressions assigning the extracted part to a variable and checking if it
// is null to return undefined
// (extracted_var = optional_part, extracted_var == null ? undefined : extracted_var.the_rest_of_the_expression)
if visitor.assignments.is_empty() || visitor.new_ident.is_none() {
return TransformResult::not_modified();
}

let new_ident = visitor.new_ident.as_mut().unwrap();

let test = Expr::Bin(BinExpr {
span: DUMMY_SP,
op: BinaryOp::EqEq,
left: Box::new(Expr::Ident(new_ident.clone())),
right: Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))),
});

let cons = Ident {
span: DUMMY_SP,
sym: JsWord::from("undefined"),
optional: false,
ctxt: SyntaxContext::empty(),
};

let cond = CondExpr {
span: DUMMY_SP,
test: Box::new(test),
cons: Box::new(Expr::Ident(cons)),
alt: Box::new(opt_chain_expr.clone()),
};

visitor.assignments.push(Expr::Cond(cond));

let expr = Expr::Paren(ParenExpr {
span: DUMMY_SP,
expr: Box::new(Expr::Seq(SeqExpr {
span: DUMMY_SP,
exprs: visitor
.assignments
.iter_mut()
.map(|assignation| Box::new(assignation.take()))
.collect::<Vec<Box<Expr>>>(),
})),
});

TransformResult::modified(expr)
}
}
7 changes: 5 additions & 2 deletions src/visitor/block_transform_visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
},
};
use std::collections::HashSet;
use swc_common::SyntaxContext;
use swc_common::{SyntaxContext, DUMMY_SP};
use swc_ecma_ast::{Stmt::Decl as DeclEnumOption, *};
use swc_ecma_visit::{Visit, VisitMut, VisitMutWith};

Expand Down Expand Up @@ -71,6 +71,7 @@ impl VisitMut for BlockTransformVisitor<'_> {
&self.config.local_var_prefix,
) {
return self.cancel_visit("Variable name duplicated");
// insert_variable_declaration(&ident_provider.idents, expr);
} else {
insert_variable_declaration(&ident_provider.idents, expr);
}
Expand All @@ -81,7 +82,9 @@ impl VisitMut for BlockTransformVisitor<'_> {

fn variables_contains_possible_duplicate(variable_decl: &HashSet<Ident>, prefix: &String) -> bool {
let prefix = get_dd_local_variable_prefix(prefix);
variable_decl.iter().any(|var| var.sym.starts_with(&prefix))
variable_decl
.iter()
.any(|var| var.span != DUMMY_SP && var.sym.starts_with(&prefix))
}

fn insert_variable_declaration(ident_expressions: &[Ident], expr: &mut BlockStmt) {
Expand Down
23 changes: 22 additions & 1 deletion src/visitor/operation_transform_visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
assign_add_transform::AssignAddTransform,
binary_add_transform::BinaryAddTransform,
call_expr_transform::CallExprTransform,
opt_chain_transform::OptChainTransform,
template_transform::TemplateTransform,
transform_status::{Status, TransformStatus},
},
Expand Down Expand Up @@ -127,7 +128,6 @@ impl VisitMut for OperationTransformVisitor<'_> {
Expr::Call(call) => {
let opv_with_child_ctx = &mut *self.with_child_ctx();
call.visit_mut_children_with(opv_with_child_ctx);

if call.callee.is_expr() {
let result = CallExprTransform::to_dd_call_expr(
call,
Expand All @@ -141,6 +141,27 @@ impl VisitMut for OperationTransformVisitor<'_> {
}
}

Expr::OptChain(_) => {
let opv_with_child_ctx = &mut *self.with_child_ctx();
let transform_result = OptChainTransform::to_dd_cond_expr(
expr,
opv_with_child_ctx.csi_methods,
opv_with_child_ctx.ident_provider,
);
if transform_result.is_modified() {
expr.map_with_mut(|e| transform_result.expr.unwrap_or(e));
opv_with_child_ctx.update_status(transform_result.status, transform_result.tag);
}

expr.visit_mut_children_with(opv_with_child_ctx);
}

Expr::Unary(unary_expr) => {
if UnaryOp::Delete != unary_expr.op {
expr.visit_mut_children_with(self);
}
}

_ => {
expr.visit_mut_children_with(self);
}
Expand Down
Loading

0 comments on commit 936ff4e

Please sign in to comment.