Skip to main content

turbopack_ecmascript/analyzer/
builtin.rs

1use std::mem::take;
2
3use smallvec::SmallVec;
4use turbo_rcstr::rcstr;
5
6use super::{ConstantNumber, ConstantValue, JsValue, LogicalOperator, LogicalProperty, ObjectPart};
7use crate::analyzer::{Bump, BumpVec, JsValueUrlKind, Modified};
8
9/// Replaces some builtin values with their resulting values. Called early
10/// without lazy nested values. This allows to skip a lot of work to process the
11/// arguments.
12pub fn early_replace_builtin(value: &mut JsValue<'_>) -> Modified {
13    match value {
14        // matching calls like `callee(arg1, arg2, ...)`
15        JsValue::Call(_, call) => {
16            let (args, callee) = call.as_parts_mut();
17            let args_have_side_effects = || args.iter().any(|arg| arg.has_side_effects());
18            match callee {
19                // We don't know what the callee is, so we can early return
20                &mut JsValue::Unknown {
21                    original_value: _,
22                    reason: _,
23                    has_side_effects,
24                } => {
25                    let has_side_effects = has_side_effects || args_have_side_effects();
26                    value.make_unknown(has_side_effects, rcstr!("unknown callee"));
27                    Modified::Yes
28                }
29                // We know that these callees will lead to an error at runtime, so we can skip
30                // processing them
31                JsValue::Constant(_)
32                | JsValue::Url(_, _)
33                | JsValue::WellKnownObject(_)
34                | JsValue::Array { .. }
35                | JsValue::Object { .. }
36                | JsValue::Concat(_, _)
37                | JsValue::Add(_, _)
38                | JsValue::Not(_, _) => {
39                    let has_side_effects = args_have_side_effects();
40                    value.make_unknown(has_side_effects, rcstr!("non-function callee"));
41                    Modified::Yes
42                }
43                // Alternatives are only certainly not callable when none of them is a function.
44                // If one of them is, `replace_builtin` later expands the call into a call per
45                // alternative, which is what makes e.g. TypeScript's `esModuleInterop` helpers
46                // (`__importDefault(require('fs'))`, which is an alternative between an unknown
47                // value and the helper function) analyzable.
48                JsValue::Alternatives { values, .. }
49                    if !values.iter().any(|value| {
50                        matches!(value, JsValue::Function(..) | JsValue::WellKnownFunction(_))
51                    }) =>
52                {
53                    let has_side_effects = args_have_side_effects();
54                    value.make_unknown(has_side_effects, rcstr!("non-function callee"));
55                    Modified::Yes
56                }
57                _ => Modified::No,
58            }
59        }
60        // matching calls with this context like `obj.prop(arg1, arg2, ...)`
61        JsValue::MemberCall(_, call) => {
62            let (args, prop, obj) = call.as_parts_mut();
63            let args_have_side_effects = || args.iter().any(|arg| arg.has_side_effects());
64            match obj {
65                // We don't know what the callee is, so we can early return
66                &mut JsValue::Unknown {
67                    original_value: _,
68                    reason: _,
69                    has_side_effects,
70                } => {
71                    let side_effects =
72                        has_side_effects || prop.has_side_effects() || args_have_side_effects();
73                    value.make_unknown(side_effects, rcstr!("unknown callee object"));
74                    Modified::Yes
75                }
76                // otherwise we need to look at the property
77                _ => match prop {
78                    // We don't know what the property is, so we can early return
79                    &mut JsValue::Unknown {
80                        original_value: _,
81                        reason: _,
82                        has_side_effects,
83                    } => {
84                        let side_effects = has_side_effects || args_have_side_effects();
85                        value.make_unknown(side_effects, rcstr!("unknown callee property"));
86                        Modified::Yes
87                    }
88                    _ => Modified::No,
89                },
90            }
91        }
92        // matching property access like `obj.prop` when we don't know what the obj is.
93        // We can early return here
94        JsValue::Member(_, obj, prop) => {
95            if let JsValue::Unknown {
96                has_side_effects, ..
97            } = &**obj
98            {
99                let side_effects = *has_side_effects || prop.has_side_effects();
100                value.make_unknown(side_effects, rcstr!("unknown object"));
101                Modified::Yes
102            } else {
103                Modified::No
104            }
105        }
106        _ => Modified::No,
107    }
108}
109
110/// Replaces some builtin functions and values with their resulting values. In
111/// contrast to early_replace_builtin this has all inner values already
112/// processed.
113pub fn replace_builtin<'a>(arena: &'a Bump, value: &mut JsValue<'a>) -> Modified {
114    match value {
115        JsValue::Add(_, list) => {
116            // numeric addition
117            let mut sum = 0f64;
118            for arg in list {
119                let JsValue::Constant(ConstantValue::Num(num)) = arg else {
120                    return Modified::No;
121                };
122                sum += num.0;
123            }
124            *value = JsValue::Constant(ConstantValue::Num(sum.into()));
125            Modified::Yes
126        }
127
128        // matching property access like `obj.prop`
129        // Accessing a property on something can be handled in some cases
130        JsValue::Member(_, obj, prop) => match &mut **obj {
131            // matching property access when obj is a bunch of alternatives
132            // like `(obj1 | obj2 | obj3).prop`
133            // We expand these to `obj1.prop | obj2.prop | obj3.prop`
134            JsValue::Alternatives {
135                total_nodes: _,
136                values,
137                logical_property: _,
138            } => {
139                *value = JsValue::alternatives(BumpVec::from_iter_in(
140                    arena,
141                    take(values)
142                        .into_iter()
143                        .map(|alt| JsValue::member(arena, alt, prop.clone_in(arena))),
144                ));
145                Modified::Yes
146            }
147            // matching property access on an array like `[1,2,3].prop` or `[1,2,3][1]`
148            &mut JsValue::Array {
149                ref mut items,
150                mutable,
151                total_nodes: _,
152            } => {
153                fn items_to_alternatives<'a>(
154                    arena: &'a Bump,
155                    items: &mut BumpVec<'a, JsValue<'a>>,
156                    prop: &mut JsValue<'a>,
157                ) -> JsValue<'a> {
158                    items.push(arena, JsValue::unknown(
159                        JsValue::member(arena, JsValue::array(BumpVec::new()), take(prop)),
160                        false,
161                        rcstr!("unknown array prototype methods or values"),
162                    ));
163                    JsValue::alternatives(take(items))
164                }
165                match &mut **prop {
166                    // accessing a numeric property on an array like `[1,2,3][1]`
167                    // We can replace this with the value at the index
168                    JsValue::Constant(ConstantValue::Num(num @ ConstantNumber(_))) => {
169                        if let Some(index) = num.as_u32_index() {
170                            if index < items.len() {
171                                *value = items.swap_remove(index);
172                                if mutable {
173                                    value.add_unknown_mutations(arena, true);
174                                }
175                                Modified::Yes
176                            } else {
177                                *value = JsValue::unknown(
178                                    JsValue::member(arena, take(&mut **obj), take(&mut **prop)),
179                                    false,
180                                    rcstr!("invalid index"),
181                                );
182                                Modified::Yes
183                            }
184                        } else {
185                            value.make_unknown(false, rcstr!("non-num constant property on array"));
186                            Modified::Yes
187                        }
188                    }
189                    // accessing a non-numeric property on an array like `[1,2,3].length`
190                    // We don't know what happens here
191                    JsValue::Constant(_) => {
192                        value.make_unknown(false, rcstr!("non-num constant property on array"));
193                        Modified::Yes
194                    }
195                    // accessing multiple alternative properties on an array like `[1,2,3][(1 | 2 |
196                    // prop3)]`
197                    JsValue::Alternatives {
198                        total_nodes: _,
199                        values,
200                        logical_property: _,
201                    } => {
202                        *value = JsValue::alternatives(BumpVec::from_iter_in(
203                            arena,
204                            take(values)
205                                .into_iter()
206                                .map(|alt| JsValue::member(arena, obj.clone_in(arena), alt)),
207                        ));
208                        Modified::Yes
209                    }
210                    // otherwise we can say that this might gives an item of the array
211                    // but we also add an unknown value to the alternatives for other properties
212                    _ => {
213                        *value = items_to_alternatives(arena, items, prop);
214                        Modified::Yes
215                    }
216                }
217            }
218            // matching property access on an object like `{a: 1, b: 2}.a`
219            &mut JsValue::Object {
220                ref mut parts,
221                mutability,
222                total_nodes: _,
223            } => {
224                fn parts_to_alternatives<'a>(
225                    arena: &'a Bump,
226                    parts: impl IntoIterator<Item = ObjectPart<'a>>,
227                    prop: &mut JsValue<'a>,
228                    include_unknown: bool,
229                ) -> JsValue<'a> {
230                    let parts = parts.into_iter();
231                    let (lower, upper) = parts.size_hint();
232                    let mut values = BumpVec::with_capacity_in(
233                        arena, upper.unwrap_or(lower) + if include_unknown { 1 } else { 0 }
234                    );
235                    for part in parts {
236                        match part {
237                            ObjectPart::KeyValue(_, value) => {
238                                values.push(arena, value);
239                            }
240                            ObjectPart::Spread(_) => {
241                                values.push(arena, JsValue::unknown(
242                                    JsValue::member(
243                                        arena,
244                                        JsValue::object(BumpVec::from_iter_in(arena, [part])),
245                                        prop.clone_in(arena),
246                                    ),
247                                    true,
248                                    rcstr!("spread object"),
249                                ));
250                            }
251                        }
252                    }
253                    if include_unknown {
254                        values.push(arena, JsValue::unknown(
255                            JsValue::member(
256                                arena,
257                                JsValue::object(BumpVec::new()),
258                                take(prop),
259                            ),
260                            true,
261                            rcstr!("unknown object prototype methods or values"),
262                        ));
263                    }
264                    JsValue::alternatives(values)
265                }
266
267                /// Convert a list of potential values into
268                /// JsValue::Alternatives Optionally add a
269                /// unknown value to the alternatives for object prototype
270                /// methods
271                fn potential_values_to_alternatives<'a>(
272                    arena: &'a Bump,
273                    mut potential_values: SmallVec<[usize; 8]>,
274                    parts: &mut BumpVec<'a, ObjectPart<'a>>,
275                    prop: &mut JsValue<'a>,
276                    include_unknown: bool,
277                ) -> JsValue<'a> {
278                    // Note: potential_values are already in reverse order
279                    let mut potential_values = take(parts)
280                        .into_iter()
281                        .enumerate()
282                        .filter(|(i, _)| {
283                            if potential_values.last() == Some(i) {
284                                potential_values.pop();
285                                true
286                            } else {
287                                false
288                            }
289                        })
290                        .map(|(_, part)| part);
291                    parts_to_alternatives(arena, &mut potential_values, prop, include_unknown)
292                }
293
294                match &mut **prop {
295                    // matching constant string property access on an object like `{a: 1, b:
296                    // 2}["a"]`
297                    JsValue::Constant(ConstantValue::Str(_)) => {
298                        let prop_str = prop.as_str().unwrap();
299                        let mut potential_values: SmallVec<[usize; 8]> = SmallVec::new();
300                        for (i, part) in parts.iter_mut().enumerate().rev() {
301                            match part {
302                                ObjectPart::KeyValue(key, val) => {
303                                    if let Some(key) = key.as_str() {
304                                        if key == prop_str {
305                                            if potential_values.is_empty() {
306                                                *value = take(val);
307                                            } else {
308                                                potential_values.push(i);
309                                                *value = potential_values_to_alternatives(
310                                                    arena,
311                                                    potential_values,
312                                                    parts,
313                                                    prop,
314                                                    false,
315                                                );
316                                            }
317                                            if mutability.is_mutable() {
318                                                value.add_unknown_mutations(arena, true);
319                                            }
320                                            return Modified::Yes;
321                                        }
322                                    } else {
323                                        potential_values.push(i);
324                                    }
325                                }
326                                ObjectPart::Spread(_) => {
327                                    value.make_unknown(true, rcstr!("spread object"));
328                                    return Modified::Yes;
329                                }
330                            }
331                        }
332                        if potential_values.is_empty() {
333                            if mutability.is_missing_unknown() {
334                                *value = JsValue::unknown_empty(false, rcstr!("missing object property"));
335                            } else {
336                                *value = JsValue::Constant(ConstantValue::Undefined);
337                            }
338                        } else {
339                            *value = potential_values_to_alternatives(
340                                arena,
341                                potential_values,
342                                parts,
343                                prop,
344                                true,
345                            );
346                        }
347                        if mutability.is_mutable() {
348                            value.add_unknown_mutations(arena, true);
349                        }
350                        Modified::Yes
351                    }
352                    // matching multiple alternative properties on an object like `{a: 1, b: 2}[(a |
353                    // b)]`
354                    JsValue::Alternatives {
355                        total_nodes: _,
356                        values,
357                        logical_property: _,
358                    } => {
359                        *value = JsValue::alternatives(BumpVec::from_iter_in(
360                            arena,
361                            take(values)
362                                .into_iter()
363                                .map(|alt| JsValue::member(arena, obj.clone_in(arena), alt)),
364                        ));
365                        Modified::Yes
366                    }
367                    _ => {
368                        *value = parts_to_alternatives(arena, take(parts), prop, true);
369                        Modified::Yes
370                    }
371                }
372            }
373            _ => Modified::No,
374        },
375
376        JsValue::MemberCall(_, _) => {
377            // `into_parts` pops obj + prop off the tail of the underlying `Vec`, and the
378            // remaining `Vec` (owned, not reallocated) becomes `args`. We take the whole
379            // `value` because `MemberCallList` has no `Default` to move it out directly.
380            let JsValue::MemberCall(_, call) = take(value) else {
381                unreachable!()
382            };
383            let (mut obj, prop, args) = call.into_parts();
384            match &mut obj {
385                // matching calls on an array like `[1,2,3].concat([4,5,6])`
386                JsValue::Array { items, mutable, .. } => {
387                    // matching cases where the property is a const string
388                    if let Some(str) = prop.as_str() {
389                        match str {
390                            // The Array.prototype.concat method
391                            "concat"
392                                if args.iter().all(|arg| {
393                                    matches!(
394                                        arg,
395                                        JsValue::Array { .. }
396                                            | JsValue::Constant(_)
397                                            | JsValue::Url(_, JsValueUrlKind::Absolute)
398                                            | JsValue::Concat(..)
399                                            | JsValue::Add(..)
400                                            | JsValue::WellKnownObject(_)
401                                            | JsValue::WellKnownFunction(_)
402                                            | JsValue::Function(..)
403                                    )
404                                }) => {
405                                    for arg in args {
406                                        match arg {
407                                            JsValue::Array {
408                                                items: inner,
409                                                mutable: inner_mutable,
410                                                ..
411                                            } => {
412                                                items.extend(arena, inner);
413                                                *mutable |= inner_mutable;
414                                            }
415                                            other @ (JsValue::Constant(_)
416                                            | JsValue::Url(_, JsValueUrlKind::Absolute)
417                                            | JsValue::Concat(..)
418                                            | JsValue::Add(..)
419                                            | JsValue::WellKnownObject(_)
420                                            | JsValue::WellKnownFunction(_)
421                                            | JsValue::Function(..)) => {
422                                                items.push(arena, other);
423                                            }
424                                            _ => {
425                                                unreachable!();
426                                            }
427                                        }
428                                    }
429                                    obj.update_total_nodes();
430                                    *value = obj;
431                                    return Modified::Yes;
432                                }
433                            // The Array.prototype.map method
434                            "map" => {
435                                if let Some(func) = args.first() {
436                                    *value = JsValue::array(BumpVec::from_iter_in(
437                                        arena,
438                                        take(items).into_iter().enumerate().map(|(i, item)| {
439                                            JsValue::call_from_iter(
440                                                arena,
441                                                func.clone_in(arena),
442                                                [
443                                                    item,
444                                                    JsValue::Constant(ConstantValue::Num(
445                                                        (i as f64).into(),
446                                                    )),
447                                                ],
448                                            )
449                                        }),
450                                    ));
451                                    return Modified::Yes;
452                                }
453                            }
454                            _ => {}
455                        }
456                    }
457                }
458                // matching calls on multiple alternative objects like `(obj1 | obj2).prop(arg1,
459                // arg2, ...)`
460                JsValue::Alternatives {
461                    total_nodes: _,
462                    values,
463                    logical_property: _,
464                } => {
465                    *value = JsValue::alternatives(BumpVec::from_iter_in(
466                        arena,
467                        take(values).into_iter().map(|alt| {
468                            JsValue::member_call_from_iter(
469                                arena,
470                                alt,
471                                prop.clone_in(arena),
472                                args.iter().map(|a| a.clone_in(arena)),
473                            )
474                        },
475                    )));
476                    return Modified::Yes;
477                }
478                _ => {}
479            }
480
481            // matching calls on strings like `"dayjs/locale/".concat(userLocale, ".js")`
482            if obj.is_string() == Some(true)
483                && let Some(str) = prop.as_str()
484            {
485                // The String.prototype.concat method
486                if str == "concat" {
487                    let mut values = BumpVec::with_capacity_in(arena, 1 + args.len());
488                    values.push(arena, obj);
489                    values.extend(arena, args);
490
491                    *value = JsValue::concat(values);
492                    return Modified::Yes;
493                }
494            }
495
496            // without special handling, we convert it into a normal call like
497            // `(obj.prop)(arg1, arg2, ...)`.
498            //
499            // Pass-through path: `args` came from `MemberCallList::into_parts` which yields
500            // a `Vec` with `cap >= len + 2` (slack from the original layout). Re-wrapping it
501            // into a `JsValue::Call` only needs `+1` slot, which fits in the existing slack —
502            // no realloc. This is the original motivation for the `[args..., prop, obj]`
503            // tail layout.
504            *value = JsValue::call_from_parts(arena, JsValue::member(arena, obj, prop), args);
505            Modified::Yes
506        }
507        // match calls when the callee are multiple alternative functions like `(func1 |
508        // func2)(arg1, arg2, ...)`
509        JsValue::Call(_, call)
510            if matches!(call.callee(), JsValue::Alternatives { .. }) =>
511        {
512            // Take the whole `value` (not `call`) because `CallList` has no `Default`, then
513            // move the alternatives `values` out of the callee.
514            let JsValue::Call(_, call) = take(value) else {
515                unreachable!()
516            };
517            let (callee, args) = call.into_parts();
518            let JsValue::Alternatives { values, .. } = callee else {
519                unreachable!()
520            };
521            *value = JsValue::alternatives(BumpVec::from_iter_in(arena,
522                values
523                    .into_iter()
524                    .map(|alt| JsValue::call_from_iter(arena, alt, args.iter().map(|a| a.clone_in(arena)))),
525            ));
526            Modified::Yes
527        }
528        // match object literals
529        JsValue::Object {
530            parts,
531            mutability,
532            total_nodes: _,
533        }
534            // If the object contains any spread, we might be able to flatten that
535            if parts
536                .iter()
537                .any(|part| matches!(part, ObjectPart::Spread(JsValue::Object { .. })))
538            => {
539                let old_parts = take(parts);
540                for part in old_parts {
541                    if let ObjectPart::Spread(JsValue::Object {
542                        parts: inner_parts,
543                        mutability: inner_mutability,
544                        ..
545                    }) = part
546                    {
547                        parts.extend(arena, inner_parts);
548                        mutability.merge_with(inner_mutability);
549                    } else {
550                        parts.push(arena, part);
551                    }
552                }
553                value.update_total_nodes();
554                Modified::Yes
555            }
556        // match logical expressions like `a && b` or `a || b || c` or `a ?? b`
557        // Reduce logical expressions to their final value(s)
558        JsValue::Logical(..) => {
559            let JsValue::Logical(_, op, input_parts) = take(value) else {
560                unreachable!()
561            };
562            let len = input_parts.len();
563            let mut parts = BumpVec::<JsValue<'a>>::with_capacity_in(arena, len);
564            let mut part_properties = Vec::with_capacity(len);
565            for (i, part) in input_parts.into_iter().enumerate() {
566                // The last part is never skipped.
567                if i == len - 1 {
568                    // We intentionally omit the part_properties for the last part.
569                    // This isn't always needed so we only compute it when actually needed.
570                    parts.push(arena, part);
571                    break;
572                }
573                let property = match op {
574                    LogicalOperator::And => part.is_truthy(),
575                    LogicalOperator::Or => part.is_falsy(),
576                    LogicalOperator::NullishCoalescing => part.is_nullish(),
577                };
578                // We might know at compile-time if a part is skipped or the final value.
579                match property {
580                    Some(true) => {
581                        // We known this part is skipped, so we can remove it.
582                        continue;
583                    }
584                    Some(false) => {
585                        // We known this part is the final value, so we can remove the rest.
586                        part_properties.push(property);
587                        parts.push(arena, part);
588                        break;
589                    }
590                    None => {
591                        // We don't know if this part is skipped or the final value, so we keep it.
592                        part_properties.push(property);
593                        parts.push(arena, part);
594                        continue;
595                    }
596                }
597            }
598            // If we reduced the expression to a single value, we can replace it.
599            if parts.len() == 1 {
600                *value = parts.pop().unwrap();
601                Modified::Yes
602            } else {
603                // If not, we know that it will be one of the remaining values.
604                let last_part = parts.last().unwrap();
605                let property = match op {
606                    LogicalOperator::And => last_part.is_truthy(),
607                    LogicalOperator::Or => last_part.is_falsy(),
608                    LogicalOperator::NullishCoalescing => last_part.is_nullish(),
609                };
610                part_properties.push(property);
611                let (any_unset, all_set) =
612                    part_properties
613                        .iter()
614                        .fold((false, true), |(any_unset, all_set), part| match part {
615                            Some(true) => (any_unset, all_set),
616                            Some(false) => (true, false),
617                            None => (any_unset, false),
618                        });
619                let property = match op {
620                    LogicalOperator::Or => {
621                        if any_unset {
622                            Some(LogicalProperty::Truthy)
623                        } else if all_set {
624                            Some(LogicalProperty::Falsy)
625                        } else {
626                            None
627                        }
628                    }
629                    LogicalOperator::And => {
630                        if any_unset {
631                            Some(LogicalProperty::Falsy)
632                        } else if all_set {
633                            Some(LogicalProperty::Truthy)
634                        } else {
635                            None
636                        }
637                    }
638                    LogicalOperator::NullishCoalescing => {
639                        if any_unset {
640                            Some(LogicalProperty::NonNullish)
641                        } else if all_set {
642                            Some(LogicalProperty::Nullish)
643                        } else {
644                            None
645                        }
646                    }
647                };
648                if let Some(property) = property {
649                    *value = JsValue::alternatives_with_additional_property(parts, property);
650                    Modified::Yes
651                } else {
652                    *value = JsValue::alternatives(parts);
653                    Modified::Yes
654                }
655            }
656        }
657        JsValue::Tenary(_, test, cons, alt) => {
658            if test.is_truthy() == Some(true) {
659                *value = take(&mut **cons);
660                Modified::Yes
661            } else if test.is_falsy() == Some(true) {
662                *value = take(&mut **alt);
663                Modified::Yes
664            } else {
665                Modified::No
666            }
667        }
668        // match a binary operator like `a == b`
669        JsValue::Binary(..) => {
670            if let Some(v) = value.is_truthy() {
671                let v = if v {
672                    ConstantValue::True
673                } else {
674                    ConstantValue::False
675                };
676                *value = JsValue::Constant(v);
677                Modified::Yes
678            } else {
679                Modified::No
680            }
681        }
682        // match the not operator like `!a`
683        // Evaluate not when the inner value is truthy or falsy
684        JsValue::Not(_, inner) => match inner.is_truthy() {
685            Some(true) => {
686                *value = JsValue::Constant(ConstantValue::False);
687                Modified::Yes
688            }
689            Some(false) => {
690                *value = JsValue::Constant(ConstantValue::True);
691                Modified::Yes
692            }
693            None => Modified::No,
694        },
695
696        JsValue::Iterated(_, iterable) => {
697            if let JsValue::Array { items, mutable, .. } = &mut **iterable {
698                let mut new_value = JsValue::alternatives(take(items));
699                if *mutable {
700                    new_value.add_unknown_mutations(arena, true);
701                }
702                *value = new_value;
703                Modified::Yes
704            } else {
705                Modified::No
706            }
707        }
708
709        JsValue::Awaited(_, operand) => {
710            if let JsValue::Promise(_, inner) = &mut **operand {
711                *value = take(&mut **inner);
712                Modified::Yes
713            } else {
714                *value = take(&mut **operand);
715                Modified::Yes
716            }
717        }
718
719        _ => Modified::No,
720    }
721}