Repository: swc-project/swc (rust)

Issue:
name conflict between imports and underscore prefixed function-scoped properties
**Describe the bug**
This issue is related to #2211
This is an additional case where the same problem occurs.
When an imported name is used in a function within a function, it seems that the problem that the variable name is not changed occurs.

**Input code**
```js
// import Bar from './Bar'; has the same issue.
import { bar } from './Bar';

const makeX = (props) => {
  const _bar = props.bar;
  const { list } = _bar;

  return list.map(() => bar);
};
```

**Output**

```js
"use strict";
var _bar = require("./Bar");
var makeX = function(props) {
    var _bar = props.bar;
    var list = _bar.list;
    return list.map(function() {
        return _bar.bar;
    });
};

```

**Config**

```json
{
  "module": {
    "type": "commonjs"
  },
  "jsc": {
    "parser": {
      "syntax": "ecmascript"
    },
    "target": "es5"
  }
}
```

**Expected behavior**
Generated import names should not conflict with function variables.

**Version**
The version of @swc/core: 1.2.90

Pull request:
fix(es/transforms/base): Fix `hygiene`
swc_ecma_transforms_base:
 - `hygiene`: Support usage-def conflict where def comes first. (Closes #2297)

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 95308855a6..df09c0dfd0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2739,7 +2739,7 @@ dependencies = [
 
 [[package]]
 name = "swc_ecma_transforms_base"
-version = "0.31.3"
+version = "0.31.4"
 dependencies = [
  "fxhash",
  "once_cell",
@@ -3402,7 +3402,7 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 
 [[package]]
 name = "wasm"
-version = "1.2.90"
+version = "1.2.91"
 dependencies = [
  "anyhow",
  "console_error_panic_hook",
diff --git a/ecmascript/transforms/base/Cargo.toml b/ecmascript/transforms/base/Cargo.toml
index 961bf16d73..30392ec6fa 100644
--- a/ecmascript/transforms/base/Cargo.toml
+++ b/ecmascript/transforms/base/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2018"
 license = "Apache-2.0/MIT"
 name = "swc_ecma_transforms_base"
 repository = "https://github.com/swc-project/swc.git"
-version = "0.31.3"
+version = "0.31.4"
 
 [dependencies]
 fxhash = "0.2.1"
diff --git a/ecmascript/transforms/base/src/hygiene/mod.rs b/ecmascript/transforms/base/src/hygiene/mod.rs
index 3198f4c831..dd3368e0ea 100644
--- a/ecmascript/transforms/base/src/hygiene/mod.rs
+++ b/ecmascript/transforms/base/src/hygiene/mod.rs
@@ -9,7 +9,7 @@ use std::{cell::RefCell, collections::HashMap};
 use swc_atoms::{js_word, JsWord};
 use swc_common::{chain, util::take::Take, SyntaxContext};
 use swc_ecma_ast::*;
-use swc_ecma_utils::ident::IdentLike;
+use swc_ecma_utils::{ident::IdentLike, Id};
 use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
 
 mod ops;
@@ -44,6 +44,16 @@ struct Hygiene<'a> {
 type Contexts = SmallVec<[SyntaxContext; 32]>;
 
 impl<'a> Hygiene<'a> {
+    /// Check `id` while exiting scope.
+    ///
+    /// We handle this while exiting a scope. See check_enqueued for details.
+    fn enqueue_check(&mut self, id: Id) {
+        if self.current.check_queue.contains(&id) {
+            return;
+        }
+        self.current.check_queue.push(id);
+    }
+
     fn add_decl(&mut self, ident: Ident) {
         let ctxt = ident.span.ctxt();
 
@@ -64,7 +74,7 @@ impl<'a> Hygiene<'a> {
 
         {
             let mut b = self.current.declared_symbols.borrow_mut();
-            let e = b.entry(sym.to_boxed_str()).or_default();
+            let e = b.entry(sym.clone()).or_default();
             if !e.contains(&ctxt) {
                 e.push(ctxt);
             }
@@ -77,7 +87,7 @@ impl<'a> Hygiene<'a> {
 
             while let Some(c) = cur {
                 let mut used = c.used.borrow_mut();
-                let e = used.entry(sym.to_boxed_str()).or_default();
+                let e = used.entry(sym.clone()).or_default();
 
                 if !e.contains(&ctxt) {
                     e.push(ctxt);
@@ -125,14 +135,35 @@ impl<'a> Hygiene<'a> {
 
         let ctxt = ident.span.ctxt();
 
+        {
+            let mut decl_cnt = 0;
+
+            let mut cur = Some(&self.current);
+
+            while let Some(c) = cur {
+                let b = c.declared_symbols.borrow();
+
+                if let Some(ctxts) = b.get(&ident.sym.clone()) {
+                    decl_cnt += ctxts.len();
+                }
+
+                cur = c.parent;
+            }
+
+            if decl_cnt >= 2 {
+                self.enqueue_check(ident.to_id());
+            }
+        }
+
         {
             let mut need_work = false;
+
             let mut is_cur = true;
             let mut cur = Some(&self.current);
 
             while let Some(c) = cur {
                 let mut used = c.used.borrow_mut();
-                let e = used.entry(ident.sym.to_boxed_str()).or_default();
+                let e = used.entry(ident.sym.clone()).or_default();
 
                 if !e.contains(&ctxt) {
                     e.push(ctxt);
@@ -183,7 +214,7 @@ impl<'a> Hygiene<'a> {
             let mut i = 0;
             loop {
                 i += 1;
-                let sym = format!("{}{}", sym, i);
+                let sym = format!("{}{}", sym, i).into();
 
                 if !self.current.is_declared(&sym) {
                     break sym;
@@ -196,9 +227,8 @@ impl<'a> Hygiene<'a> {
         }
 
         let sym = self.current.change_symbol(sym, ctxt);
-        let boxed_sym = sym.to_boxed_str();
         {
-            let scope = self.current.scope_of(&boxed_sym, ctxt, self.var_kind);
+            let scope = self.current.scope_of(&sym, ctxt, self.var_kind);
 
             // Update symbol list
             let mut declared_symbols = scope.declared_symbols.borrow_mut();
@@ -224,12 +254,12 @@ impl<'a> Hygiene<'a> {
                 // );
             }
 
-            let old = declared_symbols.entry(sym.to_boxed_str()).or_default();
+            let old = declared_symbols.entry(sym.clone()).or_default();
             old.retain(|c| *c != ctxt);
             //        debug_assert!(old.is_empty() || old.len() == 1);
 
             let new = declared_symbols
-                .entry(renamed.clone().into_boxed_str())
+                .entry(renamed.clone().into())
                 .or_insert_with(|| Vec::with_capacity(2));
             new.push(ctxt);
             debug_assert!(new.len() == 1);
@@ -276,10 +306,77 @@ impl VisitMut for MarkClearer {
 }
 
 impl<'a> Hygiene<'a> {
+    /// Move `check_queue` to `ops`.
+    ///
+    /// # Implementation notes
+    ////
+    /// This only handles variables declared in current scope.
+    /// You may think it's possible to avoid adding an [Id] to `check_queue`
+    /// entirely, but it's not possible because declarations can come after
+    /// usages.
+    fn check_enqueued(&mut self) {
+        if self.current.check_queue.is_empty() {
+            return;
+        }
+
+        for (sym, ctxt) in self.current.check_queue.take() {
+            // Check if it's in the current scope.
+            if let Some(decls) = self.current.declared_symbols.borrow().get(&sym) {
+                if !decls.contains(&ctxt) {
+                    continue;
+                }
+            }
+
+            {
+                // We check for all used identifiers in current scope.
+                // If `ctxt` is the only one, it's fine.
+                // If other syntax context is used, we need to rename it.
+
+                let mut other_ctxts = vec![];
+
+                let used = self.current.used.borrow();
+
+                if let Some(ctxts) = used.get(&sym) {
+                    'add_loop: for &cx in ctxts {
+                        if cx == ctxt {
+                            continue;
+                        }
+
+                        // Prevent duplicate
+                        if other_ctxts.contains(&cx) {
+                            continue;
+                        }
+
+                        // If a declaration name is going to be renamed, it's not a conflict.
+                        let mut cur = Some(&self.current);
+                        while let Some(c) = cur {
+                            let ops = c.ops.borrow();
+
+                            if ops.rename.contains_key(&(sym.clone(), cx)) {
+                                continue 'add_loop;
+                            }
+                            cur = c.parent;
+                        }
+
+                        other_ctxts.push(cx);
+                    }
+                }
+
+                if other_ctxts.is_empty() {
+                    continue;
+                }
+            }
+
+            self.rename(sym, ctxt);
+        }
+    }
+
     fn apply_ops<N>(&mut self, node: &mut N)
     where
         for<'o> N: VisitMutWith<Operator<'o>>,
     {
+        self.check_enqueued();
+
         let ops = self.current.ops.borrow();
 
         let ids_to_remove = self.current.declared_symbols.borrow();
@@ -300,7 +397,7 @@ impl<'a> Hygiene<'a> {
                 }
 
                 for ((sym, ctxt), _) in &ops.rename {
-                    let e = used.entry(sym.to_boxed_str()).or_default();
+                    let e = used.entry(sym.clone()).or_default();
 
                     if let Some(pos) = e.iter().position(|c| *c == *ctxt) {
                         e.remove(pos);
@@ -406,13 +503,15 @@ struct Scope<'a> {
     /// Kind of the scope.
     pub kind: ScopeKind,
 
-    pub used: RefCell<FxHashMap<Box<str>, Vec<SyntaxContext>>>,
+    pub used: RefCell<FxHashMap<JsWord, Vec<SyntaxContext>>>,
 
     /// All references declared in this scope
-    pub declared_symbols: RefCell<FxHashMap<Box<str>, Vec<SyntaxContext>>>,
+    pub declared_symbols: RefCell<FxHashMap<JsWord, Vec<SyntaxContext>>>,
 
     pub(crate) ops: RefCell<Operations>,
     pub renamed: FxHashSet<JsWord>,
+
+    check_queue: Vec<Id>,
 }
 
 impl<'a> Default for Scope<'a> {
@@ -438,12 +537,13 @@ impl<'a> Scope<'a> {
             ops: Default::default(),
             renamed: Default::default(),
             used: Default::default(),
+            check_queue: Default::default(),
         }
     }
 
     fn scope_of(
         &self,
-        sym: &Box<str>,
+        sym: &JsWord,
         ctxt: SyntaxContext,
         var_kind: Option<VarDeclKind>,
     ) -> &'a Scope<'_> {
@@ -499,7 +599,7 @@ impl<'a> Scope<'a> {
 
         let mut ctxts = smallvec![];
         {
-            if let Some(cxs) = self.declared_symbols.get_mut().get(&*sym) {
+            if let Some(cxs) = self.declared_symbols.get_mut().get(&sym) {
                 if cxs.len() != 1 || cxs[0] != ctxt {
                     ctxts.extend_from_slice(&cxs);
                 }
@@ -509,7 +609,7 @@ impl<'a> Scope<'a> {
         let mut cur = self.parent;
 
         while let Some(scope) = cur {
-            if let Some(cxs) = scope.declared_symbols.borrow().get(&*sym) {
+            if let Some(cxs) = scope.declared_symbols.borrow().get(&sym) {
                 if cxs.len() != 1 || cxs[0] != ctxt {
                     ctxts.extend_from_slice(&cxs);
                 }
@@ -541,7 +641,7 @@ impl<'a> Scope<'a> {
         sym
     }
 
-    fn is_declared(&self, sym: &str) -> bool {
+    fn is_declared(&self, sym: &JsWord) -> bool {
         if self.declared_symbols.borrow().contains_key(sym) {
             return true;
         }
diff --git a/package.json b/package.json
index 70fe983df2..532b96f94c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@swc/core",
-    "version": "1.2.90",
+    "version": "1.2.91",
     "description": "Super-fast alternative for babel",
     "homepage": "https://swc.rs",
     "main": "./index.js",
diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml
index 4ed362e675..eea26a3e07 100644
--- a/wasm/Cargo.toml
+++ b/wasm/Cargo.toml
@@ -6,7 +6,7 @@ license = "Apache-2.0 AND MIT"
 name = "wasm"
 publish = false
 repository = "https://github.com/swc-project/swc.git"
-version = "1.2.90"
+version = "1.2.91"
 
 [lib]
 crate-type = ["cdylib"]


Test patch:
diff --git a/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js b/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js
index 627d3715de..2e71cbe362 100644
--- a/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js
+++ b/ecmascript/minifier/tests/projects/output/react-dom-17.0.2.js
@@ -3365,7 +3365,7 @@
                 isBatchingEventUpdates = !1, finishEventHandler();
             }
         }(function() {
-            var eventSystemFlags, nativeEvent6, nativeEventTarget, dispatchQueue, dispatchQueue1, domEventName, targetInst, nativeEvent1, nativeEventTarget1, eventSystemFlags1, dispatchQueue2, domEventName1, targetInst1, nativeEvent2, nativeEventTarget2;
+            var eventSystemFlags, nativeEvent8, nativeEventTarget, dispatchQueue, dispatchQueue1, domEventName, targetInst, nativeEvent1, nativeEventTarget1, eventSystemFlags1, dispatchQueue2, domEventName1, targetInst1, nativeEvent2, nativeEventTarget2;
             (function(dispatchQueue3, domEventName, targetInst, nativeEvent3, nativeEventTarget3, eventSystemFlags2, targetContainer) {
                 var reactName = topLevelEventsToReactNames.get(domEventName);
                 if (void 0 !== reactName) {
@@ -3447,14 +3447,14 @@
                     }
                     var inCapturePhase = (4 & (eventSystemFlags1 = eventSystemFlags = eventSystemFlags3)) != 0, accumulateTargetOnly = !inCapturePhase && "scroll" === domEventName, _listeners = accumulateSinglePhaseListeners(targetInst = ancestorInst, reactName, nativeEvent3.type, inCapturePhase, accumulateTargetOnly);
                     if (_listeners.length > 0) {
-                        var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent3, nativeEventTarget1 = nativeEventTarget = getEventTarget(nativeEvent6 = nativeEvent4));
+                        var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent3, nativeEventTarget1 = nativeEventTarget = getEventTarget(nativeEvent8 = nativeEvent4));
                         (dispatchQueue1 = dispatchQueue = []).push({
                             event: _event,
                             listeners: _listeners
                         });
                     }
                 }
-            })(dispatchQueue1, domEventName = domEventName2, targetInst, nativeEvent1 = nativeEvent6, nativeEventTarget1, eventSystemFlags1), (7 & eventSystemFlags1) == 0 && ((function(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
+            })(dispatchQueue1, domEventName = domEventName2, targetInst, nativeEvent1 = nativeEvent8, nativeEventTarget1, eventSystemFlags1), (7 & eventSystemFlags1) == 0 && ((function(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
                 var win, from, to, isOverEvent = "mouseover" === domEventName || "pointerover" === domEventName, isOutEvent = "mouseout" === domEventName || "pointerout" === domEventName;
                 if (isOverEvent && (16 & eventSystemFlags1) == 0) {
                     var related = nativeEvent.relatedTarget || nativeEvent.fromElement;
@@ -3540,7 +3540,7 @@
                         null !== customData && (event.data = customData);
                     }
                 }
-            })(dispatchQueue2, domEventName1 = domEventName, targetInst1, nativeEvent2 = nativeEvent1, nativeEventTarget2 = nativeEventTarget1), (function(dispatchQueue, domEventName, targetInst, nativeEvent5, nativeEventTarget) {
+            })(dispatchQueue2, domEventName1 = domEventName, targetInst1, nativeEvent2 = nativeEvent1, nativeEventTarget2 = nativeEventTarget1), (function(dispatchQueue, domEventName, targetInst, nativeEvent7, nativeEventTarget) {
                 var chars;
                 if (!(chars = canUseTextInputEvent ? function(domEventName, nativeEvent) {
                     switch(domEventName){
@@ -3556,14 +3556,14 @@
                         default:
                             return null;
                     }
-                }(domEventName, nativeEvent5) : function(domEventName, nativeEvent) {
-                    var nativeEvent3;
+                }(domEventName, nativeEvent7) : function(domEventName, nativeEvent) {
+                    var nativeEvent5;
                     if (isComposing) return "compositionend" === domEventName || !canUseCompositionEvent && isFallbackCompositionEnd(domEventName, nativeEvent) ? (root2 = null, startText = null, fallbackText = null, isComposing = !1, getData()) : null;
                     switch(domEventName){
                         case "paste":
                             return null;
                         case "keypress":
-                            if (!(((nativeEvent3 = nativeEvent).ctrlKey || nativeEvent3.altKey || nativeEvent3.metaKey) && !(nativeEvent3.ctrlKey && nativeEvent3.altKey))) {
+                            if (!(((nativeEvent5 = nativeEvent).ctrlKey || nativeEvent5.altKey || nativeEvent5.metaKey) && !(nativeEvent5.ctrlKey && nativeEvent5.altKey))) {
                                 if (nativeEvent.char && nativeEvent.char.length > 1) return nativeEvent.char;
                                 if (nativeEvent.which) return String.fromCharCode(nativeEvent.which);
                             }
@@ -3573,10 +3573,10 @@
                         default:
                             return null;
                     }
-                }(domEventName, nativeEvent5))) return null;
+                }(domEventName, nativeEvent7))) return null;
                 var listeners = accumulateTwoPhaseListeners(targetInst1, "onBeforeInput");
                 if (listeners.length > 0) {
-                    var event = new SyntheticInputEvent("onBeforeInput", "beforeinput", null, nativeEvent5, nativeEventTarget2);
+                    var event = new SyntheticInputEvent("onBeforeInput", "beforeinput", null, nativeEvent7, nativeEventTarget2);
                     dispatchQueue2.push({
                         event: event,
                         listeners: listeners
diff --git a/ecmascript/transforms/base/src/hygiene/tests.rs b/ecmascript/transforms/base/src/hygiene/tests.rs
index e98af538c2..148c38b0ec 100644
--- a/ecmascript/transforms/base/src/hygiene/tests.rs
+++ b/ecmascript/transforms/base/src/hygiene/tests.rs
@@ -1582,3 +1582,43 @@ fn issue_2211_2() {
         ",
     );
 }
+
+#[test]
+fn issue_2297_1() {
+    test(
+        |tester| {
+            let mark1 = Mark::fresh(Mark::root());
+            let mark2 = Mark::fresh(Mark::root());
+
+            let stmts = tester
+                .parse_stmts(
+                    "actual1.js",
+                    "
+                    var _bar = require('./Bar');
+                    var makeX = function(props) {
+                        var _bar = props.bar;
+                        var list = _bar.list;
+                        return list.map(function() {
+                            return _bar.bar;
+                        });
+                    };
+                    ",
+                )?
+                .fold_with(&mut OnceMarker::new(&[(
+                    "_bar",
+                    &[mark1, mark2, mark2, mark1],
+                )]));
+            Ok(stmts)
+        },
+        "
+        var _bar = require('./Bar');
+        var makeX = function(props) {
+            var _bar1 = props.bar;
+            var list = _bar1.list;
+            return list.map(function() {
+                return _bar.bar;
+            });
+        };
+        ",
+    );
+}
diff --git a/ecmascript/transforms/compat/tests/es2015_classes.rs b/ecmascript/transforms/compat/tests/es2015_classes.rs
index 9234e42634..befa9ed8ba 100644
--- a/ecmascript/transforms/compat/tests/es2015_classes.rs
+++ b/ecmascript/transforms/compat/tests/es2015_classes.rs
@@ -6151,9 +6151,9 @@ test!(
     'use strict';
     _classCallCheck(this, Foo);
   };
-  let Test = function(Foo) {
+  let Test = function(Foo1) {
     'use strict';
-    _inherits(Test, Foo);
+    _inherits(Test, Foo1);
     function Test() {
         _classCallCheck(this, Test);
         return _possibleConstructorReturn(this, _getPrototypeOf(Test).apply(this, arguments));
diff --git a/ecmascript/transforms/module/tests/fixture/commonjs/issue-2297/input.js b/ecmascript/transforms/module/tests/fixture/commonjs/issue-2297/input.js
new file mode 100644
index 0000000000..1cc2ae6e75
--- /dev/null
+++ b/ecmascript/transforms/module/tests/fixture/commonjs/issue-2297/input.js
@@ -0,0 +1,8 @@
+import { bar } from './Bar';
+
+const makeX = (props) => {
+    const _bar = props.bar;
+    const { list } = _bar;
+
+    return list.map(() => bar);
+};
\ No newline at end of file
diff --git a/ecmascript/transforms/module/tests/fixture/commonjs/issue-2297/output.js b/ecmascript/transforms/module/tests/fixture/commonjs/issue-2297/output.js
new file mode 100644
index 0000000000..80d0a0eb26
--- /dev/null
+++ b/ecmascript/transforms/module/tests/fixture/commonjs/issue-2297/output.js
@@ -0,0 +1,8 @@
+"use strict";
+var _bar = require("./Bar");
+const makeX = (props)=>{
+    const _bar1 = props.bar;
+    const { list  } = _bar1;
+    return list.map(()=>_bar.bar
+    );
+};