Repository: swc-project/swc (rust)

Issue:
Incorrect compression: compound assignment with inc/dec in template string
### Describe the bug

SWC with `"compress"` option transpiles some expressions involving assignment, increment/decrement operator and template literal into invalid JS for `ES2015+` targets. The occurrence of this issue appears to have started since `v1.3.27`.

### Input code

```typescript
let id = 0;

function getId() {
    id = id % 9999;
    return `${id++}`
}
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es2015",
    "loose": false,
    "minify": {
      "compress": {
        "arguments": false,
        "arrows": true,
        "booleans": true,
        "booleans_as_integers": false,
        "collapse_vars": true,
        "comparisons": true,
        "computed_props": true,
        "conditionals": true,
        "dead_code": true,
        "directives": true,
        "drop_console": false,
        "drop_debugger": true,
        "evaluate": true,
        "expression": true,
        "hoist_funs": false,
        "hoist_props": true,
        "hoist_vars": false,
        "if_return": true,
        "join_vars": true,
        "keep_classnames": false,
        "keep_fargs": true,
        "keep_fnames": false,
        "keep_infinity": false,
        "loops": true,
        "negate_iife": true,
        "properties": true,
        "reduce_funcs": false,
        "reduce_vars": false,
        "side_effects": true,
        "switches": true,
        "typeofs": true,
        "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": true,
        "pristine_globals": true,
        "passes": 2
      },
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "isModule": false
}
```


### Playground link

https://play.swc.rs/?version=1.3.70&code=H4sIAAAAAAAAA8tJLVHITFGwVTCw5uJKK81LLsnMz1NITy3xTNHQVKjmUgACsDyQUFWwBAJrsFhRaklpUZ5Cgkp1Zoq2dm0CVy0AFqXyMEoAAAA%3D&config=H4sIAAAAAAAAA32UwW7bMAyG732KwOcdtgLdYQ%2Bw255BUCTKUSaLhkilMYq8%2B2jZSbOGzs3mx5%2BUSIofL7tddyTX%2Fdp9yKf8jLYQlNu%2FWGjKbM9i6cANllyJI3ffrvRIMwo2ETTTZSEd29IDNxW9fv%2Fxtiq6hEhwVay2IeYYpvucDoexANGdTawSsg6Qmf7Xr6zg%2Bwy41Hv7HjGBzU%2BIsWRiZuihaIEdpmRHAnOyRYkyn9SWSKilmGFl8GYsOKo8%2B8gRs%2BR8pB6sNw49KCgWcBxPoMkkl8gyyfWU%2BzTsYV%2F7vvX5ixpONlXLSk44t5bIaR%2FZASOxCTVrFVzgRgkWuNb2qzIGU4BrUTIeMeaNlvwFkAIkS5TtAFrc5hFknLbU4aky5iATy5PCZby1W2bopaYmxqAUdq4MFI5aMwv46mCurNOOs%2BKN8lH0YCAEGRUlNL1HdgctKU8jYFCA9NcGbagWYG6PcIPP7%2BEJ%2Fi235GW%2BNjwGy4dtStOwx%2FQkwQB8QP%2FEQVrBuI2LLInzuM1r9iCjAV51qdTA4w6QB8BoUluXD7Mhz0Mimj7hXt0So8x5a%2BLrarvcdvNgc%2F%2B5A5b1%2FLI6dAP62uC6%2BOemL%2Bv6Z%2FfpdN3Mt9t0kf5clUvYyz%2FC6tcBRQYAAA%3D%3D

### Expected behavior

Transpilation to ES5 returns syntactically correct Javascript
```
var getId=function(){return id%=9999,"".concat(id++)},id=0;
```

resembling, for instance, Terser with ecma2015 target
```
let id=0;function getId(){return id%=9999,""+id++}
```


### Actual behavior

It tries to combine compound assignment with increment operator in one expression inside template string:
```
let id = 0;
function getId() {
    return `${id %= 9999++}`;
}
```

### Version

1.3.70

### Additional context

The example above can be reduced to something like:
```
function add11(a) {
    a += 10
    return `${a++}`
}
```

which is transpiled into:

```
function add11(a) {
    return `${a += 10++}`;
}
```
where `10++` leads to an error `Uncaught SyntaxError: Invalid left-hand side expression in postfix operation`


Pull request:
fix(es/minifier): Abort seq inliner using visitor
**Description:**


**Related issue:**

 - Closes #7697.

Patch:
diff --git a/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs b/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
index e78964c3a7..b7873cfb1e 100644
--- a/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
+++ b/crates/swc_ecma_minifier/src/compress/optimize/sequences.rs
@@ -2031,9 +2031,11 @@ impl Optimizer<'_> {
                             pat_usage: Default::default(),
                             target: a_id,
                             in_lhs: false,
+                            abort: false,
+                            in_abort: false,
                         };
                         b.visit_with(&mut v);
-                        if v.expr_usage != 1 || v.pat_usage != 0 {
+                        if v.expr_usage != 1 || v.pat_usage != 0 || v.abort {
                             log_abort!(
                                 "sequences: Aborting merging of an update expression because of \
                                  usage counts ({}, ref = {}, pat = {})",
@@ -2103,9 +2105,11 @@ impl Optimizer<'_> {
                             pat_usage: Default::default(),
                             target: a_id,
                             in_lhs: false,
+                            abort: false,
+                            in_abort: false,
                         };
                         b.visit_with(&mut v);
-                        if v.expr_usage != 1 || v.pat_usage != 0 {
+                        if v.expr_usage != 1 || v.pat_usage != 0 || v.abort {
                             log_abort!(
                                 "sequences: Aborting merging of an update expression because of \
                                  usage counts ({}, ref = {}, pat = {})",
@@ -2426,9 +2430,11 @@ impl Optimizer<'_> {
                 pat_usage: Default::default(),
                 target: &left_id,
                 in_lhs: false,
+                abort: false,
+                in_abort: false,
             };
             b.visit_with(&mut v);
-            if v.expr_usage != 1 || v.pat_usage != 0 {
+            if v.expr_usage != 1 || v.pat_usage != 0 || v.abort {
                 log_abort!(
                     "sequences: Aborting because of usage counts ({}{:?}, ref = {}, pat = {})",
                     left_id.sym,
@@ -2493,8 +2499,11 @@ struct UsageCounter<'a> {
     expr_usage: usize,
     pat_usage: usize,
 
+    abort: bool,
+
     target: &'a Ident,
     in_lhs: bool,
+    in_abort: bool,
 }
 
 impl Visit for UsageCounter<'_> {
@@ -2502,6 +2511,11 @@ impl Visit for UsageCounter<'_> {
 
     fn visit_ident(&mut self, i: &Ident) {
         if self.target.sym == i.sym && self.target.span.ctxt == i.span.ctxt {
+            if self.in_abort {
+                self.abort = true;
+                return;
+            }
+
             if self.in_lhs {
                 self.pat_usage += 1;
             } else {
@@ -2521,6 +2535,27 @@ impl Visit for UsageCounter<'_> {
         }
     }
 
+    fn visit_update_expr(&mut self, e: &UpdateExpr) {
+        let old_in_abort = self.in_abort;
+        self.in_abort = true;
+        e.visit_children_with(self);
+        self.in_abort = old_in_abort;
+    }
+
+    fn visit_await_expr(&mut self, e: &AwaitExpr) {
+        let old_in_abort = self.in_abort;
+        self.in_abort = true;
+        e.visit_children_with(self);
+        self.in_abort = old_in_abort;
+    }
+
+    fn visit_yield_expr(&mut self, e: &YieldExpr) {
+        let old_in_abort = self.in_abort;
+        self.in_abort = true;
+        e.visit_children_with(self);
+        self.in_abort = old_in_abort;
+    }
+
     fn visit_pat(&mut self, p: &Pat) {
         let old = self.in_lhs;
         self.in_lhs = true;


Test patch:
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/config.json b/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/config.json
new file mode 100644
index 0000000000..2c1ceea5a0
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/config.json
@@ -0,0 +1,47 @@
+{
+    "arguments": false,
+    "arrows": true,
+    "booleans": true,
+    "booleans_as_integers": false,
+    "collapse_vars": true,
+    "comparisons": true,
+    "computed_props": true,
+    "conditionals": true,
+    "dead_code": true,
+    "directives": true,
+    "drop_console": false,
+    "drop_debugger": true,
+    "evaluate": true,
+    "expression": true,
+    "hoist_funs": false,
+    "hoist_props": true,
+    "hoist_vars": false,
+    "if_return": true,
+    "join_vars": true,
+    "keep_classnames": false,
+    "keep_fargs": true,
+    "keep_fnames": false,
+    "keep_infinity": false,
+    "loops": true,
+    "negate_iife": true,
+    "properties": true,
+    "reduce_funcs": false,
+    "reduce_vars": false,
+    "side_effects": true,
+    "switches": true,
+    "typeofs": true,
+    "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": true,
+    "pristine_globals": true,
+    "passes": 2
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/input.js
new file mode 100644
index 0000000000..9205884ee3
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/input.js
@@ -0,0 +1,6 @@
+let id = 0;
+
+export function getId() {
+    id = id % 9999;
+    return `${id++}`
+}
\ No newline at end of file
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/output.js
new file mode 100644
index 0000000000..bde1103104
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/7697/1/output.js
@@ -0,0 +1,4 @@
+let id = 0;
+export function getId() {
+    return id %= 9999, `${id++}`;
+}