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