Skip to main content

turbopack_ecmascript/analyzer/graph/
eval_context.rs

1use std::sync::Arc;
2
3use anyhow::{Ok, Result};
4use rustc_hash::FxHashSet;
5use swc_core::{
6    base::try_with_handler,
7    common::{GLOBALS, Mark, SourceMap, Spanned, SyntaxContext, comments::Comments, sync::Lrc},
8    ecma::{ast::*, atoms::atom},
9};
10use turbo_rcstr::{RcStr, rcstr};
11
12use crate::{
13    SpecifiedModuleType,
14    analyzer::{
15        Bump, BumpVec, ConstantNumber, ConstantValue, ImportMap, JsValue, ObjectPart,
16        WellKnownObjectKind, is_unresolved, is_unresolved_id,
17    },
18    references::constant_value::parse_single_expr_lit,
19    utils::unparen,
20};
21
22/// A context used for assembling the evaluation graph.
23#[derive(Debug)]
24pub struct EvalContext {
25    /// Should be the same [`Mark`] used by [`swc_core::ecma::transforms::base::resolver`].
26    pub(crate) unresolved_mark: Mark,
27    /// Should be the same [`Mark`] used by [`swc_core::ecma::transforms::base::resolver`].
28    pub(crate) top_level_mark: Mark,
29    pub(crate) imports: ImportMap,
30    pub(crate) force_free_values: Arc<FxHashSet<Id>>,
31}
32
33impl EvalContext {
34    /// Produce a new [`EvalContext`] from a [`Program`].
35    ///
36    /// If you wish to support `webpackIgnore` or `turbopackIgnore` comments, you must pass those
37    /// in, since the AST does not include comments by default.
38    ///
39    /// You should use the same `unresolved_mark` and `top_level_mark` [Mark] values for this
40    /// context that you passed to [`swc_core::ecma::transforms::base::resolver`].
41    pub fn new(
42        module: Option<&Program>,
43        unresolved_mark: Mark,
44        top_level_mark: Mark,
45        force_free_values: Arc<FxHashSet<Id>>,
46        comments: Option<&dyn Comments>,
47    ) -> Self {
48        Self {
49            unresolved_mark,
50            top_level_mark,
51            imports: module.map_or(ImportMap::default(), |m| {
52                ImportMap::analyze(unresolved_mark, m, comments)
53            }),
54            force_free_values,
55        }
56    }
57
58    pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
59        self.imports.is_esm(specified_type)
60    }
61
62    pub fn is_cjs(&self, specified_type: SpecifiedModuleType) -> bool {
63        self.imports.is_cjs(specified_type)
64    }
65
66    pub(super) fn eval_prop_name<'a>(&self, arena: &'a Bump, prop: &PropName) -> JsValue<'a> {
67        match prop {
68            PropName::Ident(ident) => ident.sym.clone().into(),
69            PropName::Str(str) => str.value.clone().to_atom_lossy().into_owned().into(),
70            PropName::Num(num) => num.value.into(),
71            PropName::Computed(ComputedPropName { expr, .. }) => self.eval(arena, expr),
72            PropName::BigInt(bigint) => (*bigint.value.clone()).into(),
73        }
74    }
75
76    pub(super) fn eval_member_prop<'a>(
77        &self,
78        arena: &'a Bump,
79        prop: &MemberProp,
80    ) -> Option<JsValue<'a>> {
81        match prop {
82            MemberProp::Ident(ident) => Some(ident.sym.clone().into()),
83            MemberProp::Computed(ComputedPropName { expr, .. }) => Some(self.eval(arena, expr)),
84            MemberProp::PrivateName(_) => None,
85        }
86    }
87
88    fn eval_tpl<'a>(&self, arena: &'a Bump, e: &Tpl, raw: bool) -> JsValue<'a> {
89        debug_assert!(e.quasis.len() == e.exprs.len() + 1);
90
91        let mut values = vec![];
92
93        for idx in 0..(e.quasis.len() + e.exprs.len()) {
94            if idx.is_multiple_of(2) {
95                let idx = idx / 2;
96                let e = &e.quasis[idx];
97                if raw {
98                    // Ignore empty strings quasis, happens frequently with e.g. after the
99                    // placeholder in `something${v}`.
100                    if !e.raw.is_empty() {
101                        values.push(JsValue::from(e.raw.clone()));
102                    }
103                } else {
104                    match &e.cooked {
105                        Some(v) => {
106                            if !v.is_empty() {
107                                values.push(JsValue::from(v.clone().to_atom_lossy().into_owned()));
108                            }
109                        }
110                        // This is actually unreachable
111                        None => return JsValue::unknown_empty(true, rcstr!("")),
112                    }
113                }
114            } else {
115                let idx = idx / 2;
116                let e = &e.exprs[idx];
117
118                values.push(self.eval(arena, e));
119            }
120        }
121
122        match values.len() {
123            0 => JsValue::Constant(ConstantValue::Str(rcstr!("").into())),
124            1 => values.into_iter().next().unwrap(),
125            _ => JsValue::concat(BumpVec::from_iter_in(arena, values)),
126        }
127    }
128
129    pub fn eval_id<'a>(&self, arena: &'a Bump, id: Id) -> JsValue<'a> {
130        if let Some(imported) = self.imports.get_import(arena, &id) {
131            return imported;
132        }
133        if is_unresolved_id(&id, self.unresolved_mark) || self.force_free_values.contains(&id) {
134            // These are special globals that we shouldn't consider to be free variables and we can
135            // model their values mostly useful for truthy/falsy checks.
136            match id.0.as_str() {
137                "undefined" => JsValue::Constant(ConstantValue::Undefined),
138                "NaN" => JsValue::Constant(ConstantValue::Num(f64::NAN.into())),
139                "Infinity" => JsValue::Constant(ConstantValue::Num(f64::INFINITY.into())),
140                _ => JsValue::FreeVar(id.0.clone()),
141            }
142        } else {
143            JsValue::Variable(id)
144        }
145    }
146
147    pub fn eval<'a>(&self, arena: &'a Bump, e: &Expr) -> JsValue<'a> {
148        let value = self.eval_inner(arena, e);
149        // A `turbopackIgnore` comment on this expression opts it out of static
150        // analysis. Downgrade it to an unknown so the opt-out lives on the value
151        // itself and bubbles up to any consumer (e.g. an enclosing
152        // `fs.readFileSync(...)`, or through a variable). A dynamic (unknown) path
153        // isn't rooted at the project directory, so tracing skips it instead of
154        // pulling in the whole project. The attribute is keyed to the annotated
155        // call's callee position, so only that exact expression matches — nested
156        // subexpressions are unaffected.
157        if self.imports.get_attributes(e.span()).ignore {
158            JsValue::unknown(value, true, rcstr!("turbopackIgnore"))
159        } else {
160            value
161        }
162    }
163
164    fn eval_inner<'a>(&self, arena: &'a Bump, e: &Expr) -> JsValue<'a> {
165        debug_assert!(
166            GLOBALS.is_set(),
167            "Eval requires globals from its parsed result"
168        );
169        match e {
170            Expr::Paren(e) => self.eval(arena, &e.expr),
171            Expr::Lit(e) => JsValue::Constant(e.clone().into()),
172            Expr::Ident(i) => self.eval_id(arena, i.to_id()),
173            Expr::Unary(UnaryExpr {
174                op: op!("void"),
175                // Only treat literals as constant undefined, allowing arbitrary values inside here
176                // would mean that they can have sideeffects, and `JsValue::Constant` can't model
177                // that.
178                arg: Expr::Lit(_),
179                ..
180            }) => JsValue::Constant(ConstantValue::Undefined),
181
182            Expr::Unary(UnaryExpr {
183                op: op!(unary, "-"),
184                arg: Expr::Lit(Lit::Num(n)),
185                ..
186            }) => JsValue::Constant(ConstantValue::Num(ConstantNumber(-n.value))),
187
188            Expr::Unary(UnaryExpr {
189                op: op!("!"), arg, ..
190            }) => {
191                let arg = self.eval(arena, arg);
192
193                JsValue::logical_not(arena, arg)
194            }
195
196            Expr::Unary(UnaryExpr {
197                op: op!("typeof"),
198                arg,
199                ..
200            }) => {
201                let arg = self.eval(arena, arg);
202
203                JsValue::type_of(arena, arg)
204            }
205
206            Expr::Bin(BinExpr {
207                op: op!(bin, "+"),
208                left,
209                right,
210                ..
211            }) => {
212                let l = self.eval(arena, left);
213                let r = self.eval(arena, right);
214
215                match (l, r) {
216                    (JsValue::Add(c, mut l), r) => {
217                        let total = c + r.total_nodes();
218                        l.push(arena, r);
219                        JsValue::Add(total, l)
220                    }
221                    (l, r) => JsValue::add(BumpVec::from_iter_in(arena, [l, r])),
222                }
223            }
224
225            Expr::Bin(BinExpr {
226                op: op!("&&"),
227                left,
228                right,
229                ..
230            }) => JsValue::logical_and(BumpVec::from_iter_in(
231                arena,
232                [self.eval(arena, left), self.eval(arena, right)],
233            )),
234
235            Expr::Bin(BinExpr {
236                op: op!("||"),
237                left,
238                right,
239                ..
240            }) => JsValue::logical_or(BumpVec::from_iter_in(
241                arena,
242                [self.eval(arena, left), self.eval(arena, right)],
243            )),
244
245            Expr::Bin(BinExpr {
246                op: op!("??"),
247                left,
248                right,
249                ..
250            }) => JsValue::nullish_coalescing(BumpVec::from_iter_in(
251                arena,
252                [self.eval(arena, left), self.eval(arena, right)],
253            )),
254
255            Expr::Bin(BinExpr {
256                op: op!("=="),
257                left,
258                right,
259                ..
260            }) => JsValue::equal(arena, self.eval(arena, left), self.eval(arena, right)),
261
262            Expr::Bin(BinExpr {
263                op: op!("!="),
264                left,
265                right,
266                ..
267            }) => JsValue::not_equal(arena, self.eval(arena, left), self.eval(arena, right)),
268
269            Expr::Bin(BinExpr {
270                op: op!("==="),
271                left,
272                right,
273                ..
274            }) => JsValue::strict_equal(arena, self.eval(arena, left), self.eval(arena, right)),
275
276            Expr::Bin(BinExpr {
277                op: op!("!=="),
278                left,
279                right,
280                ..
281            }) => JsValue::strict_not_equal(arena, self.eval(arena, left), self.eval(arena, right)),
282
283            Expr::Bin(BinExpr {
284                op: op!("in"),
285                left,
286                right,
287                ..
288            }) => JsValue::r#in(arena, self.eval(arena, left), self.eval(arena, right)),
289
290            &Expr::Cond(CondExpr {
291                ref cons,
292                ref alt,
293                ref test,
294                ..
295            }) => {
296                let test = self.eval(arena, test);
297                if let Some(truthy) = test.is_truthy() {
298                    if truthy {
299                        self.eval(arena, cons)
300                    } else {
301                        self.eval(arena, alt)
302                    }
303                } else {
304                    JsValue::tenary(arena, test, self.eval(arena, cons), self.eval(arena, alt))
305                }
306            }
307
308            Expr::Tpl(e) => self.eval_tpl(arena, e, false),
309
310            Expr::TaggedTpl(TaggedTpl {
311                tag:
312                    Expr::Member(MemberExpr {
313                        obj: Expr::Ident(tag_obj),
314                        prop: MemberProp::Ident(tag_prop),
315                        ..
316                    }),
317                tpl,
318                ..
319            }) => {
320                if &*tag_obj.sym == "String"
321                    && &*tag_prop.sym == "raw"
322                    && is_unresolved(tag_obj, self.unresolved_mark)
323                {
324                    self.eval_tpl(arena, tpl, true)
325                } else {
326                    JsValue::unknown_empty(
327                        true,
328                        rcstr!("tagged template literal is not supported yet"),
329                    )
330                }
331            }
332
333            Expr::Fn(expr) => {
334                if let Some(ident) = &expr.ident {
335                    JsValue::Variable(ident.to_id())
336                } else {
337                    JsValue::Variable((
338                        format!("*anonymous function {}*", expr.function.span.lo.0).into(),
339                        SyntaxContext::empty(),
340                    ))
341                }
342            }
343            Expr::Arrow(expr) => JsValue::Variable((
344                format!("*arrow function {}*", expr.span.lo.0).into(),
345                SyntaxContext::empty(),
346            )),
347
348            Expr::Await(AwaitExpr { arg, .. }) => JsValue::awaited(arena, self.eval(arena, arg)),
349
350            Expr::Seq(e) => {
351                let mut seq = e.exprs.iter().map(|e| self.eval(arena, e)).peekable();
352                let mut side_effects = false;
353                let mut last = seq.next().unwrap();
354                for e in seq {
355                    side_effects |= last.has_side_effects();
356                    last = e;
357                }
358                if side_effects {
359                    last.make_unknown(true, rcstr!("sequence with side effects"));
360                }
361                last
362            }
363
364            Expr::Member(MemberExpr {
365                obj,
366                prop: MemberProp::Ident(prop),
367                ..
368            }) => {
369                let obj = self.eval(arena, obj);
370                JsValue::member(arena, obj, prop.sym.clone().into())
371            }
372
373            Expr::Member(MemberExpr {
374                obj,
375                prop: MemberProp::Computed(computed),
376                ..
377            }) => {
378                let obj = self.eval(arena, obj);
379                let prop = self.eval(arena, &computed.expr);
380                JsValue::member(arena, obj, prop)
381            }
382
383            Expr::New(NewExpr { callee, args, .. }) => {
384                let args = args.as_deref().unwrap_or(&[]);
385                // We currently do not handle spreads.
386                if args.iter().any(|arg| arg.spread.is_some()) {
387                    return JsValue::unknown_empty(
388                        true,
389                        rcstr!("spread in new calls is not supported"),
390                    );
391                }
392
393                JsValue::new_from_iter(
394                    arena,
395                    self.eval(arena, callee),
396                    args.iter().map(|arg| self.eval(arena, &arg.expr)),
397                )
398            }
399
400            Expr::Call(CallExpr {
401                callee: Callee::Expr(callee),
402                args,
403                ..
404            }) => {
405                // We currently do not handle spreads.
406                if args.iter().any(|arg| arg.spread.is_some()) {
407                    return JsValue::unknown_empty(
408                        true,
409                        rcstr!("spread in function calls is not supported"),
410                    );
411                }
412
413                if let Expr::Member(MemberExpr { obj, prop, .. }) = unparen(callee) {
414                    let prop = match prop {
415                        MemberProp::Ident(i) => i.sym.clone().into(),
416                        MemberProp::PrivateName(_) => {
417                            return JsValue::unknown_empty(
418                                false,
419                                rcstr!("private names in function calls is not supported"),
420                            );
421                        }
422                        MemberProp::Computed(ComputedPropName { expr, .. }) => {
423                            self.eval(arena, expr)
424                        }
425                    };
426                    let obj = self.eval(arena, obj);
427                    JsValue::member_call_from_iter(
428                        arena,
429                        obj,
430                        prop,
431                        args.iter().map(|arg| self.eval(arena, &arg.expr)),
432                    )
433                } else {
434                    JsValue::call_from_iter(
435                        arena,
436                        self.eval(arena, callee),
437                        args.iter().map(|arg| self.eval(arena, &arg.expr)),
438                    )
439                }
440            }
441
442            Expr::Call(CallExpr {
443                callee: Callee::Super(_),
444                args,
445                ..
446            }) => {
447                // We currently do not handle spreads.
448                if args.iter().any(|arg| arg.spread.is_some()) {
449                    return JsValue::unknown_empty(
450                        true,
451                        rcstr!("spread in function calls is not supported"),
452                    );
453                }
454
455                let args = bumpalo::collections::Vec::from_iter_in(
456                    args.iter().map(|arg| self.eval(arena, &arg.expr)),
457                    arena,
458                )
459                .into_boxed_slice();
460
461                JsValue::super_call(args)
462            }
463
464            Expr::Call(CallExpr {
465                callee: Callee::Import(_),
466                args,
467                ..
468            }) => {
469                // We currently do not handle spreads.
470                if args.iter().any(|arg| arg.spread.is_some()) {
471                    return JsValue::unknown_empty(
472                        true,
473                        rcstr!("spread in import() is not supported"),
474                    );
475                }
476                JsValue::call_from_iter(
477                    arena,
478                    JsValue::FreeVar(atom!("import")),
479                    args.iter().map(|arg| self.eval(arena, &arg.expr)),
480                )
481            }
482
483            Expr::Array(arr) => {
484                if arr.elems.iter().flatten().any(|v| v.spread.is_some()) {
485                    return JsValue::unknown_empty(true, rcstr!("spread is not supported"));
486                }
487
488                let arr = BumpVec::from_iter_in(
489                    arena,
490                    arr.elems.iter().map(|e| match e {
491                        Some(e) => self.eval(arena, &e.expr),
492                        _ => JsValue::Constant(ConstantValue::Undefined),
493                    }),
494                );
495                JsValue::array(arr)
496            }
497
498            Expr::Object(obj) => JsValue::object(BumpVec::from_iter_in(
499                arena,
500                obj.props.iter().map(|prop| match prop {
501                    PropOrSpread::Spread(SpreadElement { expr, .. }) => {
502                        ObjectPart::Spread(self.eval(arena, expr))
503                    }
504                    PropOrSpread::Prop(Prop::KeyValue(KeyValueProp { key, value })) => {
505                        ObjectPart::KeyValue(
506                            self.eval_prop_name(arena, key),
507                            self.eval(arena, value),
508                        )
509                    }
510                    PropOrSpread::Prop(Prop::Shorthand(ident)) => ObjectPart::KeyValue(
511                        ident.sym.clone().into(),
512                        self.eval(arena, &Expr::Ident(ident.clone())),
513                    ),
514                    _ => ObjectPart::Spread(JsValue::unknown_empty(
515                        true,
516                        rcstr!("unsupported object part"),
517                    )),
518                }),
519            )),
520
521            Expr::MetaProp(MetaPropExpr {
522                kind: MetaPropKind::ImportMeta,
523                ..
524            }) => JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta),
525
526            Expr::Assign(AssignExpr { op, .. }) => match op {
527                // TODO: `self.eval(arena, right)` would be the value, but we need to handle the
528                // side effect of that expression
529                AssignOp::Assign => JsValue::unknown_empty(true, rcstr!("assignment expression")),
530                _ => JsValue::unknown_empty(true, rcstr!("compound assignment expression")),
531            },
532
533            _ => JsValue::unknown_empty(true, rcstr!("unsupported expression")),
534        }
535    }
536
537    pub fn eval_single_expr_lit<'a>(arena: &'a Bump, expr_lit: &RcStr) -> Result<JsValue<'a>> {
538        let cm = Lrc::new(SourceMap::default());
539
540        let js_value = try_with_handler(cm, Default::default(), |_| {
541            GLOBALS.set(&Default::default(), || {
542                let expr = parse_single_expr_lit(expr_lit);
543                let eval_context =
544                    EvalContext::new(None, Mark::new(), Mark::new(), Default::default(), None);
545
546                Ok(eval_context.eval(arena, &expr))
547            })
548        })
549        .map_err(|e| e.to_pretty_error())?;
550
551        Ok(js_value)
552    }
553}