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}