Skip to main content

turbopack_ecmascript/analyzer/
builtin.rs

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