Repository: swc-project/swc (rust)

Issue:
Unused variable with ternary expression addition is improperly minified.
### Describe the bug

When the `dead_code` and `unused` compression options are both enabled, SWC improperly minifies the provided code snippet (an unused variable where two ternary expressions are added to each other).

### Input code

```typescript
const totalCount =
    (a ? a.length : 0) +
    (b ? b.length : 0);
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false
    },
    "target": "es2015",
    "loose": false,
    "minify": {
      "compress": {
        "arguments": false,
        "arrows": false,
        "booleans": false,
        "booleans_as_integers": false,
        "collapse_vars": false,
        "comparisons": false,
        "computed_props": false,
        "conditionals": false,
        "dead_code": true,
        "directives": false,
        "drop_console": false,
        "drop_debugger": false,
        "evaluate": false,
        "expression": false,
        "hoist_funs": false,
        "hoist_props": false,
        "hoist_vars": false,
        "if_return": false,
        "join_vars": false,
        "keep_classnames": false,
        "keep_fargs": false,
        "keep_fnames": false,
        "keep_infinity": false,
        "loops": false,
        "negate_iife": false,
        "properties": false,
        "reduce_funcs": false,
        "reduce_vars": false,
        "side_effects": false,
        "switches": false,
        "typeofs": false,
        "unsafe": false,
        "unsafe_arrows": false,
        "unsafe_comps": false,
        "unsafe_Function": false,
        "unsafe_math": false,
        "unsafe_symbols": false,
        "unsafe_methods": false,
        "unsafe_proto": false,
        "unsafe_regexp": false,
        "unsafe_undefined": false,
        "unused": true,
        "const_to_let": false,
        "pristine_globals": false
      },
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": true,
  "isModule": true
}
```


### Playground link

https://play.swc.rs/?version=1.2.245&code=H4sIAAAAAAAAA0vOzysuUSjJL0nMcc4vzStRsOVSAAKNRAV7hUS9nNS89JIMBSsFA00FbYhEElAiCVnCGgCsVxBCQwAAAA%3D%3D&config=H4sIAAAAAAAAA22TQZLcIAxF7%2BL1LDKpShZzgOzmDBRthFsTQC4ketrV1XePaLtsbGeHHh%2FxJcSj%2B%2BK%2B%2B3h0o80Mua54SmLv3Ucn0wjcZxyle%2BuEFXkbGJ4a2DyAqAT454%2F3X7odiBgWwVsXMaGfarKe4piBua71UImQhFedzZm%2Bt%2FBCFMCmMzCWDSaBAfK22VMIdmQwN7ujUUtBprRnRcCZMdPY4uRQkJKGK3RgnenJaTGSSwWYoRe8QaPRNKpJrO720MGlDEPt40LhZkOxssng%2FuqH3rqiKyGL8aVxPKO93ZntikVvMkjJW64vwrTX%2FAVQs8EyJxvhwL2%2ByRH9R4bJ64vKtFJ97sZZgkFLNIh%2Bq7N6hyzYpMrgSg%2B10P4Ed5YZHRjwXvvewG%2BU%2Ftqkq%2BNJfou1f7YxMIfmMGILrRNxgn%2FUmLQPs%2FBo5XpkPMULhVOKCHIld8LaDKEjzDrO9%2FFIS3KgzQbXbBSu4TyOdezECJlQP%2BDabB0NPWSGQJdtmvWnRpuGdUifFZArFTxe%2FXt94d%2Fdc%2Fuy8y3In4uuxs9%2FrkPqJiYEAAA%3D

### Expected behavior

SWC should preserve the ternary checks, resulting in the following output:

```
a&&a.length,b&&b.length
```

### Actual behavior

Instead, SWC outputs the following code:

```
a.length,b.length;
```

This can cause runtime errors if `a` or `b` is undefined, since we no longer perform the ternary check.

### Version

1.2.245

### Additional context

This appears to be a problem with how the `dead_code` and `unused` compression options interact with each other. SWC correctly minifies this code if either of these options are disabled.

Pull request:
fix(es/minifier): Preserve more side effects
**Description:**

We now preserve more side effects.

**Related issue:**

 - Closes https://github.com/swc-project/swc/issues/5680.

Patch:
diff --git a/crates/swc_ecma_utils/src/lib.rs b/crates/swc_ecma_utils/src/lib.rs
index eaafb26582..428b793892 100644
--- a/crates/swc_ecma_utils/src/lib.rs
+++ b/crates/swc_ecma_utils/src/lib.rs
@@ -2267,9 +2267,14 @@ impl ExprCtx {
             | Expr::This(..)
             | Expr::Fn(..)
             | Expr::Arrow(..)
-            | Expr::Ident(..)
             | Expr::PrivateName(..) => {}
 
+            Expr::Ident(..) => {
+                if expr.may_have_side_effects(self) {
+                    to.push(Box::new(expr));
+                }
+            }
+
             // In most case, we can do nothing for this.
             Expr::Update(_) | Expr::Assign(_) | Expr::Yield(_) | Expr::Await(_) => {
                 to.push(Box::new(expr))
@@ -2301,6 +2306,10 @@ impl ExprCtx {
             Expr::Cond(_) => to.push(Box::new(expr)),
 
             Expr::Unary(UnaryExpr { arg, .. }) => self.extract_side_effects_to(to, *arg),
+
+            Expr::Bin(BinExpr { op, .. }) if op.may_short_circuit() => {
+                to.push(Box::new(expr));
+            }
             Expr::Bin(BinExpr { left, right, .. }) => {
                 self.extract_side_effects_to(to, *left);
                 self.extract_side_effects_to(to, *right);


Test patch:
diff --git a/crates/swc/tests/tsc-references/parserComputedPropertyName6.2.minified.js b/crates/swc/tests/tsc-references/parserComputedPropertyName6.2.minified.js
index 49827cfe35..29c1cfe3b3 100644
--- a/crates/swc/tests/tsc-references/parserComputedPropertyName6.2.minified.js
+++ b/crates/swc/tests/tsc-references/parserComputedPropertyName6.2.minified.js
@@ -1,2 +1,2 @@
 //// [parserComputedPropertyName6.ts]
-e;
+e, e, e;
diff --git a/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity1.2.minified.js b/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity1.2.minified.js
index 995ba792bc..df6a5f30b6 100644
--- a/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity1.2.minified.js
+++ b/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity1.2.minified.js
@@ -1,2 +1,2 @@
 //// [parserRegularExpressionDivideAmbiguity1.ts]
-a.foo();
+notregexp, a.foo();
diff --git a/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity2.2.minified.js b/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity2.2.minified.js
index e7a12911c9..23f5dec9e7 100644
--- a/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity2.2.minified.js
+++ b/crates/swc/tests/tsc-references/parserRegularExpressionDivideAmbiguity2.2.minified.js
@@ -1,2 +1,2 @@
 //// [parserRegularExpressionDivideAmbiguity2.ts]
-a.foo();
+notregexp, a.foo();
diff --git a/crates/swc_ecma_minifier/tests/TODO.txt b/crates/swc_ecma_minifier/tests/TODO.txt
index 3f72c53710..9440ea51ac 100644
--- a/crates/swc_ecma_minifier/tests/TODO.txt
+++ b/crates/swc_ecma_minifier/tests/TODO.txt
@@ -1,6 +1,7 @@
 asm/asm_mixed/input.js
 collapse_vars/collapse_vars_do_while/input.js
 collapse_vars/collapse_vars_misc1/input.js
+dead_code/dead_code_constant_boolean_should_warn_more/input.js
 drop_unused/keep_assign/input.js
 drop_unused/reassign_const/input.js
 drop_unused/unused_circular_references_1/input.js
@@ -18,3 +19,4 @@ pure_funcs/unary/input.js
 pure_getters/issue_2265_3/input.js
 reduce_vars/multi_def_3/input.js
 reduce_vars/variables_collision_in_immediately_invoked_func/input.js
+sequences/issue_2062/input.js
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5680/analysis-snapshot.rust-debug b/crates/swc_ecma_minifier/tests/fixture/issues/5680/analysis-snapshot.rust-debug
new file mode 100644
index 0000000000..8413eb77d0
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5680/analysis-snapshot.rust-debug
@@ -0,0 +1,120 @@
+TestSnapshot {
+    vars: [
+        (
+            (
+                Atom('a' type=static),
+                #2,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 2,
+                cond_init: false,
+                declared: false,
+                declared_count: 0,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 0,
+                mutation_by_call_count: 0,
+                usage_count: 2,
+                reassigned_with_assignment: false,
+                reassigned_with_var_decl: false,
+                mutated: false,
+                has_property_access: true,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: true,
+                is_fn_local: true,
+                used_by_nested_fn: false,
+                executed_multiple_time: false,
+                used_in_cond: true,
+                var_kind: None,
+                var_initialized: false,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: false,
+                used_as_callee: false,
+                used_as_arg: false,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+        (
+            (
+                Atom('b' type=static),
+                #2,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 2,
+                cond_init: false,
+                declared: false,
+                declared_count: 0,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 0,
+                mutation_by_call_count: 0,
+                usage_count: 2,
+                reassigned_with_assignment: false,
+                reassigned_with_var_decl: false,
+                mutated: false,
+                has_property_access: true,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: true,
+                is_fn_local: true,
+                used_by_nested_fn: false,
+                executed_multiple_time: false,
+                used_in_cond: true,
+                var_kind: None,
+                var_initialized: false,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: false,
+                used_as_callee: false,
+                used_as_arg: false,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+        (
+            (
+                Atom('totalCount' type=dynamic),
+                #1,
+            ),
+            VarUsageInfo {
+                inline_prevented: false,
+                ref_count: 0,
+                cond_init: false,
+                declared: true,
+                declared_count: 1,
+                declared_as_fn_param: false,
+                declared_as_fn_expr: false,
+                assign_count: 0,
+                mutation_by_call_count: 0,
+                usage_count: 0,
+                reassigned_with_assignment: false,
+                reassigned_with_var_decl: false,
+                mutated: false,
+                has_property_access: false,
+                has_property_mutation: false,
+                accessed_props: {},
+                exported: false,
+                used_above_decl: false,
+                is_fn_local: true,
+                used_by_nested_fn: false,
+                executed_multiple_time: false,
+                used_in_cond: false,
+                var_kind: Some(
+                    "const",
+                ),
+                var_initialized: true,
+                declared_as_catch_param: false,
+                no_side_effect_for_member_access: false,
+                used_as_callee: false,
+                used_as_arg: false,
+                pure_fn: false,
+                infects: [],
+            },
+        ),
+    ],
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5680/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/5680/input.js
new file mode 100644
index 0000000000..657f952d76
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5680/input.js
@@ -0,0 +1,3 @@
+const totalCount =
+    (a ? a.length : 0) +
+    (b ? b.length : 0);
\ No newline at end of file
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5680/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/5680/output.js
new file mode 100644
index 0000000000..ef0a93958b
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5680/output.js
@@ -0,0 +1,1 @@
+a && a.length, b && b.length;
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/5680/output.mangleOnly.js b/crates/swc_ecma_minifier/tests/fixture/issues/5680/output.mangleOnly.js
new file mode 100644
index 0000000000..d8984dac02
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/5680/output.mangleOnly.js
@@ -0,0 +1,1 @@
+const n = (a ? a.length : 0) + (b ? b.length : 0);
diff --git a/crates/swc_ecma_minifier/tests/golden.txt b/crates/swc_ecma_minifier/tests/golden.txt
index 22859f9288..dfb1fbd6c7 100644
--- a/crates/swc_ecma_minifier/tests/golden.txt
+++ b/crates/swc_ecma_minifier/tests/golden.txt
@@ -295,7 +295,6 @@ dead_code/dead_code_1/input.js
 dead_code/dead_code_block_decls_die/input.js
 dead_code/dead_code_const_annotation_regex/input.js
 dead_code/dead_code_const_declaration/input.js
-dead_code/dead_code_constant_boolean_should_warn_more/input.js
 dead_code/dead_code_constant_boolean_should_warn_more_strict/input.js
 dead_code/global_timeout_and_interval_symbols/input.js
 dead_code/issue_2233_1/input.js
@@ -1339,7 +1338,6 @@ sequences/func_def_5/input.js
 sequences/hoist_defun/input.js
 sequences/iife/input.js
 sequences/issue_1685/input.js
-sequences/issue_2062/input.js
 sequences/issue_2313/input.js
 sequences/issue_27/input.js
 sequences/lift_sequences_1/input.js
diff --git a/crates/swc_ecma_transforms_optimization/tests/simplify.rs b/crates/swc_ecma_transforms_optimization/tests/simplify.rs
index cd35414c5c..d899c3012e 100644
--- a/crates/swc_ecma_transforms_optimization/tests/simplify.rs
+++ b/crates/swc_ecma_transforms_optimization/tests/simplify.rs
@@ -431,7 +431,7 @@ fn test_object_literal() {
 fn test_array_literal() {
     test("([])", "");
     test("([1])", "");
-    test("([a])", "");
+    test("([a])", "a");
     test("([foo()])", "foo()");
 }