swc_ecma_minifier/compress/optimize/
unused.rs

1use std::borrow::Borrow;
2
3use rustc_hash::FxHashSet;
4use swc_atoms::Atom;
5use swc_common::{util::take::Take, DUMMY_SP};
6use swc_ecma_ast::*;
7use swc_ecma_usage_analyzer::util::is_global_var_with_pure_property_access;
8use swc_ecma_utils::{contains_ident_ref, contains_this_expr, ExprExt};
9use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
10
11use super::Optimizer;
12#[cfg(feature = "debug")]
13use crate::debug::dump;
14use crate::{
15    compress::optimize::{util::extract_class_side_effect, BitCtx},
16    option::PureGetterOption,
17    program_data::{ScopeData, VarUsageInfoFlags},
18};
19
20#[derive(Debug, Default, Clone, Copy)]
21pub(crate) struct PropertyAccessOpts {
22    pub allow_getter: bool,
23
24    pub only_ident: bool,
25}
26
27/// Methods related to the option `unused`.
28impl Optimizer<'_> {
29    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
30    pub(super) fn drop_unused_var_declarator(
31        &mut self,
32        var: &mut VarDeclarator,
33        storage_for_side_effects: &mut Option<Box<Expr>>,
34    ) {
35        if self.mode.preserve_vars() {
36            return;
37        }
38        if var.name.is_invalid() {
39            return;
40        }
41
42        #[cfg(debug_assertions)]
43        let had_init = var.init.is_some();
44
45        match &mut var.init {
46            Some(init) => match &**init {
47                Expr::Invalid(..) => {
48                    self.drop_unused_vars(&mut var.name, None);
49                }
50                // I don't know why, but terser preserves this
51                Expr::Fn(FnExpr { function, .. })
52                    if matches!(&**function, Function { is_async: true, .. }) => {}
53                _ => {
54                    self.drop_unused_vars(&mut var.name, Some(init));
55
56                    if var.name.is_invalid() {
57                        report_change!("unused: Removing an unused variable declarator");
58                        let side_effects = self.ignore_return_value(init).map(Box::new);
59                        if let Some(e) = side_effects {
60                            *storage_for_side_effects = Some(e);
61                        }
62                    }
63                }
64            },
65            None => {
66                self.drop_unused_vars(&mut var.name, var.init.as_deref_mut());
67            }
68        }
69
70        if var.name.is_invalid() {
71            return;
72        }
73
74        #[cfg(debug_assertions)]
75        {
76            if self
77                .ctx
78                .bit_ctx
79                .intersects(BitCtx::IsConst.union(BitCtx::IsLet))
80                && had_init
81                && var.init.is_none()
82            {
83                unreachable!("const/let variable without initializer: {:#?}", var);
84            }
85        }
86    }
87
88    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
89    pub(super) fn drop_unused_param(&mut self, pat: &mut Pat, ignore_fn_length: bool) {
90        if !self.options.unused && !self.options.reduce_fns {
91            return;
92        }
93
94        if let Some(scope) = self.data.scopes.get(&self.ctx.scope) {
95            if scope.intersects(ScopeData::HAS_EVAL_CALL.union(ScopeData::HAS_WITH_STMT)) {
96                return;
97            }
98        }
99
100        if !ignore_fn_length {
101            // Preserve `length` of function.
102            if pat.is_ident() {
103                return;
104            }
105        }
106
107        self.take_pat_if_unused(pat, None, false)
108    }
109
110    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
111    pub(super) fn drop_unused_vars(&mut self, name: &mut Pat, init: Option<&mut Expr>) {
112        if self
113            .ctx
114            .bit_ctx
115            .intersects(BitCtx::IsExported | BitCtx::InAsm)
116        {
117            return;
118        }
119
120        trace_op!("unused: drop_unused_vars({})", dump(&*name, false));
121
122        if !self.options.unused && !self.options.side_effects {
123            return;
124        }
125
126        if self.ctx.bit_ctx.contains(BitCtx::InVarDeclOfForInOrOfLoop) {
127            return;
128        }
129
130        if let Some(scope) = self.data.scopes.get(&self.ctx.scope) {
131            if scope.intersects(ScopeData::HAS_EVAL_CALL.union(ScopeData::HAS_WITH_STMT)) {
132                log_abort!(
133                    "unused: Preserving `{}` because of usages",
134                    dump(&*name, false)
135                );
136                return;
137            }
138        }
139
140        if !name.is_ident() && init.is_none() {
141            return;
142        }
143
144        self.take_pat_if_unused(name, init, true);
145    }
146
147    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
148    pub(super) fn drop_unused_params(&mut self, params: &mut Vec<Param>) {
149        if self.options.keep_fargs || !self.options.unused {
150            return;
151        }
152
153        for param in params.iter_mut().rev() {
154            self.take_pat_if_unused(&mut param.pat, None, false);
155
156            if !param.pat.is_invalid() {
157                break;
158            }
159        }
160
161        params.retain(|p| !p.pat.is_invalid());
162    }
163
164    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
165    pub(super) fn drop_unused_arrow_params(&mut self, params: &mut Vec<Pat>) {
166        if self.options.keep_fargs || !self.options.unused {
167            return;
168        }
169
170        for param in params.iter_mut().rev() {
171            self.take_pat_if_unused(param, None, false);
172
173            if !param.is_invalid() {
174                break;
175            }
176        }
177
178        params.retain(|p| !p.is_invalid());
179    }
180
181    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
182    fn take_ident_of_pat_if_unused(&mut self, i: &mut Ident, init: Option<&mut Expr>) {
183        trace_op!("unused: Checking identifier `{}`", i);
184
185        if !self.may_remove_ident(i) {
186            log_abort!("unused: Preserving var `{:#?}` because it's top-level", i);
187            return;
188        }
189
190        if let Some(v) = self.data.vars.get(&i.to_id()) {
191            let is_used_in_member =
192                v.property_mutation_count > 0 || v.flags.contains(VarUsageInfoFlags::USED_AS_REF);
193            if v.ref_count == 0
194                && v.usage_count == 0
195                && !v.flags.contains(VarUsageInfoFlags::REASSIGNED)
196                && !is_used_in_member
197            {
198                self.changed = true;
199                report_change!(
200                    "unused: Dropping a variable '{}{:?}' because it is not used",
201                    i.sym,
202                    i.ctxt
203                );
204                // This will remove variable.
205                i.take();
206                return;
207            }
208
209            if v.ref_count == 0 && v.usage_count == 0 {
210                if let Some(e) = init {
211                    if self
212                        .ctx
213                        .bit_ctx
214                        .intersects(BitCtx::IsConst.union(BitCtx::IsLet))
215                    {
216                        if let Expr::Lit(Lit::Null(..)) = e {
217                            return;
218                        }
219                    }
220
221                    let ret = self.ignore_return_value(e);
222                    if let Some(ret) = ret {
223                        *e = ret;
224                    } else {
225                        if self
226                            .ctx
227                            .bit_ctx
228                            .intersects(BitCtx::IsConst.union(BitCtx::IsLet))
229                        {
230                            *e = Null { span: DUMMY_SP }.into();
231                        } else {
232                            *e = Invalid { span: DUMMY_SP }.into();
233                        }
234                    }
235                }
236            }
237
238            log_abort!(
239                "unused: Cannot drop ({}) because it's used",
240                dump(&*i, false)
241            );
242        }
243    }
244
245    pub(crate) fn should_preserve_property_access(
246        &self,
247        e: &Expr,
248        opts: PropertyAccessOpts,
249    ) -> bool {
250        if opts.only_ident && !e.is_ident() {
251            return true;
252        }
253
254        match e {
255            Expr::Ident(e) => {
256                if e.ctxt.outer() == self.marks.unresolved_mark {
257                    if is_global_var_with_pure_property_access(&e.sym) {
258                        return false;
259                    }
260                }
261
262                if let Some(usage) = self.data.vars.get(&e.to_id()) {
263                    if !usage.flags.contains(VarUsageInfoFlags::DECLARED) {
264                        return true;
265                    }
266
267                    if !usage.mutated()
268                        && usage
269                            .flags
270                            .contains(VarUsageInfoFlags::NO_SIDE_EFFECT_FOR_MEMBER_ACCESS)
271                    {
272                        return false;
273                    }
274                }
275            }
276
277            Expr::Object(o) => {
278                // We should check properties
279                return o.props.iter().any(|p| match p {
280                    PropOrSpread::Spread(p) => self.should_preserve_property_access(&p.expr, opts),
281                    PropOrSpread::Prop(p) => match &**p {
282                        Prop::Assign(_) => true,
283                        Prop::Getter(_) => !opts.allow_getter,
284                        Prop::Shorthand(p) => {
285                            // Check if `p` is __proto__
286
287                            if p.sym == "__proto__" {
288                                return true;
289                            }
290
291                            false
292                        }
293                        Prop::KeyValue(k) => {
294                            // Check if `k` is __proto__
295
296                            if let PropName::Ident(i) = &k.key {
297                                if i.sym == "__proto__" {
298                                    return true;
299                                }
300                            }
301
302                            false
303                        }
304
305                        Prop::Setter(_) => true,
306                        Prop::Method(_) => false,
307                        #[cfg(swc_ast_unknown)]
308                        _ => panic!("unable to access unknown nodes"),
309                    },
310                    #[cfg(swc_ast_unknown)]
311                    _ => panic!("unable to access unknown nodes"),
312                });
313            }
314
315            Expr::Paren(p) => return self.should_preserve_property_access(&p.expr, opts),
316
317            Expr::Fn(..) | Expr::Arrow(..) | Expr::Array(..) | Expr::Class(..) => {
318                return false;
319            }
320
321            Expr::Seq(e) => {
322                if let Some(last) = e.exprs.last() {
323                    return self.should_preserve_property_access(last, opts);
324                }
325                return true;
326            }
327
328            _ => {}
329        }
330
331        true
332    }
333
334    /// `parent_span` should be [Span] of [VarDeclarator] or [AssignExpr]
335    #[allow(clippy::only_used_in_recursion)]
336    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
337    pub(super) fn take_pat_if_unused(
338        &mut self,
339        name: &mut Pat,
340        mut init: Option<&mut Expr>,
341        is_var_decl: bool,
342    ) {
343        if self.ctx.bit_ctx.contains(BitCtx::IsExported) {
344            return;
345        }
346
347        trace_op!("unused: take_pat_if_unused({})", dump(&*name, false));
348
349        let pure_mark = self.marks.pure;
350        let has_pure_ann = match init {
351            Some(Expr::Call(c)) => c.ctxt.has_mark(pure_mark),
352            Some(Expr::New(n)) => n.ctxt.has_mark(pure_mark),
353            Some(Expr::TaggedTpl(t)) => t.ctxt.has_mark(pure_mark),
354            _ => false,
355        };
356
357        if !name.is_ident() {
358            // TODO: Use smart logic
359            if self.options.pure_getters != PureGetterOption::Bool(true) && !has_pure_ann {
360                return;
361            }
362
363            if !has_pure_ann {
364                if let Some(init) = init.as_mut() {
365                    if !matches!(init, Expr::Ident(_))
366                        && self.should_preserve_property_access(
367                            init,
368                            PropertyAccessOpts {
369                                allow_getter: false,
370                                only_ident: false,
371                            },
372                        )
373                    {
374                        return;
375                    }
376                }
377            }
378        }
379
380        match name {
381            Pat::Ident(i) => {
382                self.take_ident_of_pat_if_unused(&mut i.id, init);
383
384                // Removed
385                if i.id.is_dummy() {
386                    name.take();
387                }
388            }
389
390            Pat::Array(arr) => {
391                for (idx, arr_elem) in arr.elems.iter_mut().enumerate() {
392                    if let Some(p) = arr_elem {
393                        let elem = init
394                            .as_mut()
395                            .and_then(|expr| self.access_numeric_property(expr, idx));
396
397                        self.take_pat_if_unused(p, elem, is_var_decl);
398
399                        if p.is_invalid() {
400                            *arr_elem = None;
401                        }
402                    }
403                }
404
405                if has_pure_ann && arr.elems.iter().all(|e| e.is_none()) {
406                    name.take();
407                }
408            }
409
410            Pat::Object(obj) => {
411                // If a rest pattern exists, we can't remove anything at current level.
412                if obj
413                    .props
414                    .iter()
415                    .any(|p| matches!(p, ObjectPatProp::Rest(_)))
416                {
417                    return;
418                }
419
420                for prop in &mut obj.props {
421                    match prop {
422                        ObjectPatProp::KeyValue(p) => {
423                            if p.key.is_computed() {
424                                continue;
425                            }
426
427                            self.take_pat_if_unused(&mut p.value, None, is_var_decl);
428                        }
429                        ObjectPatProp::Assign(AssignPatProp { key, value, .. }) => {
430                            if has_pure_ann {
431                                if let Some(e) = value {
432                                    *value = self.ignore_return_value(e).map(Box::new);
433                                }
434                            }
435
436                            if value.is_none() {
437                                self.take_ident_of_pat_if_unused(key, None);
438                            }
439                        }
440                        _ => {}
441                    }
442                }
443
444                obj.props.retain(|p| {
445                    match p {
446                        ObjectPatProp::KeyValue(p) => {
447                            if p.value.is_invalid() {
448                                return false;
449                            }
450                        }
451                        ObjectPatProp::Assign(p) => {
452                            if p.key.is_dummy() {
453                                return false;
454                            }
455                        }
456                        ObjectPatProp::Rest(_) => {}
457                        #[cfg(swc_ast_unknown)]
458                        _ => panic!("unable to access unknown nodes"),
459                    }
460
461                    true
462                });
463
464                if obj.props.is_empty() {
465                    name.take();
466                }
467            }
468
469            Pat::Rest(_) => {}
470            Pat::Assign(_) => {
471                // TODO
472            }
473            _ => {}
474        }
475    }
476
477    /// Creates an empty [VarDecl] if `decl` should be removed.
478    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
479    pub(super) fn drop_unused_decl(&mut self, decl: &mut Decl) {
480        if self.ctx.bit_ctx.contains(BitCtx::IsExported) {
481            return;
482        }
483
484        if !self.options.top_level() && self.ctx.is_top_level_for_block_level_vars() {
485            return;
486        }
487
488        if !self.options.unused {
489            return;
490        }
491
492        if let Some(scope) = self.data.scopes.get(&self.ctx.scope) {
493            if scope.intersects(ScopeData::HAS_EVAL_CALL.union(ScopeData::HAS_WITH_STMT)) {
494                return;
495            }
496        }
497
498        match decl {
499            Decl::Class(ClassDecl { ident, class, .. }) => {
500                if ident.sym == "arguments" {
501                    return;
502                }
503
504                // Fix https://github.com/swc-project/swc/issues/5588
505                let may_have_side_effect = class.body.iter().any(|m| match m {
506                    ClassMember::ClassProp(ClassProp {
507                        is_static: true,
508                        value: Some(_),
509                        ..
510                    })
511                    | ClassMember::PrivateProp(PrivateProp {
512                        is_static: true,
513                        value: Some(_),
514                        ..
515                    }) => true,
516                    ClassMember::StaticBlock(StaticBlock {
517                        body: BlockStmt { stmts, .. },
518                        ..
519                    }) if !stmts.is_empty() => true,
520                    _ => false,
521                });
522                if may_have_side_effect {
523                    return;
524                }
525
526                // If it is not used, drop it.
527                if self
528                    .data
529                    .vars
530                    .get(&ident.to_id())
531                    .map(|v| v.usage_count == 0 && v.property_mutation_count == 0)
532                    .unwrap_or(false)
533                {
534                    let Some(side_effects) = extract_class_side_effect(self.ctx.expr_ctx, class)
535                    else {
536                        return;
537                    };
538
539                    self.changed = true;
540                    report_change!(
541                        "unused: Dropping a decl '{}{:?}' because it is not used",
542                        ident.sym,
543                        ident.ctxt
544                    );
545
546                    let mut side_effects: Vec<_> =
547                        side_effects.into_iter().map(|expr| expr.take()).collect();
548                    decl.take();
549
550                    if side_effects.is_empty() {
551                        return;
552                    }
553
554                    self.prepend_stmts.push(
555                        ExprStmt {
556                            span: DUMMY_SP,
557                            expr: if side_effects.len() > 1 {
558                                SeqExpr {
559                                    span: DUMMY_SP,
560                                    exprs: side_effects,
561                                }
562                                .into()
563                            } else {
564                                side_effects.remove(0)
565                            },
566                        }
567                        .into(),
568                    );
569                }
570            }
571            Decl::Fn(FnDecl { ident, .. }) => {
572                // We should skip if the name of decl is arguments.
573                if ident.sym == "arguments" {
574                    return;
575                }
576
577                if !self.may_remove_ident(ident) {
578                    log_abort!(
579                        "unused: Preserving function `{}` because it's top-level",
580                        ident.sym
581                    );
582                    return;
583                }
584
585                // If it is not used, drop it.
586                if self
587                    .data
588                    .vars
589                    .get(&ident.to_id())
590                    .map(|v| v.usage_count == 0 && v.property_mutation_count == 0)
591                    .unwrap_or(false)
592                {
593                    self.changed = true;
594                    report_change!(
595                        "unused: Dropping a decl '{}{:?}' because it is not used",
596                        ident.sym,
597                        ident.ctxt
598                    );
599                    // This will remove the declaration.
600                    decl.take();
601                }
602            }
603
604            Decl::Var(_) => {
605                // Variable declarations are handled by other functions.
606            }
607
608            Decl::Using(..) => {
609                // TODO: Optimize
610            }
611
612            Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {
613                // Nothing to do. We might change this to unreachable!()
614            }
615
616            #[cfg(swc_ast_unknown)]
617            _ => panic!("unable to access unknown nodes"),
618        }
619    }
620
621    /// This should be only called from ignore_return_value
622    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
623    pub(super) fn drop_unused_update(&mut self, e: &mut Expr) {
624        if !self.options.unused {
625            return;
626        }
627
628        let update = match e {
629            Expr::Update(u) => u,
630            _ => return,
631        };
632
633        if let Expr::Ident(arg) = &*update.arg {
634            if let Some(var) = self.data.vars.get(&arg.to_id()) {
635                // Update is counted as usage
636                if var
637                    .flags
638                    .contains(VarUsageInfoFlags::DECLARED.union(VarUsageInfoFlags::IS_FN_LOCAL))
639                    && var.usage_count == 1
640                {
641                    self.changed = true;
642                    report_change!(
643                        "unused: Dropping an update '{}{:?}' because it is not used",
644                        arg.sym,
645                        arg.ctxt
646                    );
647                    // This will remove the update.
648                    e.take();
649                }
650            }
651        }
652    }
653
654    /// This should be only called from ignore_return_value
655    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
656    pub(super) fn drop_unused_op_assign(&mut self, e: &mut Expr) {
657        if !self.options.unused {
658            return;
659        }
660
661        if self.ctx.bit_ctx.contains(BitCtx::IsDeleteArg) {
662            return;
663        }
664
665        if self
666            .data
667            .top
668            .intersects(ScopeData::HAS_EVAL_CALL.union(ScopeData::HAS_WITH_STMT))
669        {
670            return;
671        }
672
673        let assign = match e {
674            Expr::Assign(AssignExpr { op: op!("="), .. }) => return,
675            // RHS may not be evaluated
676            Expr::Assign(AssignExpr { op, .. }) if op.may_short_circuit() => return,
677            Expr::Assign(e) => e,
678            _ => return,
679        };
680
681        if let AssignTarget::Simple(SimpleAssignTarget::Ident(left)) = &assign.left {
682            if let Some(var) = self.data.vars.get(&left.to_id()) {
683                // TODO: We don't need fn_local check
684                if var
685                    .flags
686                    .contains(VarUsageInfoFlags::DECLARED.union(VarUsageInfoFlags::IS_FN_LOCAL))
687                    && var.usage_count == 1
688                {
689                    self.changed = true;
690                    report_change!(
691                        "unused: Dropping an op-assign '{}{:?}' because it is not used",
692                        left.id.sym,
693                        left.id.ctxt
694                    );
695                    // This will remove the op-assign.
696                    *e = *assign.right.take();
697                }
698            }
699        }
700    }
701
702    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
703    pub(super) fn drop_unused_assignments(&mut self, e: &mut Expr) {
704        if self.ctx.bit_ctx.contains(BitCtx::IsDeleteArg) {
705            return;
706        }
707
708        if self
709            .data
710            .top
711            .intersects(ScopeData::HAS_EVAL_CALL.union(ScopeData::HAS_WITH_STMT))
712        {
713            return;
714        }
715
716        let assign = match e {
717            Expr::Assign(e) => e,
718            _ => return,
719        };
720
721        if !self.options.unused {
722            return;
723        }
724
725        let used_arguments = self
726            .data
727            .scopes
728            .get(&self.ctx.scope)
729            .unwrap_or_else(|| {
730                unreachable!(
731                    "scope should exist\nScopes: {:?};\nCtxt: {:?}",
732                    self.data.scopes, self.ctx.scope
733                )
734            })
735            .contains(ScopeData::USED_ARGUMENTS);
736
737        trace_op!(
738            "unused: drop_unused_assignments: Target: `{}`",
739            dump(&assign.left, false)
740        );
741
742        if let AssignTarget::Simple(SimpleAssignTarget::Ident(i)) = &mut assign.left {
743            if !self.may_remove_ident(&i.id) {
744                return;
745            }
746
747            if let Some(var) = self.data.vars.get(&i.to_id()) {
748                // technically this is inline
749                if !var.flags.intersects(
750                    VarUsageInfoFlags::INLINE_PREVENTED.union(VarUsageInfoFlags::EXPORTED),
751                ) && var.usage_count == 0
752                    && var.flags.contains(VarUsageInfoFlags::DECLARED)
753                    && (!var.flags.contains(VarUsageInfoFlags::DECLARED_AS_FN_PARAM)
754                        || !used_arguments
755                        || self.ctx.expr_ctx.in_strict)
756                {
757                    report_change!(
758                        "unused: Dropping assignment to var '{}{:?}', which is never used",
759                        i.id.sym,
760                        i.id.ctxt
761                    );
762                    self.changed = true;
763                    if self.ctx.bit_ctx.contains(BitCtx::IsThisAwareCallee) {
764                        *e = SeqExpr {
765                            span: DUMMY_SP,
766                            exprs: vec![0.into(), assign.right.take()],
767                        }
768                        .into()
769                    } else {
770                        *e = *assign.right.take();
771                    }
772                } else {
773                    log_abort!(
774                        "unused: Preserving assignment to `{}` because of usage: {:?}",
775                        dump(&assign.left, false),
776                        var
777                    )
778                }
779            }
780        }
781    }
782
783    /// Make `name` [None] if the name is not used.
784    #[cfg_attr(feature = "debug", tracing::instrument(level = "debug", skip_all))]
785    pub(super) fn remove_name_if_not_used(&mut self, name: &mut Option<Ident>) {
786        if !self.options.unused {
787            return;
788        }
789
790        if self.ctx.bit_ctx.contains(BitCtx::IsExported) {
791            return;
792        }
793
794        if let Some(i) = &name {
795            let can_remove_ident = self
796                .data
797                .vars
798                .get(&i.to_id())
799                .map(|v| {
800                    (!v.flags.contains(VarUsageInfoFlags::USED_RECURSIVELY)
801                        && v.ref_count == 0
802                        && v.usage_count == 0)
803                        || v.var_kind.is_some()
804                })
805                .unwrap_or(false);
806
807            if can_remove_ident {
808                self.changed = true;
809                report_change!("Removing ident of an class / function expression");
810                *name = None;
811            }
812        }
813    }
814
815    pub(super) fn remove_duplicate_var_decls(&mut self, s: &mut Stmt) -> Option<()> {
816        if !self.options.unused {
817            return None;
818        }
819
820        let var = match s {
821            Stmt::Decl(Decl::Var(v)) => v,
822            _ => return None,
823        };
824
825        for d in var.decls.iter_mut() {
826            if d.init.is_none() {
827                if let Pat::Ident(name) = &d.name {
828                    if let Some(usage) = self.data.vars.get_mut(&name.to_id()) {
829                        if usage.flags.contains(
830                            VarUsageInfoFlags::IS_FN_LOCAL
831                                .union(VarUsageInfoFlags::DECLARED_AS_FN_PARAM),
832                        ) && usage.declared_count >= 2
833                        {
834                            d.name.take();
835                            usage.declared_count -= 1;
836
837                            report_change!(
838                                "Removing a variable statement because it's a function parameter"
839                            );
840                            self.changed = true;
841                        }
842                    }
843                }
844            }
845        }
846
847        var.decls.retain(|v| !v.name.is_invalid());
848
849        None
850    }
851
852    /// `var Parser = function Parser() {};` => `var Parser = function () {}`
853    pub(super) fn remove_duplicate_name_of_function(&mut self, v: &mut VarDeclarator) {
854        if !self.options.unused || self.options.hoist_props {
855            return;
856        }
857
858        if let Some(Expr::Fn(f)) = v.init.as_deref_mut() {
859            let Some(f_ident) = f.ident.as_ref() else {
860                return;
861            };
862
863            if contains_ident_ref(&f.function.body, f_ident) {
864                return;
865            }
866
867            self.changed = true;
868            report_change!(
869                "unused: Removing the name of a function expression because it's not used by it'"
870            );
871            f.ident = None;
872        }
873    }
874
875    pub(super) fn drop_unused_properties(&mut self, v: &mut VarDeclarator) -> Option<()> {
876        if !self.options.hoist_props || self.ctx.bit_ctx.contains(BitCtx::IsExported) {
877            return None;
878        }
879
880        if self.ctx.bit_ctx.contains(BitCtx::TopLevel) && !self.options.top_level() {
881            return None;
882        }
883
884        let name = v.name.as_ident()?;
885        let obj = v.init.as_mut()?.as_mut_object()?;
886
887        let usage = self.data.vars.get(&name.to_id())?;
888
889        if usage.flags.intersects(
890            VarUsageInfoFlags::INDEXED_WITH_DYNAMIC_KEY
891                .union(VarUsageInfoFlags::USED_AS_REF)
892                .union(VarUsageInfoFlags::USED_RECURSIVELY),
893        ) {
894            return None;
895        }
896
897        if obj.props.iter().any(|p| match p {
898            PropOrSpread::Spread(_) => true,
899            PropOrSpread::Prop(p) => match &**p {
900                Prop::Shorthand(_) => false,
901                Prop::KeyValue(p) => {
902                    p.key.is_computed() || p.value.may_have_side_effects(self.ctx.expr_ctx)
903                }
904                Prop::Assign(_) => true,
905                Prop::Getter(p) => p.key.is_computed(),
906                Prop::Setter(p) => p.key.is_computed(),
907                Prop::Method(p) => p.key.is_computed(),
908                #[cfg(swc_ast_unknown)]
909                _ => panic!("unable to access unknown nodes"),
910            },
911            #[cfg(swc_ast_unknown)]
912            _ => panic!("unable to access unknown nodes"),
913        }) {
914            return None;
915        }
916
917        let properties_used_via_this = {
918            let mut v = ThisPropertyVisitor::default();
919            obj.visit_with(&mut v);
920            if v.should_abort {
921                return None;
922            }
923            v.properties
924        };
925
926        let mut unknown_used_props = self
927            .data
928            .vars
929            .get(&name.to_id())
930            .map(|v| v.accessed_props.clone())
931            .unwrap_or_default();
932
933        // If there's an access to an unknown property, we should preserve all
934        // properties.
935        for prop in &obj.props {
936            let prop = match prop {
937                PropOrSpread::Spread(_) => return None,
938                PropOrSpread::Prop(prop) => prop,
939                #[cfg(swc_ast_unknown)]
940                _ => panic!("unable to access unknown nodes"),
941            };
942
943            match &**prop {
944                Prop::Method(prop) => {
945                    if contains_this_expr(&prop.function.body) {
946                        return None;
947                    }
948                }
949                Prop::Getter(prop) => {
950                    if contains_this_expr(&prop.body) {
951                        return None;
952                    }
953                }
954                Prop::Setter(prop) => {
955                    if contains_this_expr(&prop.body) {
956                        return None;
957                    }
958                }
959                Prop::KeyValue(prop) => match &*prop.value {
960                    Expr::Fn(f) => {
961                        if contains_this_expr(&f.function.body) {
962                            return None;
963                        }
964                    }
965                    Expr::Arrow(f) => {
966                        if contains_this_expr(&f.body) {
967                            return None;
968                        }
969                    }
970                    _ => {}
971                },
972                _ => {}
973            }
974
975            if contains_this_expr(prop) {
976                return None;
977            }
978
979            match &**prop {
980                Prop::KeyValue(p) => match &p.key {
981                    PropName::Str(s) => {
982                        if !can_remove_property(&s.value) {
983                            return None;
984                        }
985
986                        if let Some(v) = unknown_used_props.get_mut(&s.value) {
987                            *v = 0;
988                        }
989                    }
990                    PropName::Ident(i) => {
991                        if !can_remove_property(i.sym.borrow()) {
992                            return None;
993                        }
994
995                        if let Some(v) = unknown_used_props.get_mut(i.sym.borrow()) {
996                            *v = 0;
997                        }
998                    }
999                    _ => return None,
1000                },
1001                Prop::Shorthand(p) => {
1002                    if !can_remove_property(p.sym.borrow()) {
1003                        return None;
1004                    }
1005
1006                    if let Some(v) = unknown_used_props.get_mut(p.sym.borrow()) {
1007                        *v = 0;
1008                    }
1009                }
1010                _ => return None,
1011            }
1012        }
1013
1014        if !unknown_used_props.iter().all(|(_, v)| *v == 0) {
1015            log_abort!("[x] unknown used props: {:?}", unknown_used_props);
1016            return None;
1017        }
1018
1019        let should_preserve_property = |sym: &swc_atoms::Wtf8Atom| {
1020            if let Some(s) = sym.as_str() {
1021                if s == "toString" {
1022                    return true;
1023                }
1024
1025                if s.parse::<f64>().is_ok() || s.parse::<i32>().is_ok() {
1026                    return true;
1027                }
1028            }
1029
1030            usage.accessed_props.contains_key(sym) || properties_used_via_this.contains(sym)
1031        };
1032        let should_preserve = |key: &PropName| match key {
1033            PropName::Ident(k) => should_preserve_property(k.sym.borrow()),
1034            PropName::Str(k) => should_preserve_property(&k.value),
1035            PropName::Num(..) => true,
1036            PropName::Computed(..) => true,
1037            PropName::BigInt(..) => true,
1038            #[cfg(swc_ast_unknown)]
1039            _ => panic!("unable to access unknown nodes"),
1040        };
1041
1042        let len = obj.props.len();
1043        obj.props.retain(|prop| match prop {
1044            PropOrSpread::Spread(_) => {
1045                unreachable!()
1046            }
1047            PropOrSpread::Prop(p) => match &**p {
1048                Prop::Shorthand(p) => should_preserve_property(p.sym.borrow()),
1049                Prop::KeyValue(p) => should_preserve(&p.key),
1050                Prop::Assign(..) => {
1051                    unreachable!()
1052                }
1053                Prop::Getter(p) => should_preserve(&p.key),
1054                Prop::Setter(p) => should_preserve(&p.key),
1055                Prop::Method(p) => should_preserve(&p.key),
1056                #[cfg(swc_ast_unknown)]
1057                _ => panic!("unable to access unknown nodes"),
1058            },
1059            #[cfg(swc_ast_unknown)]
1060            _ => panic!("unable to access unknown nodes"),
1061        });
1062
1063        if obj.props.len() != len {
1064            self.changed = true;
1065            report_change!("unused: Removing unused properties");
1066        }
1067
1068        None
1069    }
1070}
1071
1072fn can_remove_property(sym: &swc_atoms::Wtf8Atom) -> bool {
1073    sym.as_str()
1074        .map_or(true, |s| !matches!(s, "toString" | "valueOf"))
1075}
1076
1077#[derive(Default)]
1078struct ThisPropertyVisitor {
1079    properties: FxHashSet<Atom>,
1080
1081    should_abort: bool,
1082}
1083
1084impl Visit for ThisPropertyVisitor {
1085    noop_visit_type!(fail);
1086
1087    fn visit_assign_expr(&mut self, e: &AssignExpr) {
1088        if self.should_abort {
1089            return;
1090        }
1091
1092        e.visit_children_with(self);
1093
1094        if self.should_abort {
1095            return;
1096        }
1097
1098        if let Expr::This(..) = &*e.right {
1099            if e.op == op!("=") || e.op.may_short_circuit() {
1100                self.should_abort = true;
1101            }
1102        }
1103    }
1104
1105    fn visit_call_expr(&mut self, n: &CallExpr) {
1106        if self.should_abort {
1107            return;
1108        }
1109
1110        n.visit_children_with(self);
1111
1112        if self.should_abort {
1113            return;
1114        }
1115
1116        for arg in &n.args {
1117            if arg.expr.is_this() {
1118                self.should_abort = true;
1119                return;
1120            }
1121        }
1122    }
1123
1124    fn visit_callee(&mut self, c: &Callee) {
1125        if self.should_abort {
1126            return;
1127        }
1128
1129        c.visit_children_with(self);
1130
1131        if self.should_abort {
1132            return;
1133        }
1134
1135        if let Callee::Expr(e) = c {
1136            match &**e {
1137                Expr::This(..) => {
1138                    self.should_abort = true;
1139                }
1140
1141                Expr::Member(e) => {
1142                    if e.obj.is_this() {
1143                        self.should_abort = true;
1144                    }
1145                }
1146
1147                _ => {}
1148            }
1149        }
1150    }
1151
1152    fn visit_member_expr(&mut self, e: &MemberExpr) {
1153        if self.should_abort {
1154            return;
1155        }
1156
1157        e.visit_children_with(self);
1158
1159        if self.should_abort {
1160            return;
1161        }
1162
1163        if let Expr::This(..) = &*e.obj {
1164            match &e.prop {
1165                MemberProp::Ident(p) => {
1166                    self.properties.insert(p.sym.clone());
1167                }
1168                MemberProp::Computed(_) => {
1169                    self.should_abort = true;
1170                }
1171                _ => {}
1172            }
1173        }
1174    }
1175}