turbopack_ecmascript/analyzer/
builtin.rs

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