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}