swc_ecma_minifier/compress/pure/
evaluate.rs

1use radix_fmt::Radix;
2use swc_atoms::atom;
3use swc_common::{util::take::Take, Spanned, SyntaxContext};
4use swc_ecma_ast::*;
5use swc_ecma_utils::{
6    number::ToJsString,
7    unicode::{is_high_surrogate, is_low_surrogate},
8    ExprExt, IsEmpty, Type, Value,
9};
10
11use super::Pure;
12#[cfg(feature = "debug")]
13use crate::debug::dump;
14use crate::{
15    compress::{
16        pure::Ctx,
17        util::{eval_as_number, is_pure_undefined_or_null},
18    },
19    util::ValueExt,
20};
21
22impl Pure<'_> {
23    ///
24    /// - `1 == 1` => `true`
25    /// - `1 == 2` => `false`
26    pub(super) fn optimize_lit_cmp(&mut self, n: &mut BinExpr) -> Option<Expr> {
27        if n.op != op!("==") && n.op != op!("!=") {
28            return None;
29        }
30        let flag = n.op == op!("!=");
31        let mut make_lit_bool = |value: bool| {
32            self.changed = true;
33            Some(
34                Lit::Bool(Bool {
35                    span: n.span,
36                    value: flag ^ value,
37                })
38                .into(),
39            )
40        };
41        match (
42            n.left.get_type(self.expr_ctx).opt()?,
43            n.right.get_type(self.expr_ctx).opt()?,
44        ) {
45            // Abort if types differ, or one of them is unknown.
46            (lt, rt) if lt != rt => {}
47            (Type::Obj, Type::Obj) => {}
48            (Type::Num, Type::Num) => {
49                let l = n.left.as_pure_number(self.expr_ctx).opt()?;
50                let r = n.right.as_pure_number(self.expr_ctx).opt()?;
51                report_change!("Optimizing: literal comparison => num");
52                return make_lit_bool(l == r);
53            }
54            (Type::Str, Type::Str) => {
55                let l = &n.left.as_pure_string(self.expr_ctx).opt()?;
56                let r = &n.right.as_pure_string(self.expr_ctx).opt()?;
57                report_change!("Optimizing: literal comparison => str");
58                return make_lit_bool(l == r);
59            }
60            (_, _) => {
61                let l = n.left.as_pure_bool(self.expr_ctx).opt()?;
62                let r = n.right.as_pure_bool(self.expr_ctx).opt()?;
63                report_change!("Optimizing: literal comparison => bool");
64                return make_lit_bool(l == r);
65            }
66        };
67
68        None
69    }
70
71    pub(super) fn eval_array_spread(&mut self, e: &mut Expr) {
72        if !self.options.evaluate {
73            return;
74        }
75
76        let Expr::Array(ArrayLit { elems, .. }) = e else {
77            return;
78        };
79
80        if !elems.iter().any(|elem| match elem {
81            Some(ExprOrSpread {
82                spread: Some(..),
83                expr,
84            }) => expr.is_array(),
85            _ => false,
86        }) {
87            return;
88        }
89
90        report_change!("evaluate: Evaluated array spread");
91        self.changed = true;
92
93        let mut new_elems = Vec::with_capacity(elems.len());
94
95        for elem in elems.take() {
96            match elem {
97                Some(ExprOrSpread {
98                    spread: Some(..),
99                    expr,
100                }) if expr.is_array() => {
101                    new_elems.extend(expr.expect_array().elems);
102                }
103                _ => {
104                    new_elems.push(elem);
105                }
106            }
107        }
108
109        *elems = new_elems;
110    }
111
112    pub(super) fn eval_logical_expr(&mut self, e: &mut Expr) {
113        let Expr::Bin(
114            b @ BinExpr {
115                op: op!("||") | op!("&&"),
116                ..
117            },
118        ) = e
119        else {
120            return;
121        };
122
123        let (purity, lv) = b.left.cast_to_bool(self.expr_ctx);
124
125        if purity.is_pure() {
126            if let Value::Known(lv) = lv {
127                match (lv, b.op) {
128                    (true, op!("||")) => {
129                        self.changed = true;
130                        report_change!("evaluate: `true || foo` => `true`");
131
132                        *e = *b.left.take();
133                    }
134                    (false, op!("||")) => {
135                        self.changed = true;
136                        report_change!("evaluate: `false || foo` => `foo`");
137
138                        *e = *b.right.take();
139                    }
140                    (true, op!("&&")) => {
141                        self.changed = true;
142                        report_change!("evaluate: `true && foo` => `foo`");
143
144                        *e = *b.right.take();
145                    }
146                    (false, op!("&&")) => {
147                        self.changed = true;
148                        report_change!("evaluate: `false && foo` => `false`");
149
150                        *e = *b.left.take();
151                    }
152                    _ => {}
153                }
154            }
155        } else {
156            if let Value::Known(lv) = lv {
157                match (lv, b.op) {
158                    (true, op!("||")) => {
159                        self.changed = true;
160                        report_change!("evaluate: `truthy || foo` => `truthy`");
161                        *e = *b.left.take();
162                    }
163
164                    (false, op!("&&")) => {
165                        self.changed = true;
166                        report_change!("evaluate: `falsy && foo` => `falsy`");
167                        *e = *b.left.take();
168                    }
169
170                    (true, op!("&&")) => {
171                        self.changed = true;
172                        report_change!("evaluate: `truthy && foo` => `truthy, foo`");
173                        *e = *Expr::from_exprs(vec![b.left.take(), b.right.take()]);
174                    }
175
176                    (false, op!("||")) => {
177                        self.changed = true;
178                        report_change!("evaluate: `falsy || foo` => `falsy, foo`");
179                        *e = *Expr::from_exprs(vec![b.left.take(), b.right.take()]);
180                    }
181
182                    _ => {}
183                }
184            }
185        }
186    }
187
188    pub(super) fn eval_array_method_call(&mut self, e: &mut Expr) {
189        if !self.options.evaluate {
190            return;
191        }
192
193        if self.ctx.intersects(
194            Ctx::IN_DELETE
195                .union(Ctx::IS_UPDATE_ARG)
196                .union(Ctx::IS_LHS_OF_ASSIGN),
197        ) {
198            return;
199        }
200
201        let call = match e {
202            Expr::Call(e) => e,
203            _ => return,
204        };
205
206        let has_spread = call.args.iter().any(|arg| arg.spread.is_some());
207
208        for arg in &call.args {
209            if arg.expr.may_have_side_effects(self.expr_ctx) {
210                return;
211            }
212        }
213
214        let callee = match &mut call.callee {
215            Callee::Super(_) | Callee::Import(_) => return,
216            Callee::Expr(e) => &mut **e,
217            #[cfg(swc_ast_unknown)]
218            _ => panic!("unable to access unknown nodes"),
219        };
220
221        if let Expr::Member(MemberExpr {
222            span,
223            obj,
224            prop: MemberProp::Ident(method_name),
225        }) = callee
226        {
227            if obj.may_have_side_effects(self.expr_ctx) {
228                return;
229            }
230
231            let arr = match &mut **obj {
232                Expr::Array(arr) => arr,
233                _ => return,
234            };
235
236            let has_spread_elem = arr.elems.iter().any(|s| {
237                matches!(
238                    s,
239                    Some(ExprOrSpread {
240                        spread: Some(..),
241                        ..
242                    })
243                )
244            });
245
246            // Ignore array
247
248            if &*method_name.sym == "slice" {
249                if has_spread || has_spread_elem {
250                    return;
251                }
252
253                match call.args.len() {
254                    0 => {
255                        self.changed = true;
256                        report_change!("evaluate: Dropping array.slice call");
257                        *e = *obj.take();
258                    }
259                    1 => {
260                        if let Value::Known(start) = call.args[0].expr.as_pure_number(self.expr_ctx)
261                        {
262                            if start.is_sign_negative() {
263                                return;
264                            }
265
266                            let start = start.floor() as usize;
267
268                            self.changed = true;
269                            report_change!("evaluate: Reducing array.slice({}) call", start);
270
271                            if start >= arr.elems.len() {
272                                *e = ArrayLit {
273                                    span: *span,
274                                    elems: Default::default(),
275                                }
276                                .into();
277                                return;
278                            }
279
280                            let elems = arr.elems.drain(start..).collect();
281
282                            *e = ArrayLit { span: *span, elems }.into();
283                        }
284                    }
285                    _ => {
286                        let start = call.args[0].expr.as_pure_number(self.expr_ctx);
287                        let end = call.args[1].expr.as_pure_number(self.expr_ctx);
288                        if let Value::Known(start) = start {
289                            if start.is_sign_negative() {
290                                return;
291                            }
292
293                            let start = start.floor() as usize;
294
295                            if let Value::Known(end) = end {
296                                if end.is_sign_negative() {
297                                    return;
298                                }
299
300                                let end = end.floor() as usize;
301                                let end = end.min(arr.elems.len());
302
303                                if start >= end {
304                                    return;
305                                }
306
307                                self.changed = true;
308                                report_change!(
309                                    "evaluate: Reducing array.slice({}, {}) call",
310                                    start,
311                                    end
312                                );
313                                if start >= arr.elems.len() {
314                                    *e = ArrayLit {
315                                        span: *span,
316                                        elems: Default::default(),
317                                    }
318                                    .into();
319                                    return;
320                                }
321
322                                let elems = arr.elems.drain(start..end).collect();
323
324                                *e = ArrayLit { span: *span, elems }.into();
325                            }
326                        }
327                    }
328                }
329                return;
330            }
331
332            if self.options.unsafe_passes
333                && &*method_name.sym == "toString"
334                && arr.elems.len() == 1
335                && arr.elems[0].is_some()
336            {
337                report_change!("evaluate: Reducing array.toString() call");
338                self.changed = true;
339                *obj = arr.elems[0]
340                    .take()
341                    .map(|elem| elem.expr)
342                    .unwrap_or_else(|| Expr::undefined(*span));
343            }
344        }
345    }
346
347    pub(super) fn eval_fn_method_call(&mut self, e: &mut Expr) {
348        if !self.options.evaluate {
349            return;
350        }
351
352        if self.ctx.intersects(
353            Ctx::IN_DELETE
354                .union(Ctx::IS_UPDATE_ARG)
355                .union(Ctx::IS_LHS_OF_ASSIGN),
356        ) {
357            return;
358        }
359
360        let call = match e {
361            Expr::Call(e) => e,
362            _ => return,
363        };
364
365        let has_spread = call.args.iter().any(|arg| arg.spread.is_some());
366
367        for arg in &call.args {
368            if arg.expr.may_have_side_effects(self.expr_ctx) {
369                return;
370            }
371        }
372
373        let callee = match &mut call.callee {
374            Callee::Super(_) | Callee::Import(_) => return,
375            Callee::Expr(e) => &mut **e,
376            #[cfg(swc_ast_unknown)]
377            _ => panic!("unable to access unknown nodes"),
378        };
379
380        if let Expr::Member(MemberExpr {
381            obj,
382            prop: MemberProp::Ident(method_name),
383            ..
384        }) = callee
385        {
386            if obj.may_have_side_effects(self.expr_ctx) {
387                return;
388            }
389
390            let f = match &mut **obj {
391                Expr::Fn(v) => v,
392                _ => return,
393            };
394
395            if &*method_name.sym == "valueOf" {
396                if has_spread {
397                    return;
398                }
399
400                self.changed = true;
401                report_change!("evaluate: Reduced `function.valueOf()` into a function expression");
402
403                *e = *obj.take();
404                return;
405            }
406
407            if self.options.unsafe_passes
408                && &*method_name.sym == "toString"
409                && f.function.params.is_empty()
410                && f.function.body.is_empty()
411            {
412                if has_spread {
413                    return;
414                }
415
416                self.changed = true;
417                report_change!("evaluate: Reduced `function.toString()` into a string");
418
419                *e = Str {
420                    span: call.span,
421                    value: atom!("function(){}").into(),
422                    raw: None,
423                }
424                .into();
425            }
426        }
427    }
428
429    pub(super) fn eval_arguments_member_access(&mut self, e: &mut Expr) {
430        let member = match e {
431            Expr::Member(e) => e,
432            _ => return,
433        };
434
435        if !member.obj.is_ident_ref_to("arguments") {
436            return;
437        }
438
439        match &mut member.prop {
440            MemberProp::Ident(_) => {}
441            MemberProp::PrivateName(_) => {}
442            MemberProp::Computed(p) => {
443                if let Expr::Lit(Lit::Str(s)) = &*p.expr {
444                    if let Some(value) = s.value.as_str() {
445                        if let Ok(value) = value.parse::<u32>() {
446                            p.expr = Lit::Num(Number {
447                                span: s.span,
448                                value: value as f64,
449                                raw: None,
450                            })
451                            .into();
452                        }
453                    }
454                }
455            }
456            #[cfg(swc_ast_unknown)]
457            _ => panic!("unable to access unknown nodes"),
458        }
459    }
460
461    /// unsafely evaluate call to `Number`.
462    pub(super) fn eval_number_call(&mut self, e: &mut Expr) {
463        if self.options.unsafe_passes && self.options.unsafe_math {
464            if let Expr::Call(CallExpr {
465                span,
466                callee: Callee::Expr(callee),
467                args,
468                ..
469            }) = e
470            {
471                if args.len() == 1 && args[0].spread.is_none() {
472                    if callee.is_ident_ref_to("Number") {
473                        self.changed = true;
474                        report_change!(
475                            "evaluate: Reducing a call to `Number` into an unary operation"
476                        );
477
478                        *e = UnaryExpr {
479                            span: *span,
480                            op: op!(unary, "+"),
481                            arg: args.take().into_iter().next().unwrap().expr,
482                        }
483                        .into();
484                    }
485                }
486            }
487        }
488    }
489
490    /// Evaluates method calls of a numeric constant.
491    pub(super) fn eval_number_method_call(&mut self, e: &mut Expr) {
492        if !self.options.evaluate {
493            return;
494        }
495
496        let (num, method, args) = match e {
497            Expr::Call(CallExpr {
498                callee: Callee::Expr(callee),
499                args,
500                ..
501            }) => match &mut **callee {
502                Expr::Member(MemberExpr {
503                    obj,
504                    prop: MemberProp::Ident(prop),
505                    ..
506                }) => match &mut **obj {
507                    Expr::Lit(Lit::Num(obj)) => (obj, prop, args),
508                    _ => return,
509                },
510                _ => return,
511            },
512            _ => return,
513        };
514
515        if args
516            .iter()
517            .any(|arg| arg.expr.may_have_side_effects(self.expr_ctx))
518        {
519            return;
520        }
521
522        if &*method.sym == "toFixed" {
523            // https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.prototype.tofixed
524            //
525            // Note 1: This method returns a String containing this Number value represented
526            // in decimal fixed-point notation with fractionDigits digits after the decimal
527            // point. If fractionDigits is undefined, 0 is assumed.
528
529            // Note 2: The output of toFixed may be more precise than toString for some
530            // values because toString only prints enough significant digits to distinguish
531            // the number from adjacent Number values. For example,
532            //
533            // (1000000000000000128).toString() returns "1000000000000000100", while
534            // (1000000000000000128).toFixed(0) returns "1000000000000000128".
535
536            // 1. Let x be ? thisNumberValue(this value).
537            // 2. Let f be ? ToIntegerOrInfinity(fractionDigits).
538            if let Some(precision) = args
539                .first()
540                // 3. Assert: If fractionDigits is undefined, then f is 0.
541                .map_or(Some(0f64), |arg| eval_as_number(self.expr_ctx, &arg.expr))
542            {
543                let f = precision.trunc() as u8;
544
545                // 4. If f is not finite, throw a RangeError exception.
546                // 5. If f < 0 or f > 100, throw a RangeError exception.
547
548                // Note: ES2018 increased the maximum number of fraction digits from 20 to 100.
549                // It relies on runtime behavior.
550                if !(0..=20).contains(&f) {
551                    return;
552                }
553
554                let mut buffer = ryu_js::Buffer::new();
555                let value = buffer.format_to_fixed(num.value, f);
556
557                self.changed = true;
558                report_change!(
559                    "evaluate: Evaluating `{}.toFixed({})` as `{}`",
560                    num,
561                    precision,
562                    value
563                );
564
565                *e = Lit::Str(Str {
566                    span: e.span(),
567                    raw: None,
568                    value: value.into(),
569                })
570                .into();
571            }
572
573            return;
574        }
575
576        if &*method.sym == "toPrecision" {
577            // TODO: handle num.toPrecision(undefined)
578            if args.is_empty() {
579                // https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.prototype.toprecision
580                // 2. If precision is undefined, return ! ToString(x).
581                let value = num.value.to_js_string().into();
582
583                self.changed = true;
584                report_change!(
585                    "evaluate: Evaluating `{}.toPrecision()` as `{}`",
586                    num,
587                    value
588                );
589
590                *e = Lit::Str(Str {
591                    span: e.span(),
592                    raw: None,
593                    value,
594                })
595                .into();
596                return;
597            }
598
599            if let Some(precision) = args
600                .first()
601                .and_then(|arg| eval_as_number(self.expr_ctx, &arg.expr))
602            {
603                let p = precision.trunc() as usize;
604                // 5. If p < 1 or p > 100, throw a RangeError exception.
605                if !(1..=21).contains(&p) {
606                    return;
607                }
608
609                let value = f64_to_precision(num.value, p);
610                self.changed = true;
611                report_change!(
612                    "evaluate: Evaluating `{}.toPrecision()` as `{}`",
613                    num,
614                    value
615                );
616                *e = Lit::Str(Str {
617                    span: e.span(),
618                    raw: None,
619                    value: value.into(),
620                })
621                .into();
622                return;
623            }
624        }
625
626        if &*method.sym == "toExponential" {
627            // TODO: handle num.toExponential(undefined)
628            if args.is_empty() {
629                let value = f64_to_exponential(num.value).into();
630
631                self.changed = true;
632                report_change!(
633                    "evaluate: Evaluating `{}.toExponential()` as `{}`",
634                    num,
635                    value
636                );
637
638                *e = Lit::Str(Str {
639                    span: e.span(),
640                    raw: None,
641                    value,
642                })
643                .into();
644                return;
645            } else if let Some(precision) = args
646                .first()
647                .and_then(|arg| eval_as_number(self.expr_ctx, &arg.expr))
648            {
649                let p = precision.trunc() as usize;
650                // 5. If p < 1 or p > 100, throw a RangeError exception.
651                if !(0..=20).contains(&p) {
652                    return;
653                }
654
655                let value = f64_to_exponential_with_precision(num.value, p).into();
656
657                self.changed = true;
658                report_change!(
659                    "evaluate: Evaluating `{}.toPrecision({})` as `{}`",
660                    num,
661                    precision,
662                    value
663                );
664
665                *e = Lit::Str(Str {
666                    span: e.span(),
667                    raw: None,
668                    value,
669                })
670                .into();
671                return;
672            }
673        }
674
675        if &*method.sym == "toString" {
676            if let Some(base) = args
677                .first()
678                .map_or(Some(10f64), |arg| eval_as_number(self.expr_ctx, &arg.expr))
679            {
680                if base.trunc() == 10. {
681                    let value = num.value.to_js_string().into();
682                    *e = Lit::Str(Str {
683                        span: e.span(),
684                        raw: None,
685                        value,
686                    })
687                    .into();
688                    return;
689                }
690
691                if num.value.fract() == 0.0 && (2.0..=36.0).contains(&base) && base.fract() == 0.0 {
692                    let base = base.floor() as u8;
693
694                    self.changed = true;
695
696                    let value = {
697                        let x = num.value;
698                        if x < 0. {
699                            // I don't know if u128 is really needed, but it works.
700                            format!("-{}", Radix::new(-x as u128, base))
701                        } else {
702                            Radix::new(x as u128, base).to_string()
703                        }
704                    }
705                    .into();
706
707                    *e = Lit::Str(Str {
708                        span: e.span(),
709                        raw: None,
710                        value,
711                    })
712                    .into()
713                }
714            }
715        }
716    }
717
718    pub(super) fn eval_opt_chain(&mut self, e: &mut Expr) {
719        let opt = match e {
720            Expr::OptChain(e) => e,
721            _ => return,
722        };
723
724        match &mut *opt.base {
725            OptChainBase::Member(MemberExpr { span, obj, .. }) => {
726                //
727                if is_pure_undefined_or_null(self.expr_ctx, obj) {
728                    self.changed = true;
729                    report_change!(
730                        "evaluate: Reduced an optional chaining operation because object is \
731                         always null or undefined"
732                    );
733
734                    *e = *Expr::undefined(*span);
735                }
736            }
737
738            OptChainBase::Call(OptCall { span, callee, .. }) => {
739                if is_pure_undefined_or_null(self.expr_ctx, callee) {
740                    self.changed = true;
741                    report_change!(
742                        "evaluate: Reduced a call expression with optional chaining operation \
743                         because object is always null or undefined"
744                    );
745
746                    *e = *Expr::undefined(*span);
747                }
748            }
749            #[cfg(swc_ast_unknown)]
750            _ => panic!("unable to access unknown nodes"),
751        }
752    }
753
754    pub(super) fn eval_trivial_values_in_expr(&mut self, seq: &mut SeqExpr) {
755        if seq.exprs.len() < 2 {
756            return;
757        }
758
759        'outer: for idx in 0..seq.exprs.len() {
760            let (a, b) = seq.exprs.split_at_mut(idx);
761
762            for a in a.iter().rev() {
763                if let Some(b) = b.first_mut() {
764                    self.eval_trivial_two(a, b);
765
766                    match &**b {
767                        Expr::Ident(..) | Expr::Lit(..) => {}
768                        _ => break 'outer,
769                    }
770                }
771            }
772        }
773    }
774
775    pub(super) fn eval_member_expr(&mut self, e: &mut Expr) {
776        if self.ctx.contains(Ctx::IN_OPT_CHAIN) {
777            return;
778        }
779
780        let member_expr = match e {
781            Expr::Member(x) => x,
782            _ => return,
783        };
784
785        if let Some(replacement) =
786            self.optimize_member_expr(&mut member_expr.obj, &member_expr.prop)
787        {
788            *e = replacement;
789            self.changed = true;
790            report_change!(
791                "member_expr: Optimized member expression as {}",
792                dump(&*e, false)
793            );
794        }
795    }
796
797    fn eval_trivial_two(&mut self, a: &Expr, b: &mut Expr) {
798        if let Expr::Assign(AssignExpr {
799            left: a_left,
800            op: op!("="),
801            right: a_right,
802            ..
803        }) = a
804        {
805            match &**a_right {
806                Expr::Lit(..) => {}
807                _ => return,
808            }
809
810            if let AssignTarget::Simple(SimpleAssignTarget::Ident(a_left)) = a_left {
811                if let Expr::Ident(b_id) = b {
812                    if b_id.ctxt == a_left.id.ctxt && b_id.sym == a_left.id.sym {
813                        report_change!("evaluate: Trivial: `{}`", a_left.id);
814                        *b = *a_right.clone();
815                        self.changed = true;
816                    }
817                }
818            }
819        }
820    }
821}
822
823/// Evaluation of strings.
824impl Pure<'_> {
825    /// Handle calls on string literals, like `'foo'.toUpperCase()`.
826    pub(super) fn eval_str_method_call(&mut self, e: &mut Expr) {
827        if !self.options.evaluate {
828            return;
829        }
830
831        if self.ctx.intersects(
832            Ctx::IN_DELETE
833                .union(Ctx::IS_UPDATE_ARG)
834                .union(Ctx::IS_LHS_OF_ASSIGN),
835        ) {
836            return;
837        }
838
839        let call = match e {
840            Expr::Call(v) => v,
841            _ => return,
842        };
843
844        let (s, method) = match &call.callee {
845            Callee::Super(_) | Callee::Import(_) => return,
846            Callee::Expr(callee) => match &**callee {
847                Expr::Member(MemberExpr {
848                    obj,
849                    prop: MemberProp::Ident(prop),
850                    ..
851                }) => match &**obj {
852                    Expr::Lit(Lit::Str(s)) => (s.clone(), prop.sym.clone()),
853                    _ => return,
854                },
855                _ => return,
856            },
857            #[cfg(swc_ast_unknown)]
858            _ => panic!("unable to access unknown nodes"),
859        };
860
861        let new_val = match &*method {
862            "toLowerCase" => s.value.to_lowercase(),
863            "toUpperCase" => s.value.to_uppercase(),
864            "charCodeAt" => {
865                if call.args.len() != 1 {
866                    return;
867                }
868                if let Expr::Lit(Lit::Num(Number { value, .. })) = &*call.args[0].expr {
869                    if value.fract() != 0.0 {
870                        return;
871                    }
872
873                    let idx = value.round() as i64 as usize;
874                    let c = s.value.to_ill_formed_utf16().nth(idx);
875
876                    match c {
877                        Some(v) => {
878                            self.changed = true;
879                            report_change!(
880                                "evaluate: Evaluated `charCodeAt` of a string literal as `{}`",
881                                v
882                            );
883                            *e = Lit::Num(Number {
884                                span: call.span,
885                                value: v as usize as f64,
886                                raw: None,
887                            })
888                            .into()
889                        }
890                        None => {
891                            self.changed = true;
892                            report_change!(
893                                "evaluate: Evaluated `charCodeAt` of a string literal as `NaN`",
894                            );
895                            *e = Ident::new(atom!("NaN"), e.span(), SyntaxContext::empty()).into()
896                        }
897                    }
898                }
899                return;
900            }
901            "codePointAt" => {
902                if call.args.len() != 1 {
903                    return;
904                }
905                if let Expr::Lit(Lit::Num(Number { value, .. })) = &*call.args[0].expr {
906                    if value.fract() != 0.0 {
907                        return;
908                    }
909
910                    let idx = value.round() as i64 as usize;
911                    let mut c = s.value.to_ill_formed_utf16().skip(idx).peekable();
912                    match c.next() {
913                        Some(v) => {
914                            match (v, c.peek()) {
915                                (high, Some(&low))
916                                    if is_high_surrogate(high as u32)
917                                        && is_low_surrogate(low as u32) =>
918                                {
919                                    // Decode surrogate pair
920                                    let code_point = swc_ecma_utils::unicode::pair_to_code_point(
921                                        high as u32,
922                                        low as u32,
923                                    );
924                                    self.changed = true;
925                                    report_change!(
926                                        "evaluate: Evaluated `codePointAt` of a string literal as \
927                                         `{}`",
928                                        code_point
929                                    );
930                                    *e = Lit::Num(Number {
931                                        span: call.span,
932                                        value: code_point as f64,
933                                        raw: None,
934                                    })
935                                    .into();
936                                    return;
937                                }
938                                _ => {
939                                    // Not a surrogate pair
940                                    self.changed = true;
941                                    report_change!(
942                                        "evaluate: Evaluated `codePointAt` of a string literal as \
943                                         `{}`",
944                                        v
945                                    );
946                                    *e = Lit::Num(Number {
947                                        span: call.span,
948                                        value: v as usize as f64,
949                                        raw: None,
950                                    })
951                                    .into()
952                                }
953                            }
954                        }
955                        None => {
956                            self.changed = true;
957                            report_change!(
958                                "evaluate: Evaluated `codePointAt` of a string literal as `NaN`",
959                            );
960                            *e = Ident::new(
961                                atom!("NaN"),
962                                e.span(),
963                                SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
964                            )
965                            .into()
966                        }
967                    }
968                }
969                return;
970            }
971            _ => return,
972        };
973
974        self.changed = true;
975        report_change!("evaluate: Evaluated `{method}` of a string literal");
976        *e = Lit::Str(Str {
977            value: new_val.into(),
978            raw: None,
979            ..s
980        })
981        .into();
982    }
983}
984
985// Code from boa
986// https://github.com/boa-dev/boa/blob/f8b682085d7fe0bbfcd0333038e93cf2f5aee710/boa_engine/src/builtins/number/mod.rs#L408
987fn f64_to_precision(value: f64, precision: usize) -> String {
988    let mut x = value;
989    let p_i32 = precision as i32;
990
991    // 7. Let s be the empty String.
992    let mut s = String::new();
993    let mut m: String;
994    let mut e: i32;
995
996    // 8. If x < 0, then a. Set s to the code unit 0x002D (HYPHEN-MINUS). b. Set x
997    //    to -x.
998    if x < 0. {
999        s.push('-');
1000        x = -x;
1001    }
1002
1003    // 9. If x = 0, then a. Let m be the String value consisting of p occurrences of
1004    //    the code unit 0x0030 (DIGIT ZERO). b. Let e be 0.
1005    if x == 0.0 {
1006        m = "0".repeat(precision);
1007        e = 0;
1008    // 10
1009    } else {
1010        // Due to f64 limitations, this part differs a bit from the spec,
1011        // but has the same effect. It manipulates the string constructed
1012        // by `format`: digits with an optional dot between two of them.
1013        m = format!("{x:.100}");
1014
1015        // a: getting an exponent
1016        e = flt_str_to_exp(&m);
1017        // b: getting relevant digits only
1018        if e < 0 {
1019            m = m.split_off((1 - e) as usize);
1020        } else if let Some(n) = m.find('.') {
1021            m.remove(n);
1022        }
1023        // impl: having exactly `precision` digits in `suffix`
1024        if round_to_precision(&mut m, precision) {
1025            e += 1;
1026        }
1027
1028        // c: switching to scientific notation
1029        let great_exp = e >= p_i32;
1030        if e < -6 || great_exp {
1031            assert_ne!(e, 0);
1032
1033            // ii
1034            if precision > 1 {
1035                m.insert(1, '.');
1036            }
1037            // vi
1038            m.push('e');
1039            // iii
1040            if great_exp {
1041                m.push('+');
1042            }
1043            // iv, v
1044            m.push_str(&e.to_string());
1045
1046            return s + &*m;
1047        }
1048    }
1049
1050    // 11
1051    let e_inc = e + 1;
1052    if e_inc == p_i32 {
1053        return s + &*m;
1054    }
1055
1056    // 12
1057    if e >= 0 {
1058        m.insert(e_inc as usize, '.');
1059    // 13
1060    } else {
1061        s.push('0');
1062        s.push('.');
1063        s.push_str(&"0".repeat(-e_inc as usize));
1064    }
1065
1066    // 14
1067    s + &*m
1068}
1069
1070fn flt_str_to_exp(flt: &str) -> i32 {
1071    let mut non_zero_encountered = false;
1072    let mut dot_encountered = false;
1073    for (i, c) in flt.chars().enumerate() {
1074        if c == '.' {
1075            if non_zero_encountered {
1076                return (i as i32) - 1;
1077            }
1078            dot_encountered = true;
1079        } else if c != '0' {
1080            if dot_encountered {
1081                return 1 - (i as i32);
1082            }
1083            non_zero_encountered = true;
1084        }
1085    }
1086    (flt.len() as i32) - 1
1087}
1088
1089fn round_to_precision(digits: &mut String, precision: usize) -> bool {
1090    if digits.len() > precision {
1091        let to_round = digits.split_off(precision);
1092        let mut digit = digits
1093            .pop()
1094            .expect("already checked that length is bigger than precision")
1095            as u8;
1096        if let Some(first) = to_round.chars().next() {
1097            if first > '4' {
1098                digit += 1;
1099            }
1100        }
1101
1102        if digit as char == ':' {
1103            // ':' is '9' + 1
1104            // need to propagate the increment backward
1105            let mut replacement = String::from("0");
1106            let mut propagated = false;
1107            for c in digits.chars().rev() {
1108                let d = match (c, propagated) {
1109                    ('0'..='8', false) => (c as u8 + 1) as char,
1110                    (_, false) => '0',
1111                    (_, true) => c,
1112                };
1113                replacement.push(d);
1114                if d != '0' {
1115                    propagated = true;
1116                }
1117            }
1118            digits.clear();
1119            let replacement = if propagated {
1120                replacement.as_str()
1121            } else {
1122                digits.push('1');
1123                &replacement.as_str()[1..]
1124            };
1125            for c in replacement.chars().rev() {
1126                digits.push(c);
1127            }
1128            !propagated
1129        } else {
1130            digits.push(digit as char);
1131            false
1132        }
1133    } else {
1134        digits.push_str(&"0".repeat(precision - digits.len()));
1135        false
1136    }
1137}
1138
1139/// Helper function that formats a float as a ES6-style exponential number
1140/// string.
1141fn f64_to_exponential(n: f64) -> String {
1142    match n.abs() {
1143        x if x >= 1.0 || x == 0.0 => format!("{n:e}").replace('e', "e+"),
1144        _ => format!("{n:e}"),
1145    }
1146}
1147
1148/// Helper function that formats a float as a ES6-style exponential number
1149/// string with a given precision.
1150// We can't use the same approach as in `f64_to_exponential`
1151// because in cases like (0.999).toExponential(0) the result will be 1e0.
1152// Instead we get the index of 'e', and if the next character is not '-'
1153// we insert the plus sign
1154fn f64_to_exponential_with_precision(n: f64, prec: usize) -> String {
1155    let mut res = format!("{n:.prec$e}");
1156    let idx = res.find('e').expect("'e' not found in exponential string");
1157    if res.as_bytes()[idx + 1] != b'-' {
1158        res.insert(idx + 1, '+');
1159    }
1160    res
1161}