swc_ecma_minifier/compress/optimize/
evaluate.rs

1use std::num::FpCategory;
2
3use swc_atoms::atom;
4use swc_common::{util::take::Take, Spanned, SyntaxContext, DUMMY_SP};
5use swc_ecma_ast::*;
6use swc_ecma_utils::{ExprExt, Value::Known};
7
8use super::{BitCtx, Optimizer};
9use crate::{
10    compress::util::eval_as_number, program_data::VarUsageInfoFlags, DISABLE_BUGGY_PASSES,
11};
12
13/// Methods related to the option `evaluate`.
14impl Optimizer<'_> {
15    /// Evaluate expression if possible.
16    ///
17    /// This method call appropriate methods for each ast types.
18    pub(super) fn evaluate(&mut self, e: &mut Expr) {
19        self.eval_global_vars(e);
20
21        self.eval_fn_props(e);
22
23        self.eval_numbers(e);
24
25        self.eval_known_static_method_call(e);
26    }
27
28    fn eval_fn_props(&mut self, e: &mut Expr) -> Option<()> {
29        if self
30            .ctx
31            .bit_ctx
32            .intersects(BitCtx::IsDeleteArg | BitCtx::IsUpdateArg | BitCtx::IsLhsOfAssign)
33        {
34            return None;
35        }
36
37        if let Expr::Member(MemberExpr {
38            span,
39            obj,
40            prop: MemberProp::Ident(prop),
41            ..
42        }) = e
43        {
44            if let Expr::Ident(obj) = &**obj {
45                let metadata = *self.functions.get(&obj.to_id())?;
46
47                let usage = self.data.vars.get(&obj.to_id())?;
48
49                if usage.flags.contains(VarUsageInfoFlags::REASSIGNED) {
50                    return None;
51                }
52
53                if self.options.unsafe_passes {
54                    match &*prop.sym {
55                        "length" => {
56                            report_change!("evaluate: function.length");
57
58                            *e = Lit::Num(Number {
59                                span: *span,
60                                value: metadata.len as _,
61                                raw: None,
62                            })
63                            .into();
64                            self.changed = true;
65                        }
66
67                        "name" => {
68                            report_change!("evaluate: function.name");
69
70                            *e = Lit::Str(Str {
71                                span: *span,
72                                value: obj.sym.clone().into(),
73                                raw: None,
74                            })
75                            .into();
76                            self.changed = true;
77                        }
78
79                        _ => {}
80                    }
81                }
82            }
83        }
84
85        None
86    }
87
88    fn eval_global_vars(&mut self, e: &mut Expr) {
89        if self.options.ie8 {
90            return;
91        }
92
93        if self.ctx.bit_ctx.intersects(
94            BitCtx::IsDeleteArg | BitCtx::IsUpdateArg | BitCtx::IsLhsOfAssign | BitCtx::InWithStmt,
95        ) {
96            return;
97        }
98
99        // We should not convert used-defined `undefined` to `void 0`.
100        if let Expr::Ident(i) = e {
101            if self
102                .data
103                .vars
104                .get(&i.to_id())
105                .map(|var| var.flags.contains(VarUsageInfoFlags::DECLARED))
106                .unwrap_or(false)
107            {
108                return;
109            }
110        }
111
112        match e {
113            Expr::Ident(Ident { span, sym, .. }) if &**sym == "undefined" => {
114                report_change!("evaluate: `undefined` -> `void 0`");
115                self.changed = true;
116                *e = *Expr::undefined(*span);
117            }
118
119            Expr::Ident(Ident { span, sym, .. }) if &**sym == "Infinity" => {
120                report_change!("evaluate: `Infinity` -> `1 / 0`");
121                self.changed = true;
122                *e = BinExpr {
123                    span: *span,
124                    op: op!("/"),
125                    left: Lit::Num(Number {
126                        span: DUMMY_SP,
127                        value: 1.0,
128                        raw: None,
129                    })
130                    .into(),
131                    right: Lit::Num(Number {
132                        span: DUMMY_SP,
133                        value: 0.0,
134                        raw: None,
135                    })
136                    .into(),
137                }
138                .into();
139            }
140
141            Expr::Member(MemberExpr {
142                obj,
143                prop: MemberProp::Ident(prop),
144                span,
145                ..
146            }) if matches!(obj.as_ref(), Expr::Ident(ident) if &*ident.sym == "Number") => {
147                if let Expr::Ident(number_ident) = &**obj {
148                    if number_ident.ctxt != self.ctx.expr_ctx.unresolved_ctxt {
149                        return;
150                    }
151                }
152
153                match &*prop.sym {
154                    "MIN_VALUE" => {
155                        report_change!("evaluate: `Number.MIN_VALUE` -> `5e-324`");
156                        self.changed = true;
157                        *e = Lit::Num(Number {
158                            span: *span,
159                            value: 5e-324,
160                            raw: None,
161                        })
162                        .into();
163                    }
164                    "NaN" => {
165                        report_change!("evaluate: `Number.NaN` -> `NaN`");
166                        self.changed = true;
167                        *e = Ident::new(
168                            atom!("NaN"),
169                            *span,
170                            SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
171                        )
172                        .into();
173                    }
174                    "POSITIVE_INFINITY" => {
175                        report_change!("evaluate: `Number.POSITIVE_INFINITY` -> `Infinity`");
176                        self.changed = true;
177                        *e = Ident::new(
178                            atom!("Infinity"),
179                            *span,
180                            SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
181                        )
182                        .into();
183                    }
184                    "NEGATIVE_INFINITY" => {
185                        report_change!("evaluate: `Number.NEGATIVE_INFINITY` -> `-Infinity`");
186                        self.changed = true;
187                        *e = UnaryExpr {
188                            span: *span,
189                            op: op!(unary, "-"),
190                            arg: Ident::new(
191                                atom!("Infinity"),
192                                *span,
193                                SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
194                            )
195                            .into(),
196                        }
197                        .into();
198                    }
199                    _ => {}
200                }
201            }
202
203            _ => {}
204        }
205    }
206
207    /// Handle calls on some static classes.
208    /// e.g. `String.fromCharCode`, `Object.keys()`
209    fn eval_known_static_method_call(&mut self, e: &mut Expr) {
210        if !self.options.evaluate {
211            return;
212        }
213
214        if self
215            .ctx
216            .bit_ctx
217            .intersects(BitCtx::IsDeleteArg | BitCtx::IsUpdateArg | BitCtx::IsLhsOfAssign)
218        {
219            return;
220        }
221
222        let (span, callee, args) = match e {
223            Expr::Call(CallExpr {
224                span,
225                callee: Callee::Expr(callee),
226                args,
227                ..
228            }) => (span, callee, args),
229            _ => return,
230        };
231        let span = *span;
232
233        //
234
235        for arg in &*args {
236            if arg.spread.is_some() || arg.expr.may_have_side_effects(self.ctx.expr_ctx) {
237                return;
238            }
239        }
240
241        match &**callee {
242            Expr::Ident(Ident { sym, .. }) if &**sym == "RegExp" && self.options.unsafe_regexp => {
243                if !args.is_empty() {
244                    self.optimize_expr_in_str_ctx(&mut args[0].expr);
245                }
246                if args.len() >= 2 {
247                    self.optimize_expr_in_str_ctx(&mut args[1].expr);
248                }
249
250                // Disable
251                if DISABLE_BUGGY_PASSES {
252                    return;
253                }
254
255                match args.len() {
256                    0 => {}
257                    1 => {
258                        if let Expr::Lit(Lit::Str(exp)) = &*args[0].expr {
259                            let Some(value) = exp.value.as_str() else {
260                                return;
261                            };
262                            self.changed = true;
263                            report_change!(
264                                "evaluate: Converting RegExpr call into a regexp literal `/{}/`",
265                                exp.value
266                            );
267
268                            *e = Lit::Regex(Regex {
269                                span,
270                                exp: value.into(),
271                                flags: atom!(""),
272                            })
273                            .into();
274                        }
275                    }
276                    _ => {
277                        if let (Expr::Lit(Lit::Str(exp)), Expr::Lit(Lit::Str(flags))) =
278                            (&*args[0].expr, &*args[1].expr)
279                        {
280                            let Some(value) = exp.value.as_str() else {
281                                return;
282                            };
283                            let Some(flags) = flags.value.as_str() else {
284                                return;
285                            };
286
287                            self.changed = true;
288                            report_change!(
289                                "evaluate: Converting RegExpr call into a regexp literal `/{}/{}`",
290                                exp.value,
291                                flags.value
292                            );
293
294                            *e = Lit::Regex(Regex {
295                                span,
296                                exp: value.into(),
297                                flags: flags.into(),
298                            })
299                            .into();
300                        }
301                    }
302                }
303            }
304
305            Expr::Member(MemberExpr {
306                obj,
307                prop: MemberProp::Ident(prop),
308                ..
309            }) => match &**obj {
310                Expr::Ident(Ident { sym, .. }) if &**sym == "String" => {
311                    if &*prop.sym == "fromCharCode" {
312                        if args.len() != 1 {
313                            return;
314                        }
315
316                        if let Known(char_code) = args[0].expr.as_pure_number(self.ctx.expr_ctx) {
317                            let v = char_code.floor() as u32;
318
319                            if let Some(v) = char::from_u32(v) {
320                                if !v.is_ascii() {
321                                    return;
322                                }
323                                self.changed = true;
324                                report_change!(
325                                    "evaluate: Evaluated `String.charCodeAt({})` as `{}`",
326                                    char_code,
327                                    v
328                                );
329
330                                let value = v.to_string();
331
332                                *e = Lit::Str(Str {
333                                    span: e.span(),
334                                    raw: None,
335                                    value: value.into(),
336                                })
337                                .into();
338                            }
339                        }
340                    }
341                }
342
343                Expr::Ident(Ident { sym, .. }) if &**sym == "Object" => {
344                    if &*prop.sym == "keys" {
345                        if args.len() != 1 {
346                            return;
347                        }
348
349                        let obj = match &*args[0].expr {
350                            Expr::Object(obj) => obj,
351                            _ => return,
352                        };
353
354                        let mut keys = Vec::new();
355
356                        for prop in &obj.props {
357                            match prop {
358                                PropOrSpread::Spread(_) => return,
359                                PropOrSpread::Prop(p) => match &**p {
360                                    Prop::Shorthand(p) => {
361                                        keys.push(Some(ExprOrSpread {
362                                            spread: None,
363                                            expr: Lit::Str(Str {
364                                                span: p.span,
365                                                raw: None,
366                                                value: p.sym.clone().into(),
367                                            })
368                                            .into(),
369                                        }));
370                                    }
371                                    Prop::KeyValue(p) => match &p.key {
372                                        PropName::Ident(key) => {
373                                            keys.push(Some(ExprOrSpread {
374                                                spread: None,
375                                                expr: Lit::Str(Str {
376                                                    span: key.span,
377                                                    raw: None,
378                                                    value: key.sym.clone().into(),
379                                                })
380                                                .into(),
381                                            }));
382                                        }
383                                        PropName::Str(key) => {
384                                            keys.push(Some(ExprOrSpread {
385                                                spread: None,
386                                                expr: Lit::Str(key.clone()).into(),
387                                            }));
388                                        }
389                                        _ => return,
390                                    },
391                                    _ => return,
392                                },
393                                #[cfg(swc_ast_unknown)]
394                                _ => panic!("unable to access unknown nodes"),
395                            }
396                        }
397
398                        *e = ArrayLit { span, elems: keys }.into()
399                    }
400                }
401
402                Expr::Ident(Ident { sym, .. }) => {
403                    if &**sym == "console" && &*prop.sym == "log" {
404                        for arg in args {
405                            self.optimize_expr_in_str_ctx_unsafely(&mut arg.expr);
406                        }
407                    }
408                }
409
410                _ => {}
411            },
412            _ => {}
413        }
414    }
415
416    fn eval_numbers(&mut self, e: &mut Expr) {
417        if !self.options.evaluate {
418            return;
419        }
420
421        if self
422            .ctx
423            .bit_ctx
424            .intersects(BitCtx::IsDeleteArg | BitCtx::IsUpdateArg | BitCtx::IsLhsOfAssign)
425        {
426            return;
427        }
428
429        if let Expr::Call(..) = e {
430            if let Some(value) = eval_as_number(self.ctx.expr_ctx, e) {
431                self.changed = true;
432                report_change!("evaluate: Evaluated an expression as `{}`", value);
433
434                if value.is_nan() {
435                    *e = Ident::new(
436                        atom!("NaN"),
437                        e.span(),
438                        SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
439                    )
440                    .into();
441                    return;
442                }
443
444                *e = Lit::Num(Number {
445                    span: e.span(),
446                    value,
447                    raw: None,
448                })
449                .into();
450                return;
451            }
452        }
453
454        match e {
455            Expr::Bin(bin @ BinExpr { op: op!("**"), .. }) => {
456                let l = bin.left.as_pure_number(self.ctx.expr_ctx);
457                let r = bin.right.as_pure_number(self.ctx.expr_ctx);
458
459                if let Known(l) = l {
460                    if let Known(r) = r {
461                        self.changed = true;
462                        report_change!("evaluate: Evaluated `{:?} ** {:?}`", l, r);
463
464                        if l.is_nan() || r.is_nan() {
465                            *e = Ident::new(
466                                atom!("NaN"),
467                                bin.span,
468                                SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
469                            )
470                            .into();
471                        } else {
472                            *e = Lit::Num(Number {
473                                span: bin.span,
474                                value: l.powf(r),
475                                raw: None,
476                            })
477                            .into();
478                        };
479                    }
480                }
481            }
482
483            Expr::Bin(bin @ BinExpr { op: op!("/"), .. }) => {
484                let ln = bin.left.as_pure_number(self.ctx.expr_ctx);
485
486                let rn = bin.right.as_pure_number(self.ctx.expr_ctx);
487                if let (Known(ln), Known(rn)) = (ln, rn) {
488                    // Prefer `0/0` over NaN.
489                    if ln == 0.0 && rn == 0.0 {
490                        return;
491                    }
492                    // Prefer `1/0` over Infinity.
493                    if ln == 1.0 && rn == 0.0 {
494                        return;
495                    }
496
497                    // It's NaN
498                    if let (FpCategory::Normal, FpCategory::Zero) = (ln.classify(), rn.classify()) {
499                        self.changed = true;
500                        report_change!("evaluate: `{} / 0` => `Infinity`", ln);
501
502                        // Sign does not matter for NaN
503                        *e = if ln.is_sign_positive() == rn.is_sign_positive() {
504                            Ident::new_no_ctxt(atom!("Infinity"), bin.span).into()
505                        } else {
506                            UnaryExpr {
507                                span: bin.span,
508                                op: op!(unary, "-"),
509                                arg: Ident::new_no_ctxt(atom!("Infinity"), bin.span).into(),
510                            }
511                            .into()
512                        };
513                    }
514                }
515            }
516
517            _ => {}
518        }
519    }
520
521    ///
522    /// - `Object(1) && 1 && 2` => `Object(1) && 2`.
523    pub(super) fn optimize_bin_and_or(&mut self, e: &mut BinExpr) {
524        if !self.options.evaluate {
525            return;
526        }
527        if e.left.is_invalid() || e.right.is_invalid() {
528            return;
529        }
530
531        match e.op {
532            op!("&&") | op!("||") => {}
533            _ => return,
534        }
535
536        if let Expr::Bin(left) = &mut *e.left {
537            if left.op != e.op {
538                return;
539            }
540            // Remove rhs of lhs if possible.
541
542            let v = left.right.as_pure_bool(self.ctx.expr_ctx);
543            if let Known(v) = v {
544                // As we used as_pure_bool, we can drop it.
545                if v && e.op == op!("&&") {
546                    self.changed = true;
547                    report_change!("Removing `b` from `a && b && c` because b is always truthy");
548
549                    left.right.take();
550                    return;
551                }
552
553                if !v && e.op == op!("||") {
554                    self.changed = true;
555                    report_change!("Removing `b` from `a || b || c` because b is always falsy");
556
557                    left.right.take();
558                }
559            }
560        }
561    }
562}