Repository: swc-project/swc (rust)

Issue:
Compression removes variable declaration/assignment
### Describe the bug

Compressing an implementation of throttleTime removes an assignment, where it should not. Refer to the playground link below. The `currentValue` variable is dropped, and the assignment happening on line 5 is gone. 

### Input code

_No response_

### Config

_No response_

### Playground link

https://play.swc.rs/?version=1.3.58&code=H4sIAAAAAAAAA1WPwQqDMAyG7z7Ff0xhh93FPYXsLhpZoWtHlpbB6LtrtQ53KcnP1y8Jf15BFHP0o9rgoQ8Jqo57%2B2SyXlnS4Ay%2BDeBYMUYR9nofXOS2ZrqiIWpphTWKB03Bs0F3A6VCbmVR4E%2BADukQAXYGVZWp8CHcgby9FVn%2Fvln7vSE6TTgjKdgJ17bmZSs6L2Cq%2BILfpSXJbZObBWSr33cZAQAA&config=H4sIAAAAAAAAA32US5LjIAyG932KlNe9nVnMAXo3Z6AICIc0Ri4k0nF15e4jY%2BcxHTk7W59%2BCSSh77fdrjuS6%2F7svuVTfkZbCMrtXyw0ZbZnsXTgBkuuxJG79ys90oyCTQTNdFlIx7b0wE1Fv1b3LiESXN1X2xBzDNNjQofDWIDowSZWiVcHyEz%2F61dW8GsGXOqjfY%2BYwOYXxFgyMTP0ULTADlOyI4E52aJEmU9qSyTUUsywMngzFhxVnn3kiFlyPlMP1huHHhQUCziOJ9BkkktkmeR6yn0a9rCvfd%2Ba%2FEMNJ5uqZSUnnFtL5LRK1ANGYhNq1kq4wI0aLHAt7k9lDKYA15KfdUeMeaMnnwBSgWSJsh1Ai9s8gszTljq8VMYcZGR5UrjMt3bLDL0U1cQYlMrOlYHCUetmAV8dzJV12nFWvFE%2Bih4MhCCzooSmr8juoCXlaQQMCpD%2B2qBN1QLM7RVu8PlBvMAfckvWB2z1GCwftilNwx7TiwQD8AH9CwdpBeM2LrIlzuM2r9mDjAZ41aVSA89LQB4Ao0ltWT7NhjwPiWj6hPv7mlgdLrclPNjc39%2F7soffVoduQF8bXDf83N9lL%2F%2Fu7k7XLXw7eBfp71XZkl7%2BAVPL6HotBgAA

### Expected behavior

declaration and assignment should be preserved. I've worked around this particular instance by assigning an initial value of `null` to the `currentValue`. 

### Actual behavior

`currentValue` is dropped, and the assignment is missing. 

### Version

1.3.58

### Additional context

_No response_

Pull request description:
fix(es/minifier): Prevent inlining vars assigned outside current function scope

<!-- Note: CI script will automatically rebase your PR so please do not rebase unless required -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:**

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

**BREAKING CHANGE:**

<!--
If this PR introduces a breaking change, it must contain a notice for it to be included in the CHANGELOG. Add description or remove entirely if not breaking.

You may need to update `MIGRATION.md` for the breaking changes.
-->

**Related issue (if exists):**
closes #7412 

Patch:
diff --git a/crates/swc_ecma_minifier/src/compress/optimize/inline.rs b/crates/swc_ecma_minifier/src/compress/optimize/inline.rs
index 0dbba24a70..e0877e8c59 100644
--- a/crates/swc_ecma_minifier/src/compress/optimize/inline.rs
+++ b/crates/swc_ecma_minifier/src/compress/optimize/inline.rs
@@ -182,14 +182,14 @@ where
             // new variant is added for multi inline, think carefully
             if is_inline_enabled
                 && usage.declared_count == 1
-                && (usage.can_inline_var())
+                && usage.can_inline_var()
                 && match init {
                     Expr::Ident(Ident {
                         sym: js_word!("eval"),
                         ..
                     }) => false,
 
-                    Expr::Ident(id) if !id.eq_ignore_span(ident) => self
+                    Expr::Ident(id) if !id.eq_ignore_span(ident) && usage.assigned_fn_local => self
                         .data
                         .vars
                         .get(&id.to_id())
@@ -362,6 +362,10 @@ where
                     }
 
                     Expr::Ident(id) if !id.eq_ignore_span(ident) => {
+                        if !usage.assigned_fn_local {
+                            return;
+                        }
+
                         if let Some(init_usage) = self.data.vars.get(&id.to_id()) {
                             if init_usage.reassigned() || !init_usage.declared {
                                 return;
diff --git a/crates/swc_ecma_minifier/src/program_data.rs b/crates/swc_ecma_minifier/src/program_data.rs
index 5fb28a65ac..8444c96407 100644
--- a/crates/swc_ecma_minifier/src/program_data.rs
+++ b/crates/swc_ecma_minifier/src/program_data.rs
@@ -99,6 +99,11 @@ pub(crate) struct VarUsageInfo {
     /// functions.
     pub(crate) is_fn_local: bool,
 
+    used_in_non_child_fn: bool,
+
+    /// `true` if all its assign happens in the same function scope it's defined
+    pub(crate) assigned_fn_local: bool,
+
     pub(crate) executed_multiple_time: bool,
     pub(crate) used_in_cond: bool,
 
@@ -123,8 +128,6 @@ pub(crate) struct VarUsageInfo {
     /// `infects_to`. This should be renamed, but it will be done with another
     /// PR. (because it's hard to review)
     infects_to: Vec<Access>,
-
-    pub(crate) used_in_non_child_fn: bool,
     /// Only **string** properties.
     pub(crate) accessed_props: Box<AHashMap<JsWord, u32>>,
 
@@ -166,6 +169,7 @@ impl Default for VarUsageInfo {
             accessed_props: Default::default(),
             used_recursively: Default::default(),
             is_top_level: Default::default(),
+            assigned_fn_local: true,
         }
     }
 }
@@ -232,6 +236,8 @@ impl Storage for ProgramData {
             match self.vars.entry(id.clone()) {
                 Entry::Occupied(mut e) => {
                     e.get_mut().inline_prevented |= var_info.inline_prevented;
+                    let var_assigned = var_info.assign_count > 0
+                        || (var_info.var_initialized && !e.get().var_initialized);
 
                     if var_info.var_initialized {
                         if e.get().var_initialized || e.get().ref_count > 0 {
@@ -294,6 +300,8 @@ impl Storage for ProgramData {
                     e.get_mut().is_fn_local &= var_info.is_fn_local;
                     e.get_mut().used_in_non_child_fn |= var_info.used_in_non_child_fn;
 
+                    e.get_mut().assigned_fn_local &= var_info.assigned_fn_local;
+
                     for (k, v) in *var_info.accessed_props {
                         *e.get_mut().accessed_props.entry(k).or_default() += v;
                     }
@@ -304,6 +312,10 @@ impl Storage for ProgramData {
                             if !var_info.used_recursively {
                                 e.get_mut().used_in_non_child_fn = true
                             }
+
+                            if var_assigned {
+                                e.get_mut().assigned_fn_local = false
+                            }
                         }
                         ScopeKind::Block => {
                             if var_info.used_in_non_child_fn {
@@ -608,7 +620,6 @@ impl ProgramData {
             let simple_assign = ctx.is_exact_reassignment && !ctx.is_op_assign;
 
             VarUsageInfo {
-                is_fn_local: true,
                 used_above_decl: !simple_assign,
                 ..Default::default()
             }
@@ -684,7 +695,6 @@ impl ProgramData {
                 let simple_assign = ctx.is_exact_reassignment && !ctx.is_op_assign;
 
                 VarUsageInfo {
-                    is_fn_local: true,
                     used_above_decl: !simple_assign,
                     ..Default::default()
                 }


Test patch:
diff --git a/crates/swc_ecma_minifier/tests/benches-full/vue.js b/crates/swc_ecma_minifier/tests/benches-full/vue.js
index b97c102081..a71260e5ec 100644
--- a/crates/swc_ecma_minifier/tests/benches-full/vue.js
+++ b/crates/swc_ecma_minifier/tests/benches-full/vue.js
@@ -2,7 +2,7 @@
     'object' == typeof exports && 'undefined' != typeof module ? module.exports = factory() : 'function' == typeof define && define.amd ? define(factory) : (global1 = global1 || self).Vue = factory();
 }(this, function() {
     'use strict';
-    var Vue, cid, configDef, dataDef, propsDef, hookRE, baseCompile, _isServer, _Set, timerFunc, mark, measure, initProxy, target, len, str, chr, index$1, expressionPos, expressionEndPos, target$1, svgContainer, emptyStyle, decoder, warn$2, delimiters, transforms, preTransforms, postTransforms, platformIsPreTag, platformMustUseProp, platformGetTagNamespace, maybeComponent, isStaticKey, isPlatformReservedTag, div, emptyObject = Object.freeze({});
+    var Vue, cid, configDef, dataDef, propsDef, hookRE, baseCompile, _isServer, _Set, timerFunc, mark, measure, initProxy, target, len, str, chr, index$1, expressionPos, expressionEndPos, warn$1, target$1, svgContainer, emptyStyle, decoder, warn$2, delimiters, transforms, preTransforms, postTransforms, platformIsPreTag, platformMustUseProp, platformGetTagNamespace, maybeComponent, isStaticKey, isPlatformReservedTag, div, emptyObject = Object.freeze({});
     function isUndef(v) {
         return null == v;
     }
@@ -2918,8 +2918,9 @@
         modules: modules$1,
         directives: {
             model: function(el, dir, _warn) {
+                warn$1 = _warn;
                 var code, number, valueBinding, trueValueBinding, falseValueBinding, number1, valueBinding1, value = dir.value, modifiers = dir.modifiers, tag = el.tag, type = el.attrsMap.type;
-                if ('input' === tag && 'file' === type && _warn("<" + el.tag + " v-model=\"" + value + '" type="file">:\nFile inputs are read only. Use a v-on:change listener instead.', el.rawAttrsMap['v-model']), el.component) return genComponentModel(el, value, modifiers), !1;
+                if ('input' === tag && 'file' === type && warn$1("<" + el.tag + " v-model=\"" + value + '" type="file">:\nFile inputs are read only. Use a v-on:change listener instead.', el.rawAttrsMap['v-model']), el.component) return genComponentModel(el, value, modifiers), !1;
                 if ('select' === tag) addHandler(el, 'change', code = (code = 'var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return ' + (modifiers && modifiers.number ? '_n(val)' : 'val') + "});") + " " + genAssignmentCode(value, '$event.target.multiple ? $$selectedVal : $$selectedVal[0]'), null, !0);
                 else if ('input' === tag && 'checkbox' === type) number = modifiers && modifiers.number, valueBinding = getBindingAttr(el, 'value') || 'null', trueValueBinding = getBindingAttr(el, 'true-value') || 'true', falseValueBinding = getBindingAttr(el, 'false-value') || 'false', addProp(el, 'checked', "Array.isArray(" + value + ")?_i(" + value + "," + valueBinding + ")>-1" + ('true' === trueValueBinding ? ":(" + value + ")" : ":_q(" + value + "," + trueValueBinding + ")")), addHandler(el, 'change', "var $$a=" + value + ",$$el=$event.target,$$c=$$el.checked?(" + trueValueBinding + "):(" + falseValueBinding + ");if(Array.isArray($$a)){var $$v=" + (number ? '_n(' + valueBinding + ')' : valueBinding) + ",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&(" + genAssignmentCode(value, '$$a.concat([$$v])') + ")}else{$$i>-1&&(" + genAssignmentCode(value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))') + ")}}else{" + genAssignmentCode(value, '$$c') + "}", null, !0);
                 else if ('input' === tag && 'radio' === type) number1 = modifiers && modifiers.number, valueBinding1 = getBindingAttr(el, 'value') || 'null', valueBinding1 = number1 ? "_n(" + valueBinding1 + ")" : valueBinding1, addProp(el, 'checked', "_q(" + value + "," + valueBinding1 + ")"), addHandler(el, 'change', genAssignmentCode(value, valueBinding1), null, !0);
@@ -2927,7 +2928,7 @@
                     var type = el.attrsMap.type, value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'], typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
                     if (value$1 && !typeBinding) {
                         var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';
-                        _warn(binding + "=\"" + value$1 + '" conflicts with v-model on the same element because the latter already expands to a value binding internally', el.rawAttrsMap[binding]);
+                        warn$1(binding + "=\"" + value$1 + '" conflicts with v-model on the same element because the latter already expands to a value binding internally', el.rawAttrsMap[binding]);
                     }
                     var ref = modifiers || {}, lazy = ref.lazy, number = ref.number, trim = ref.trim, valueExpression = '$event.target.value';
                     trim && (valueExpression = "$event.target.value.trim()"), number && (valueExpression = "_n(" + valueExpression + ")");
@@ -2936,7 +2937,7 @@
                 }(el, value, modifiers);
                 else {
                     if (!config.isReservedTag(tag)) return genComponentModel(el, value, modifiers), !1;
-                    _warn("<" + el.tag + " v-model=\"" + value + "\">: v-model is not supported on this element type. If you are working with contenteditable, it's recommended to wrap a library dedicated for that purpose inside a custom component.", el.rawAttrsMap['v-model']);
+                    warn$1("<" + el.tag + " v-model=\"" + value + "\">: v-model is not supported on this element type. If you are working with contenteditable, it's recommended to wrap a library dedicated for that purpose inside a custom component.", el.rawAttrsMap['v-model']);
                 }
                 return !0;
             },
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/7412/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/7412/input.js
new file mode 100644
index 0000000000..706479f4e4
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/7412/input.js
@@ -0,0 +1,14 @@
+export function throttleTime(interval) {
+    let currentValue;
+    let timeout;
+
+    return (done) => (value) => {
+        currentValue = value;
+        if (timeout) {
+            return;
+        }
+        timeout = setTimeout(() => {
+            done(currentValue);
+        }, interval);
+    };
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/7412/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/7412/output.js
new file mode 100644
index 0000000000..9292673afe
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/7412/output.js
@@ -0,0 +1,8 @@
+export function throttleTime(interval) {
+    let currentValue, timeout;
+    return (done)=>(value)=>{
+            currentValue = value, timeout || (timeout = setTimeout(()=>{
+                done(currentValue);
+            }, interval));
+        };
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/firebase-firestore/1/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/firebase-firestore/1/output.js
index 6654baee46..7f56ec03a7 100644
--- a/crates/swc_ecma_minifier/tests/fixture/issues/firebase-firestore/1/output.js
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/firebase-firestore/1/output.js
@@ -2369,14 +2369,14 @@
                         }).next(()=>i));
                 }
                 Dn(t, e, n) {
-                    let s;
-                    return this.He.getDocumentsMatchingQuery(t, e, n).next((n)=>(s = n, this.In.getAllMutationBatchesAffectingQuery(t, e))).next((e)=>this.Cn(t, e, s).next((t)=>{
-                            for (const t1 of (s = t, e))for (const e of t1.mutations){
+                    let s, i;
+                    return this.He.getDocumentsMatchingQuery(t, e, n).next((n)=>(s = n, this.In.getAllMutationBatchesAffectingQuery(t, e))).next((e)=>(i = e, this.Cn(t, i, s).next((t)=>{
+                            for (const t1 of (s = t, i))for (const e of t1.mutations){
                                 const n = e.key;
                                 let i = s.get(n);
                                 null == i && (i = Kt.newInvalidDocument(n), s = s.insert(n, i)), Ye(e, i, t1.localWriteTime), i.isFoundDocument() || (s = s.remove(n));
                             }
-                        })).next(()=>(s.forEach((t, n)=>{
+                        }))).next(()=>(s.forEach((t, n)=>{
                             Pe(e, n) || (s = s.remove(t));
                         }), s));
                 }
diff --git a/crates/swc_ecma_minifier/tests/fixture/next/react-pdf-renderer/output.js b/crates/swc_ecma_minifier/tests/fixture/next/react-pdf-renderer/output.js
index 47595670bd..68dd3f71f5 100644
--- a/crates/swc_ecma_minifier/tests/fixture/next/react-pdf-renderer/output.js
+++ b/crates/swc_ecma_minifier/tests/fixture/next/react-pdf-renderer/output.js
@@ -9302,7 +9302,7 @@
         },
         4756: function(e, t, r) {
             var n, i;
-            void 0 !== (n = "function" == typeof (i = function() {
+            void 0 !== (i = "function" == typeof (n = function() {
                 var e = "debug", t = "hyphenChar", n = "minWordLength", i = "object" == typeof r.g ? r.g : "object" == typeof window ? window : "object" == typeof this ? this : {};
                 function o(e) {
                     var t = {};
@@ -9432,14 +9432,14 @@
                         }(i, y, f[D], s, b, v, m, c);
                     };
                 };
-            }) ? i.apply(t, []) : i) && (e.exports = n);
+            }) ? n.apply(t, []) : n) && (e.exports = i);
         },
         3202: function(e, t, r) {
             e.exports = r(4756);
         },
         1487: function(e, t) {
             var r, n;
-            void 0 !== (r = "function" == typeof (n = function() {
+            void 0 !== (n = "function" == typeof (r = function() {
                 return {
                     patterns: [
                         " ",
@@ -14401,7 +14401,7 @@
                         ""
                     ]
                 };
-            }) ? n.apply(t, []) : n) && (e.exports = r);
+            }) ? r.apply(t, []) : r) && (e.exports = n);
         },
         5717: function(e) {
             "function" == typeof Object.create ? e.exports = function(e, t) {