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
27impl 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 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 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 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 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 if p.sym == "__proto__" {
288 return true;
289 }
290
291 false
292 }
293 Prop::KeyValue(k) => {
294 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 #[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 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 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 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 }
473 _ => {}
474 }
475 }
476
477 #[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 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 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 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 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 decl.take();
601 }
602 }
603
604 Decl::Var(_) => {
605 }
607
608 Decl::Using(..) => {
609 }
611
612 Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {
613 }
615
616 #[cfg(swc_ast_unknown)]
617 _ => panic!("unable to access unknown nodes"),
618 }
619 }
620
621 #[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 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 e.take();
649 }
650 }
651 }
652 }
653
654 #[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 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 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 *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 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 #[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 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 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}