turbopack_ecmascript/analyzer/
builtin.rs

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