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}