swc_ecma_compat_es2022/
optional_chaining_impl.rs

1use std::mem;
2
3use swc_common::{util::take::Take, Mark, SyntaxContext, DUMMY_SP};
4use swc_ecma_ast::*;
5use swc_ecma_utils::{alias_ident_for, prepend_stmt, quote_ident, ExprFactory, StmtLike};
6use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
7
8/// Not a public API and may break any time. Don't use it directly.
9pub fn optional_chaining_impl(c: Config, unresolved_mark: Mark) -> OptionalChaining {
10    OptionalChaining {
11        c,
12        unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
13        ..Default::default()
14    }
15}
16
17#[derive(Default)]
18pub struct OptionalChaining {
19    vars: Vec<VarDeclarator>,
20    unresolved_ctxt: SyntaxContext,
21    c: Config,
22}
23
24impl OptionalChaining {
25    pub fn take_vars(&mut self) -> Vec<VarDeclarator> {
26        mem::take(&mut self.vars)
27    }
28}
29
30/// Not a public API and may break any time. Don't use it directly.
31#[derive(Debug, Clone, Copy, Default)]
32pub struct Config {
33    pub no_document_all: bool,
34    pub pure_getter: bool,
35}
36
37impl VisitMut for OptionalChaining {
38    noop_visit_mut_type!(fail);
39
40    fn visit_mut_block_stmt_or_expr(&mut self, expr: &mut BlockStmtOrExpr) {
41        if let BlockStmtOrExpr::Expr(e) = expr {
42            let mut stmt = BlockStmt {
43                span: DUMMY_SP,
44                stmts: vec![Stmt::Return(ReturnStmt {
45                    span: DUMMY_SP,
46                    arg: Some(e.take()),
47                })],
48                ..Default::default()
49            };
50            stmt.visit_mut_with(self);
51
52            // If there are optional chains in this expression, then the visitor will have
53            // injected an VarDecl statement and we need to transform into a
54            // block. If not, then we can keep the expression.
55            match &mut stmt.stmts[..] {
56                [Stmt::Return(ReturnStmt { arg: Some(e), .. })] => {
57                    *expr = BlockStmtOrExpr::Expr(e.take())
58                }
59                _ => *expr = BlockStmtOrExpr::BlockStmt(stmt),
60            }
61        } else {
62            expr.visit_mut_children_with(self);
63        }
64    }
65
66    fn visit_mut_expr(&mut self, e: &mut Expr) {
67        match e {
68            // foo?.bar -> foo == null ? void 0 : foo.bar
69            Expr::OptChain(v) => {
70                let data = self.gather(v.take(), Vec::new());
71                *e = self.construct(data, false);
72            }
73
74            Expr::Unary(UnaryExpr {
75                arg,
76                op: op!("delete"),
77                ..
78            }) => {
79                match &mut **arg {
80                    // delete foo?.bar -> foo == null ? true : delete foo.bar
81                    Expr::OptChain(v) => {
82                        let data = self.gather(v.take(), Vec::new());
83                        *e = self.construct(data, true);
84                    }
85                    _ => e.visit_mut_children_with(self),
86                }
87            }
88
89            e => e.visit_mut_children_with(self),
90        }
91    }
92
93    fn visit_mut_pat(&mut self, n: &mut Pat) {
94        // The default initializer of an assignment pattern must not leak the memo
95        // variable into the enclosing scope.
96        // function(a, b = a?.b) {} -> function(a, b = (() => var _a; …)()) {}
97        let Pat::Assign(a) = n else {
98            n.visit_mut_children_with(self);
99            return;
100        };
101
102        let uninit = self.vars.take();
103        a.right.visit_mut_with(self);
104
105        // If we found an optional chain, we need to transform into an arrow IIFE to
106        // capture the memo variable.
107        if !self.vars.is_empty() {
108            let stmts = vec![
109                Stmt::Decl(Decl::Var(Box::new(VarDecl {
110                    kind: VarDeclKind::Var,
111                    decls: mem::take(&mut self.vars),
112                    ..Default::default()
113                }))),
114                Stmt::Return(ReturnStmt {
115                    span: DUMMY_SP,
116                    arg: Some(a.right.take()),
117                }),
118            ];
119            a.right = CallExpr {
120                span: DUMMY_SP,
121                callee: ArrowExpr {
122                    span: DUMMY_SP,
123                    params: Vec::new(),
124                    body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
125                        span: DUMMY_SP,
126                        stmts,
127                        ..Default::default()
128                    })),
129                    is_async: false,
130                    is_generator: false,
131                    ..Default::default()
132                }
133                .as_callee(),
134                args: Vec::new(),
135                ..Default::default()
136            }
137            .into();
138        }
139
140        self.vars = uninit;
141        a.left.visit_mut_with(self);
142    }
143
144    fn visit_mut_module_items(&mut self, n: &mut Vec<ModuleItem>) {
145        self.visit_mut_stmt_like(n);
146    }
147
148    fn visit_mut_stmts(&mut self, n: &mut Vec<Stmt>) {
149        self.visit_mut_stmt_like(n);
150    }
151}
152
153#[derive(Debug, Clone)]
154enum Memo {
155    Cache(Ident),
156    Raw(Box<Expr>),
157}
158
159impl Memo {
160    fn into_expr(self) -> Expr {
161        match self {
162            Memo::Cache(i) => i.into(),
163            Memo::Raw(e) => *e,
164        }
165    }
166}
167
168#[derive(Debug)]
169enum Gathering {
170    Call(CallExpr),
171    Member(MemberExpr),
172    OptCall(CallExpr, Memo),
173    OptMember(MemberExpr, Memo),
174}
175
176impl OptionalChaining {
177    /// Transforms the left-nested structure into a flat vec. The obj/callee
178    /// of every node in the chain will be Invalid, to be replaced with a
179    /// constructed node in the construct step.
180    /// The top member/call will be first, and the deepest obj/callee will be
181    /// last.
182    fn gather(
183        &mut self,
184        v: OptChainExpr,
185        mut chain: Vec<Gathering>,
186    ) -> (Expr, usize, Vec<Gathering>) {
187        let mut current = v;
188        let mut count = 0;
189        loop {
190            let OptChainExpr {
191                optional, mut base, ..
192            } = current;
193
194            if optional {
195                count += 1;
196            }
197
198            let next;
199            match &mut *base {
200                OptChainBase::Member(m) => {
201                    next = m.obj.take();
202                    m.prop.visit_mut_with(self);
203                    chain.push(if optional {
204                        Gathering::OptMember(m.take(), self.memoize(&next, false))
205                    } else {
206                        Gathering::Member(m.take())
207                    });
208                }
209
210                OptChainBase::Call(c) => {
211                    next = c.callee.take();
212                    c.args.visit_mut_with(self);
213                    // I don't know why c is an OptCall instead of a CallExpr.
214                    chain.push(if optional {
215                        Gathering::OptCall(c.take().into(), self.memoize(&next, true))
216                    } else {
217                        Gathering::Call(c.take().into())
218                    });
219                }
220            }
221
222            match *next {
223                Expr::OptChain(next) => {
224                    current = next;
225                }
226                mut base => {
227                    base.visit_mut_children_with(self);
228                    return (base, count, chain);
229                }
230            }
231        }
232    }
233
234    /// Constructs a rightward nested conditional expression out of our
235    /// flattened chain.
236    fn construct(&mut self, data: (Expr, usize, Vec<Gathering>), is_delete: bool) -> Expr {
237        let (mut current, count, chain) = data;
238
239        // Stores partially constructed CondExprs for us to assemble later on.
240        let mut committed_cond = Vec::with_capacity(count);
241
242        // Stores the memo used to construct an optional chain, so that it can be used
243        // as the this context of an optional call:
244        // foo?.bar?.() ->
245        // (_foo = foo) == null
246        //   ? void 0
247        //   : (_foo_bar = _foo.bar) == null
248        //     ?  void 0 : _foo_bar.call(_foo)
249        let mut ctx = None;
250
251        // In the first pass, we construct a "current" node and several committed
252        // CondExprs. The conditionals will have an invalid alt, waiting for the
253        // second pass to properly construct them.
254        // We reverse iterate so that we can construct a rightward conditional
255        // `(_a = a) == null ? void 0 : (_a_b = _a.b) == null ? void 0 : _a_b.c`
256        // instead of a leftward one
257        // `(_a_b = (_a = a) == null ? void 0 : _a.b) == null ? void 0 : _a_b.c`
258        for v in chain.into_iter().rev() {
259            current = match v {
260                Gathering::Call(mut c) => {
261                    c.callee = current.as_callee();
262                    ctx = None;
263                    c.into()
264                }
265                Gathering::Member(mut m) => {
266                    m.obj = Box::new(current);
267                    ctx = None;
268                    m.into()
269                }
270                Gathering::OptCall(mut c, memo) => {
271                    let mut call = false;
272
273                    // foo.bar?.() -> (_foo_bar == null) ? void 0 : _foo_bar.call(foo)
274                    match &mut current {
275                        Expr::Member(m) => {
276                            call = true;
277                            let this = ctx.unwrap_or_else(|| {
278                                let this = self.memoize(&m.obj, true);
279
280                                match &this {
281                                    Memo::Cache(i) => {
282                                        m.obj = AssignExpr {
283                                            span: DUMMY_SP,
284                                            op: op!("="),
285                                            left: i.clone().into(),
286                                            right: m.obj.take(),
287                                        }
288                                        .into();
289                                        this
290                                    }
291                                    Memo::Raw(_) => this,
292                                }
293                            });
294                            c.args.insert(0, this.into_expr().as_arg());
295                        }
296                        Expr::SuperProp(s) => {
297                            call = true;
298                            c.args.insert(0, ThisExpr { span: s.obj.span }.as_arg());
299                        }
300                        _ => {}
301                    }
302
303                    committed_cond.push(CondExpr {
304                        span: DUMMY_SP,
305                        test: init_and_eq_null_or_undefined(&memo, current, self.c.no_document_all),
306                        cons: if is_delete {
307                            true.into()
308                        } else {
309                            Expr::undefined(DUMMY_SP)
310                        },
311                        alt: Take::dummy(),
312                    });
313                    c.callee = if call {
314                        memo.into_expr()
315                            .make_member(quote_ident!("call"))
316                            .as_callee()
317                    } else {
318                        memo.into_expr().as_callee()
319                    };
320                    ctx = None;
321                    c.into()
322                }
323                Gathering::OptMember(mut m, memo) => {
324                    committed_cond.push(CondExpr {
325                        span: DUMMY_SP,
326                        test: init_and_eq_null_or_undefined(&memo, current, self.c.no_document_all),
327                        cons: if is_delete {
328                            true.into()
329                        } else {
330                            Expr::undefined(DUMMY_SP)
331                        },
332                        alt: Take::dummy(),
333                    });
334                    ctx = Some(memo.clone());
335                    m.obj = memo.into_expr().into();
336                    m.into()
337                }
338            };
339        }
340
341        // At this point, `current` is the right-most expression `_a_b.c` in `a?.b?.c`
342        if is_delete {
343            current = UnaryExpr {
344                span: DUMMY_SP,
345                op: op!("delete"),
346                arg: Box::new(current),
347            }
348            .into();
349        }
350
351        // We now need to reverse iterate the conditionals to construct out tree.
352        for mut cond in committed_cond.into_iter().rev() {
353            cond.alt = Box::new(current);
354            current = cond.into()
355        }
356        current
357    }
358
359    fn should_memo(&self, expr: &Expr, is_call: bool) -> bool {
360        fn is_simple_member(e: &Expr) -> bool {
361            match e {
362                Expr::This(..) => true,
363                Expr::Ident(_) => true,
364                Expr::SuperProp(s) if !s.prop.is_computed() => true,
365                Expr::Member(m) if !m.prop.is_computed() => is_simple_member(&m.obj),
366                _ => false,
367            }
368        }
369
370        match expr {
371            Expr::Ident(i) if i.ctxt != self.unresolved_ctxt => false,
372            _ => {
373                if is_call && self.c.pure_getter {
374                    !is_simple_member(expr)
375                } else {
376                    true
377                }
378            }
379        }
380    }
381
382    fn memoize(&mut self, expr: &Expr, is_call: bool) -> Memo {
383        if self.should_memo(expr, is_call) {
384            let memo = alias_ident_for(expr, "_this");
385            self.vars.push(VarDeclarator {
386                span: DUMMY_SP,
387                name: memo.clone().into(),
388                init: None,
389                definite: false,
390            });
391            Memo::Cache(memo)
392        } else {
393            Memo::Raw(Box::new(expr.to_owned()))
394        }
395    }
396
397    fn visit_mut_stmt_like<T>(&mut self, stmts: &mut Vec<T>)
398    where
399        T: Send + Sync + StmtLike + VisitMutWith<Self>,
400        Vec<T>: VisitMutWith<Self>,
401    {
402        let uninit = self.vars.take();
403        for stmt in stmts.iter_mut() {
404            stmt.visit_mut_with(self);
405        }
406
407        if !self.vars.is_empty() {
408            prepend_stmt(
409                stmts,
410                T::from(
411                    VarDecl {
412                        span: DUMMY_SP,
413                        declare: false,
414                        kind: VarDeclKind::Var,
415                        decls: mem::take(&mut self.vars),
416                        ..Default::default()
417                    }
418                    .into(),
419                ),
420            );
421        }
422
423        self.vars = uninit;
424    }
425}
426
427fn init_and_eq_null_or_undefined(i: &Memo, init: Expr, no_document_all: bool) -> Box<Expr> {
428    let lhs = match i {
429        Memo::Cache(i) => AssignExpr {
430            span: DUMMY_SP,
431            op: op!("="),
432            left: i.clone().into(),
433            right: Box::new(init),
434        }
435        .into(),
436        Memo::Raw(e) => e.to_owned(),
437    };
438
439    if no_document_all {
440        return BinExpr {
441            span: DUMMY_SP,
442            left: lhs,
443            op: op!("=="),
444            right: Box::new(Lit::Null(Null { span: DUMMY_SP }).into()),
445        }
446        .into();
447    }
448
449    let null_cmp = BinExpr {
450        span: DUMMY_SP,
451        left: lhs,
452        op: op!("==="),
453        right: Box::new(Lit::Null(Null { span: DUMMY_SP }).into()),
454    }
455    .into();
456
457    let left_expr = match i {
458        Memo::Cache(i) => Box::new(i.clone().into()),
459        Memo::Raw(e) => e.to_owned(),
460    };
461
462    let void_cmp = BinExpr {
463        span: DUMMY_SP,
464        left: left_expr,
465        op: op!("==="),
466        right: Expr::undefined(DUMMY_SP),
467    }
468    .into();
469
470    BinExpr {
471        span: DUMMY_SP,
472        left: null_cmp,
473        op: op!("||"),
474        right: void_cmp,
475    }
476    .into()
477}