swc_ecma_minifier/compress/optimize/
inline.rs

1use std::ops::Deref;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use swc_atoms::atom;
5use swc_common::{util::take::Take, EqIgnoreSpan, Mark};
6use swc_ecma_ast::*;
7use swc_ecma_usage_analyzer::alias::{collect_infects_from, AliasConfig};
8use swc_ecma_utils::{
9    class_has_side_effect, collect_decls, contains_this_expr, find_pat_ids, ExprExt, Remapper,
10};
11use swc_ecma_visit::VisitMutWith;
12
13use super::Optimizer;
14use crate::{
15    compress::{
16        optimize::{util::is_valid_for_lhs, BitCtx},
17        util::contains_super,
18    },
19    program_data::{ScopeData, VarUsageInfo, VarUsageInfoFlags},
20    util::{
21        idents_captured_by, idents_used_by, idents_used_by_ignoring_nested, size::SizeWithCtxt,
22    },
23};
24
25/// Methods related to option `inline`.
26impl Optimizer<'_> {
27    /// Stores the value of a variable to inline it.
28    ///
29    /// This method may remove value of initializer. It mean that the value will
30    /// be inlined and should be removed from [Vec<VarDeclarator>].
31    pub(super) fn store_var_for_inlining(
32        &mut self,
33        ident: &mut Ident,
34        init: &mut Expr,
35        can_drop: bool,
36    ) {
37        trace_op!(
38            "inline: store_var_for_inlining({}, may_remove = {:?})",
39            crate::debug::dump(ident, false),
40            self.may_remove_ident(ident)
41        );
42
43        if self.data.top.contains(ScopeData::HAS_EVAL_CALL) {
44            return;
45        }
46
47        // We will inline if possible.
48        if ident.sym == "arguments" {
49            return;
50        }
51
52        if let Expr::Arrow(ArrowExpr { body, .. }) = init {
53            if contains_super(body) {
54                return;
55            }
56            if contains_this_expr(body) {
57                return;
58            }
59        }
60
61        if let Some(usage) = self.data.vars.get(&ident.to_id()) {
62            let ref_count = usage.ref_count - u32::from(can_drop && usage.ref_count > 1);
63            if !usage.flags.contains(VarUsageInfoFlags::VAR_INITIALIZED) {
64                return;
65            }
66
67            if self.data.top.contains(ScopeData::USED_ARGUMENTS)
68                && usage
69                    .flags
70                    .contains(VarUsageInfoFlags::DECLARED_AS_FN_PARAM)
71            {
72                return;
73            }
74            if usage
75                .flags
76                .contains(VarUsageInfoFlags::DECLARED_AS_CATCH_PARAM)
77            {
78                return;
79            }
80            if usage.flags.contains(VarUsageInfoFlags::INLINE_PREVENTED) {
81                return;
82            }
83
84            let may_remove = self.may_remove_ident(ident);
85
86            if !may_remove && usage.var_kind != Some(VarDeclKind::Const) {
87                log_abort!(
88                    "inline: [x] Preserving non-const variable `{}` because it's top-level",
89                    crate::debug::dump(ident, false)
90                );
91                return;
92            }
93
94            if usage.flags.contains(VarUsageInfoFlags::USED_ABOVE_DECL) {
95                log_abort!("inline: [x] It's cond init or used before decl",);
96                return;
97            }
98
99            // No use => dropped
100            if ref_count == 0 {
101                self.mode.store(ident.to_id(), &*init);
102
103                if init.may_have_side_effects(self.ctx.expr_ctx) {
104                    // TODO: Inline partially
105                    return;
106                }
107
108                // TODO: Remove
109                return;
110            }
111
112            let is_inline_enabled =
113                self.options.reduce_vars || self.options.collapse_vars || self.options.inline != 0;
114
115            let mut inlined_into_init = false;
116
117            let id = ident.to_id();
118
119            // We inline arrays partially if it's pure (all elements are literal), and not
120            // modified.
121            // We don't drop definition, but we just inline array accesses with numeric
122            // literal key.
123            //
124            // TODO: Allow `length` in usage.accessed_props
125            if usage.flags.contains(VarUsageInfoFlags::DECLARED)
126                && !usage.mutated()
127                && usage.accessed_props.is_empty()
128                && !usage.flags.intersects(
129                    VarUsageInfoFlags::INDEXED_WITH_DYNAMIC_KEY
130                        .union(VarUsageInfoFlags::USED_AS_REF),
131                )
132                && !usage.is_infected()
133                && is_inline_enabled
134            {
135                if let Expr::Array(arr) = init {
136                    if arr.elems.len() < 32
137                        && arr.elems.iter().all(|e| match e {
138                            Some(ExprOrSpread { spread: None, expr }) => match &**expr {
139                                Expr::Lit(..) => true,
140                                _ => false,
141                            },
142                            _ => false,
143                        })
144                    {
145                        inlined_into_init = true;
146                        self.vars.inline_with_multi_replacer(arr);
147                        report_change!(
148                            "inline: Decided to store '{}{:?}' for array access",
149                            ident.sym,
150                            ident.ctxt
151                        );
152                        self.vars
153                            .lits_for_array_access
154                            .insert(ident.to_id(), Box::new(init.clone()));
155                    }
156                }
157            }
158
159            if !usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL) {
160                match init {
161                    Expr::Lit(..) | Expr::Ident(..) | Expr::Arrow(..) => {}
162
163                    Expr::Unary(UnaryExpr {
164                        op: op!("!"), arg, ..
165                    }) if matches!(&**arg, Expr::Lit(..)) => {}
166
167                    Expr::Fn(FnExpr { function, .. })
168                        if matches!(&**function, Function { body: Some(..), .. }) =>
169                    {
170                        if function.body.as_ref().unwrap().stmts.len() == 1
171                            && matches!(&function.body.as_ref().unwrap().stmts[0], Stmt::Return(..))
172                        {
173                        } else {
174                            log_abort!("inline: [x] It's not fn-local");
175                            return;
176                        }
177                    }
178                    _ => {
179                        log_abort!("inline: [x] It's not fn-local");
180                        return;
181                    }
182                }
183            }
184
185            if !usage.flags.contains(VarUsageInfoFlags::REASSIGNED) {
186                match init {
187                    Expr::Fn(..) | Expr::Arrow(..) | Expr::Class(..) => {
188                        self.typeofs.insert(ident.to_id(), atom!("function"));
189                    }
190                    Expr::Array(..) | Expr::Object(..) => {
191                        self.typeofs.insert(ident.to_id(), atom!("object"));
192                    }
193                    _ => {}
194                }
195            }
196
197            if !usage.mutated() {
198                self.mode.store(ident.to_id(), &*init);
199            }
200
201            if usage.flags.contains(VarUsageInfoFlags::USED_RECURSIVELY) {
202                return;
203            }
204
205            // Caution: for most case mutation of properties are ok, however if
206            // new variant is added for multi inline, think carefully
207            if is_inline_enabled
208                && usage.declared_count == 1
209                && usage.assign_count == 1
210                && !usage.flags.contains(VarUsageInfoFlags::REASSIGNED)
211                && (usage.property_mutation_count == 0
212                    || !usage.flags.contains(VarUsageInfoFlags::REASSIGNED))
213                && match init {
214                    Expr::Ident(Ident { sym, .. }) if &**sym == "eval" => false,
215
216                    Expr::Ident(id) if !id.eq_ignore_span(ident) => {
217                        if !usage.flags.contains(VarUsageInfoFlags::ASSIGNED_FN_LOCAL) {
218                            false
219                        } else if let Some(u) = self.data.vars.get(&id.to_id()) {
220                            let mut should_inline =
221                                !u.flags.contains(VarUsageInfoFlags::REASSIGNED)
222                                    && u.flags.contains(VarUsageInfoFlags::DECLARED);
223
224                            should_inline &=
225                                // Function declarations are hoisted
226                                //
227                                // As we copy expressions, this can cause a problem.
228                                // See https://github.com/swc-project/swc/issues/6463
229                                //
230                                // We check callee_count of `usage` because we copy simple functions
231                                !u.flags.contains(VarUsageInfoFlags::USED_ABOVE_DECL)
232                                    || !u.flags.contains(VarUsageInfoFlags::DECLARED_AS_FN_DECL)
233                                    || usage.callee_count == 0;
234
235                            if u.flags.contains(VarUsageInfoFlags::DECLARED_AS_FOR_INIT)
236                                && !usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL)
237                            {
238                                should_inline &= !matches!(
239                                    u.var_kind,
240                                    Some(VarDeclKind::Let | VarDeclKind::Const)
241                                )
242                            }
243
244                            if u.flags.intersects(
245                                VarUsageInfoFlags::DECLARED_AS_FN_DECL
246                                    .union(VarUsageInfoFlags::DECLARED_AS_FN_EXPR),
247                            ) {
248                                if self.options.keep_fnames
249                                    || self.mangle_options.is_some_and(|v| v.keep_fn_names)
250                                {
251                                    should_inline = false
252                                }
253                            }
254
255                            if u.flags.contains(VarUsageInfoFlags::DECLARED_AS_FN_EXPR) {
256                                if self.options.inline != 3 {
257                                    return;
258                                }
259                            }
260
261                            should_inline
262                        } else {
263                            false
264                        }
265                    }
266
267                    Expr::Lit(lit) => match lit {
268                        Lit::Str(s) => {
269                            // If the string literal is declared without initializer and assigned
270                            // once, we can inline it.
271                            if (ref_count == 1
272                                || (ref_count == 2
273                                    && usage.assign_count == 1
274                                    && usage.flags.intersects(VarUsageInfoFlags::LAZY_INIT)))
275                                || s.value.len() <= 3
276                            {
277                                true
278                            } else {
279                                self.vars
280                                    .lits_for_cmp
281                                    .insert(ident.to_id(), init.clone().into());
282                                false
283                            }
284                        }
285                        Lit::Bool(_) | Lit::Null(_) | Lit::Num(_) | Lit::BigInt(_) => true,
286                        Lit::Regex(_) => self.options.unsafe_regexp,
287                        _ => false,
288                    },
289                    Expr::Unary(UnaryExpr {
290                        op: op!("!"), arg, ..
291                    }) => arg.is_lit(),
292                    Expr::This(..) => usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL),
293                    Expr::Arrow(arr) => {
294                        !(usage.property_mutation_count > 0
295                            || usage
296                                .flags
297                                .contains(VarUsageInfoFlags::EXECUTED_MULTIPLE_TIME)
298                            || usage.flags.contains(VarUsageInfoFlags::USED_AS_ARG)
299                                && ref_count > 1
300                            || ref_count > usage.callee_count
301                            || !is_arrow_simple_enough_for_copy(arr).is_some_and(|cost| cost <= 8))
302                    }
303                    _ => false,
304                }
305            {
306                if !inlined_into_init {
307                    inlined_into_init = true;
308                    self.vars.inline_with_multi_replacer(init);
309                }
310
311                self.mode.store(id.clone(), &*init);
312
313                let VarUsageInfo {
314                    usage_count,
315                    property_mutation_count,
316                    flags,
317                    ..
318                } = **usage;
319                let mut inc_usage = || {
320                    for (i, _) in collect_infects_from(
321                        &*init,
322                        AliasConfig::default()
323                            .marks(Some(self.marks))
324                            .need_all(true),
325                    ) {
326                        if let Some(u) = self.data.vars.get_mut(&i) {
327                            u.flags |= flags & VarUsageInfoFlags::USED_AS_ARG;
328                            u.flags |= flags & VarUsageInfoFlags::USED_AS_REF;
329                            u.flags |= flags & VarUsageInfoFlags::INDEXED_WITH_DYNAMIC_KEY;
330                            u.flags |= flags & VarUsageInfoFlags::HAS_PROPERTY_ACCESS;
331                            u.flags |= flags & VarUsageInfoFlags::USED_ABOVE_DECL;
332                            u.flags |= flags & VarUsageInfoFlags::EXECUTED_MULTIPLE_TIME;
333                            u.flags |= flags & VarUsageInfoFlags::USED_IN_COND;
334                            u.flags |= flags & VarUsageInfoFlags::USED_RECURSIVELY;
335
336                            if !flags.contains(VarUsageInfoFlags::NO_SIDE_EFFECT_FOR_MEMBER_ACCESS)
337                            {
338                                u.flags
339                                    .remove(VarUsageInfoFlags::NO_SIDE_EFFECT_FOR_MEMBER_ACCESS)
340                            }
341
342                            u.ref_count += ref_count;
343                            u.usage_count += usage_count;
344                            u.property_mutation_count += property_mutation_count;
345                        }
346                    }
347                };
348
349                if self.options.inline != 0
350                    && may_remove
351                    && match init {
352                        Expr::Arrow(..) => self.options.unused,
353                        _ => true,
354                    }
355                {
356                    self.changed = true;
357
358                    report_change!(
359                        "inline: Decided to inline '{}{:?}' because it's simple",
360                        ident.sym,
361                        ident.ctxt
362                    );
363
364                    inc_usage();
365
366                    self.vars.lits.insert(id.clone(), init.take().into());
367
368                    ident.take();
369                } else if self.options.inline != 0 || self.options.reduce_vars {
370                    report_change!(
371                        "inline: Decided to inline '{}{:?}' because it's simple",
372                        ident.sym,
373                        ident.ctxt
374                    );
375
376                    self.mode.store(id.clone(), &*init);
377
378                    inc_usage();
379
380                    self.vars.lits.insert(id.clone(), init.clone().into());
381                }
382            }
383
384            let usage = self.data.vars.get(&id).unwrap();
385
386            // Single use => inlined
387            if !self.ctx.bit_ctx.contains(BitCtx::IsExported)
388                && is_inline_enabled
389                && usage.flags.contains(VarUsageInfoFlags::DECLARED)
390                && may_remove
391                && !usage.flags.contains(VarUsageInfoFlags::REASSIGNED)
392                && !usage
393                    .flags
394                    .contains(VarUsageInfoFlags::DECLARED_AS_FOR_INIT)
395                && usage.assign_count == 1
396                && ref_count == 1
397            {
398                match init {
399                    Expr::Fn(FnExpr { function: f, .. })
400                        if matches!(
401                            &**f,
402                            Function { is_async: true, .. }
403                                | Function {
404                                    is_generator: true,
405                                    ..
406                                }
407                        ) =>
408                    {
409                        return
410                    }
411                    Expr::Arrow(ArrowExpr { is_async: true, .. })
412                    | Expr::Arrow(ArrowExpr {
413                        is_generator: true, ..
414                    }) => return,
415
416                    Expr::Lit(Lit::Regex(..)) => {
417                        if !usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL)
418                            || usage
419                                .flags
420                                .contains(VarUsageInfoFlags::EXECUTED_MULTIPLE_TIME)
421                        {
422                            return;
423                        }
424                    }
425
426                    Expr::This(..) => {
427                        // Don't inline this if it passes function boundaries.
428                        if !usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL) {
429                            return;
430                        }
431                    }
432
433                    Expr::Lit(..) => {}
434
435                    Expr::Fn(_) | Expr::Arrow(..) if !usage.can_inline_fn_once() => {
436                        return;
437                    }
438
439                    Expr::Fn(f) => {
440                        let excluded: Vec<Id> = find_pat_ids(&f.function.params);
441
442                        for id in idents_used_by(&f.function.params) {
443                            if excluded.contains(&id) {
444                                continue;
445                            }
446                            if let Some(v_usage) = self.data.vars.get(&id) {
447                                if v_usage.flags.contains(VarUsageInfoFlags::REASSIGNED) {
448                                    return;
449                                }
450                            } else {
451                                return;
452                            }
453                        }
454                    }
455
456                    Expr::Arrow(f) => {
457                        let excluded: Vec<Id> = find_pat_ids(&f.params);
458
459                        for id in idents_used_by(&f.params) {
460                            if excluded.contains(&id) {
461                                continue;
462                            }
463                            if let Some(v_usage) = self.data.vars.get(&id) {
464                                if v_usage.flags.contains(VarUsageInfoFlags::REASSIGNED) {
465                                    return;
466                                }
467                            } else {
468                                return;
469                            }
470                        }
471                    }
472
473                    Expr::Object(..) if self.options.pristine_globals => {
474                        for id in idents_used_by_ignoring_nested(init) {
475                            if let Some(v_usage) = self.data.vars.get(&id) {
476                                if v_usage.flags.contains(VarUsageInfoFlags::REASSIGNED) {
477                                    return;
478                                }
479                            }
480                        }
481                    }
482
483                    Expr::Ident(id) if !id.eq_ignore_span(ident) => {
484                        if !usage.flags.contains(VarUsageInfoFlags::ASSIGNED_FN_LOCAL) {
485                            return;
486                        }
487
488                        if let Some(init_usage) = self.data.vars.get(&id.to_id()) {
489                            if init_usage.flags.contains(VarUsageInfoFlags::REASSIGNED)
490                                || !init_usage.flags.contains(VarUsageInfoFlags::DECLARED)
491                            {
492                                return;
493                            }
494
495                            if init_usage
496                                .flags
497                                .contains(VarUsageInfoFlags::DECLARED_AS_FN_DECL)
498                                || init_usage
499                                    .flags
500                                    .contains(VarUsageInfoFlags::DECLARED_AS_FN_EXPR)
501                            {
502                                if self.options.keep_fnames
503                                    || self.mangle_options.is_some_and(|v| v.keep_fn_names)
504                                {
505                                    return;
506                                }
507                            }
508                            if init_usage
509                                .flags
510                                .contains(VarUsageInfoFlags::DECLARED_AS_FN_EXPR)
511                            {
512                                if self.options.inline != 3 {
513                                    return;
514                                }
515                            }
516                        }
517                    }
518
519                    _ => {
520                        for id in idents_used_by(init) {
521                            if let Some(v_usage) = self.data.vars.get(&id) {
522                                if v_usage.property_mutation_count > usage.property_mutation_count
523                                    || v_usage.flags.intersects(
524                                        VarUsageInfoFlags::HAS_PROPERTY_ACCESS
525                                            .union(VarUsageInfoFlags::REASSIGNED),
526                                    )
527                                {
528                                    return;
529                                }
530                            }
531                        }
532                    }
533                }
534
535                if usage.flags.contains(VarUsageInfoFlags::USED_AS_ARG)
536                    && !usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL)
537                {
538                    if let Expr::Fn(..) | Expr::Arrow(..) = init {
539                        return;
540                    }
541                }
542
543                if usage
544                    .flags
545                    .contains(VarUsageInfoFlags::EXECUTED_MULTIPLE_TIME)
546                {
547                    match init {
548                        Expr::Lit(..) => {}
549                        Expr::Fn(f) => {
550                            // Similar to `_loop` generation of the
551                            // block_scoping pass.
552                            // If the function captures the environment, we
553                            // can't inline it.
554                            let params: Vec<Id> = find_pat_ids(&f.function.params);
555
556                            if !params.is_empty() {
557                                let captured = idents_captured_by(&f.function.body);
558
559                                for param in params {
560                                    if captured.contains(&param) {
561                                        return;
562                                    }
563                                }
564                            }
565                        }
566                        _ => {
567                            return;
568                        }
569                    }
570                }
571
572                if init.may_have_side_effects(self.ctx.expr_ctx) {
573                    return;
574                }
575
576                if !inlined_into_init {
577                    self.vars.inline_with_multi_replacer(init);
578                }
579
580                report_change!(
581                    "inline: Decided to inline var '{}' because it's used only once",
582                    ident
583                );
584                self.changed = true;
585
586                self.vars
587                    .vars_for_inlining
588                    .insert(ident.take().to_id(), init.take().into());
589            }
590        }
591    }
592
593    /// Check if the body of a function is simple enough to inline.
594    fn is_fn_body_simple_enough_to_inline(
595        &self,
596        body: &BlockStmt,
597        param_count: usize,
598        usage: &VarUsageInfo,
599    ) -> bool {
600        let param_cost = param_count * 2;
601        // if it's passed as value but not called, the function expr cannot be removed
602        let func_body_cost = if usage.ref_count == usage.callee_count {
603            // length of "function c(){}"
604            14 / usage.usage_count
605        } else {
606            0
607        } as usize;
608        let cost_limit = 3 + param_cost + func_body_cost;
609
610        if body.stmts.len() == 1 {
611            match &body.stmts[0] {
612                Stmt::Expr(ExprStmt { expr, .. })
613                    if expr.size(self.ctx.expr_ctx.unresolved_ctxt) < cost_limit =>
614                {
615                    return true
616                }
617
618                Stmt::Return(ReturnStmt { arg: Some(arg), .. })
619                    if arg.size(self.ctx.expr_ctx.unresolved_ctxt) < cost_limit =>
620                {
621                    return true
622                }
623
624                Stmt::Return(ReturnStmt { arg: None, .. }) => {
625                    // size of void 0
626                    return 6 < cost_limit;
627                }
628
629                _ => {}
630            }
631        }
632
633        false
634    }
635
636    /// Stores `typeof` of [ClassDecl] and [FnDecl].
637    pub(super) fn store_typeofs(&mut self, decl: &mut Decl) {
638        let i = match &*decl {
639            Decl::Class(v) => v.ident.clone(),
640            Decl::Fn(f) => f.ident.clone(),
641            _ => return,
642        };
643        if i.sym == *"arguments" {
644            return;
645        }
646
647        if let Some(usage) = self.data.vars.get(&i.to_id()) {
648            if !usage.flags.contains(VarUsageInfoFlags::REASSIGNED) {
649                trace_op!("typeofs: Storing typeof `{}{:?}`", i.sym, i.ctxt);
650                match &*decl {
651                    Decl::Fn(..) | Decl::Class(..) => {
652                        self.typeofs.insert(i.to_id(), atom!("function"));
653                    }
654                    _ => {}
655                }
656            }
657        }
658    }
659
660    /// This method handles only [ClassDecl] and [FnDecl]. [VarDecl] should be
661    /// handled specially.
662    pub(super) fn store_decl_for_inlining(&mut self, decl: &mut Decl) {
663        let i = match &*decl {
664            Decl::Class(v) => &v.ident,
665            Decl::Fn(f) => {
666                if f.function.is_async {
667                    return;
668                }
669                &f.ident
670            }
671            _ => return,
672        };
673
674        trace_op!("inline: Trying to inline decl ({}{:?})", i.sym, i.ctxt);
675
676        if self.options.inline == 0 && !self.options.reduce_vars {
677            log_abort!("inline: [x] Inline disabled");
678            return;
679        }
680
681        if !self.may_remove_ident(i) {
682            log_abort!("inline: [x] Top level");
683            return;
684        }
685
686        if let Some(f) = decl.as_fn_decl() {
687            if self.has_noinline(f.function.ctxt) {
688                log_abort!("inline: [x] Has noinline");
689                return;
690            }
691        }
692
693        if self.ctx.bit_ctx.contains(BitCtx::IsExported) {
694            log_abort!("inline: [x] exported");
695            return;
696        }
697
698        if self
699            .data
700            .top
701            .intersects(ScopeData::HAS_EVAL_CALL.union(ScopeData::HAS_WITH_STMT))
702        {
703            return;
704        }
705
706        let id = i.to_id();
707
708        if let Some(usage) = self.data.vars.get(&id) {
709            if usage
710                .flags
711                .contains(VarUsageInfoFlags::DECLARED_AS_CATCH_PARAM)
712            {
713                log_abort!("inline: Declared as a catch parameter");
714                return;
715            }
716
717            if usage.flags.contains(VarUsageInfoFlags::USED_AS_ARG) && usage.ref_count > 1 {
718                log_abort!("inline: Used as an arugment");
719                return;
720            }
721
722            if usage.flags.intersects(
723                VarUsageInfoFlags::REASSIGNED.union(VarUsageInfoFlags::INLINE_PREVENTED),
724            ) {
725                log_abort!(
726                    "inline: [x] reassigned = {}, inline_prevented = {}",
727                    usage.flags.contains(VarUsageInfoFlags::REASSIGNED),
728                    usage.flags.contains(VarUsageInfoFlags::INLINE_PREVENTED)
729                );
730                return;
731            }
732
733            // Inline very simple functions.
734            self.vars.inline_with_multi_replacer(decl);
735            match decl {
736                Decl::Fn(f) if self.options.inline >= 2 && f.ident.sym != *"arguments" => {
737                    if let Some(body) = &f.function.body {
738                        if !usage.flags.contains(VarUsageInfoFlags::USED_RECURSIVELY)
739                            // only callees can be inlined multiple times
740                            && usage.callee_count > 0
741                            // prefer single inline
742                            && usage.ref_count > 1
743                            && self.is_fn_body_simple_enough_to_inline(
744                                body,
745                                f.function.params.len(),
746                                usage,
747                            )
748                        {
749                            if f.function
750                                .params
751                                .iter()
752                                .any(|param| matches!(param.pat, Pat::Rest(..) | Pat::Assign(..)))
753                            {
754                                return;
755                            }
756                            report_change!(
757                                "inline: Decided to inline function `{}{:?}` as it's very simple",
758                                id.0,
759                                id.1
760                            );
761
762                            for i in collect_infects_from(
763                                &f.function,
764                                AliasConfig::default()
765                                    .marks(Some(self.marks))
766                                    .need_all(true),
767                            ) {
768                                if let Some(usage) = self.data.vars.get_mut(&i.0) {
769                                    usage.ref_count += 1;
770                                }
771                            }
772
773                            self.vars.simple_functions.insert(
774                                id,
775                                FnExpr {
776                                    ident: None,
777                                    function: f.function.clone(),
778                                }
779                                .into(),
780                            );
781
782                            return;
783                        }
784                    }
785                }
786                _ => {}
787            }
788
789            // Single use => inlined
790
791            // TODO(kdy1):
792            //
793            // (usage.flags.contains(VarUsageInfoFlags::IS_FN_LOCAL) || self.options.inline
794            // == 3)
795            //
796            // seems like a correct check, but it's way to aggressive.
797            // It does not break the code, but everything like _asyncToGenerator is inlined.
798            //
799            if (self.options.reduce_vars || self.options.collapse_vars || self.options.inline != 0)
800                && usage.ref_count == 1
801                && usage.can_inline_fn_once()
802                && (match decl {
803                    Decl::Class(..) => !usage.flags.contains(VarUsageInfoFlags::USED_ABOVE_DECL),
804                    Decl::Fn(..) => true,
805                    _ => false,
806                })
807            {
808                if let Decl::Class(ClassDecl { class, .. }) = decl {
809                    if class_has_side_effect(self.ctx.expr_ctx, class) {
810                        return;
811                    }
812                }
813
814                match &decl {
815                    Decl::Class(_c) => {
816                        if self.options.inline != 3
817                            || self.options.keep_classnames
818                            || self.mangle_options.is_some_and(|v| v.keep_class_names)
819                        {
820                            log_abort!("inline: [x] Keep class names");
821                            return;
822                        }
823
824                        self.changed = true;
825                        report_change!(
826                            "inline: Decided to inline class `{}{:?}` as it's used only once",
827                            _c.ident.sym,
828                            _c.ident.ctxt
829                        );
830                    }
831                    Decl::Fn(_f) => {
832                        if self.options.keep_fnames
833                            || self.mangle_options.is_some_and(|v| v.keep_fn_names)
834                        {
835                            log_abort!("inline: [x] Keep fn names");
836                            return;
837                        }
838
839                        self.changed = true;
840                        report_change!(
841                            "inline: Decided to inline function `{}{:?}` as it's used only once",
842                            _f.ident.sym,
843                            _f.ident.ctxt
844                        );
845                    }
846                    _ => {}
847                }
848
849                let e = match decl.take() {
850                    Decl::Class(c) => ClassExpr {
851                        ident: Some(c.ident),
852                        class: c.class,
853                    }
854                    .into(),
855                    Decl::Fn(f) => FnExpr {
856                        ident: if usage.flags.contains(VarUsageInfoFlags::USED_RECURSIVELY) {
857                            Some(f.ident)
858                        } else {
859                            None
860                        },
861                        function: f.function,
862                    }
863                    .into(),
864                    _ => {
865                        unreachable!()
866                    }
867                };
868
869                self.vars.vars_for_inlining.insert(id, e);
870            } else {
871                log_abort!("inline: [x] Usage: {:?}", usage);
872            }
873        }
874    }
875
876    /// Actually inlines variables.
877    pub(super) fn inline(&mut self, e: &mut Expr) {
878        if self.ctx.bit_ctx.contains(BitCtx::IsExactLhsOfAssign) {
879            return;
880        }
881
882        match e {
883            Expr::Member(me) => {
884                if let MemberProp::Computed(prop) = &mut me.prop {
885                    if let Expr::Lit(Lit::Num(..)) = &*prop.expr {
886                        if let Expr::Ident(obj) = &*me.obj {
887                            let new = self.vars.lits_for_array_access.get(&obj.to_id());
888
889                            if let Some(new) = new {
890                                report_change!("inline: Inlined array access");
891                                self.changed = true;
892
893                                me.obj.clone_from(new);
894                            }
895                        }
896                    }
897                }
898            }
899            Expr::Ident(i) => {
900                let id = i.to_id();
901                if let Some(value) = self.vars.lits.get(&id).or_else(|| {
902                    if self.ctx.bit_ctx.contains(BitCtx::IsCallee) {
903                        self.vars.simple_functions.get(&id)
904                    } else {
905                        None
906                    }
907                }) {
908                    if !matches!(**value, Expr::Ident(..) | Expr::Member(..))
909                        && self.ctx.bit_ctx.contains(BitCtx::IsUpdateArg)
910                    {
911                        return;
912                    }
913
914                    // currently renamer relies on the fact no distinct var has same ctxt, we need
915                    // to remap all new bindings.
916                    let bindings: FxHashSet<Id> = collect_decls(value);
917                    let new_mark = Mark::new();
918                    let mut cache = FxHashMap::default();
919                    let mut remap = FxHashMap::default();
920
921                    for id in bindings {
922                        let new_ctxt = cache
923                            .entry(id.1)
924                            .or_insert_with(|| id.1.apply_mark(new_mark));
925
926                        let new_ctxt = *new_ctxt;
927
928                        if let Some(usage) = self.data.vars.get(&id).cloned() {
929                            let new_id = (id.0.clone(), new_ctxt);
930                            self.data.vars.insert(new_id, usage);
931                        }
932
933                        remap.insert(id, new_ctxt);
934                    }
935
936                    let mut value = value.clone();
937                    if !remap.is_empty() {
938                        let mut remapper = Remapper::new(&remap);
939                        value.visit_mut_with(&mut remapper);
940                    }
941
942                    self.changed = true;
943                    report_change!("inline: Replacing a variable `{}` with cheap expression", i);
944
945                    *e = *value;
946                    return;
947                }
948
949                // Check without cloning
950                if let Some(value) = self.vars.vars_for_inlining.get(&id) {
951                    if self.ctx.bit_ctx.contains(BitCtx::IsExactLhsOfAssign)
952                        && !is_valid_for_lhs(value)
953                    {
954                        return;
955                    }
956
957                    if let Expr::Member(..) = &**value {
958                        if self.ctx.bit_ctx.contains(BitCtx::ExecutedMultipleTime) {
959                            return;
960                        }
961                    }
962                }
963
964                if let Some(value) = self.vars.vars_for_inlining.remove(&id) {
965                    self.changed = true;
966                    report_change!("inline: Replacing '{}' with an expression", i);
967
968                    *e = *value;
969
970                    log_abort!("inline: [Change] {}", crate::debug::dump(&*e, false))
971                }
972            }
973            _ => (),
974        }
975    }
976}
977
978fn is_arrow_simple_enough_for_copy(e: &ArrowExpr) -> Option<u8> {
979    if e.is_async {
980        return None;
981    }
982
983    match &*e.body {
984        BlockStmtOrExpr::BlockStmt(s) => is_block_stmt_of_fn_simple_enough_for_copy(s),
985        BlockStmtOrExpr::Expr(e) => is_arrow_body_simple_enough_for_copy(e),
986        #[cfg(swc_ast_unknown)]
987        _ => panic!("unable to access unknown nodes"),
988    }
989}
990
991fn is_arrow_body_simple_enough_for_copy(e: &Expr) -> Option<u8> {
992    match e {
993        Expr::Ident(..) | Expr::Lit(..) => return Some(1),
994        Expr::Member(MemberExpr { obj, prop, .. }) if !prop.is_computed() => {
995            return Some(is_arrow_body_simple_enough_for_copy(obj)? + 2)
996        }
997        Expr::Call(c) => {
998            let mut cost = is_arrow_body_simple_enough_for_copy(c.callee.as_expr()?.deref())?;
999            for arg in &c.args {
1000                let arg_cost = is_arrow_body_simple_enough_for_copy(&arg.expr)?;
1001                cost += arg_cost;
1002                cost += if arg.spread.is_some() { 3 } else { 0 };
1003            }
1004
1005            return Some(cost + 2);
1006        }
1007        Expr::Unary(u) => return Some(is_arrow_body_simple_enough_for_copy(&u.arg)? + 1),
1008
1009        Expr::Bin(b) => {
1010            return Some(
1011                is_arrow_body_simple_enough_for_copy(&b.left)?
1012                    + is_arrow_body_simple_enough_for_copy(&b.right)?
1013                    + 2,
1014            )
1015        }
1016        _ => {}
1017    }
1018
1019    None
1020}
1021
1022fn is_block_stmt_of_fn_simple_enough_for_copy(b: &BlockStmt) -> Option<u8> {
1023    if b.stmts.len() == 1 {
1024        if let Stmt::Return(ret) = &b.stmts[0] {
1025            return ret
1026                .arg
1027                .as_deref()
1028                .map_or(Some(0), is_arrow_body_simple_enough_for_copy);
1029        }
1030    }
1031
1032    None
1033}