Repository: swc-project/swc (rust)

Issue:
"Duplicate Binding" error for redeclared functions
### Describe the bug

When attempting to run `swc @ >=1.2.122` against a file that has redeclared a function, we receive the message "error: Duplicate Binding".

This is currently causing our jest tests to fail due to a 3rd party library that is written this way.

Functionality works fine in `v1.2.121` and lower.

Thanks!

### Input code

```typescript
function test() {}
function test(a, b) {
    console.log(a, b);
}
```


### Config

```json
{}
```


### Playground link

https://play.swc.rs/?version=1.2.126&code=H4sIAAAAAAAAA0srzUsuyczPUyhJLS7R0FSoruVKQxFK1FFIAgpzKQBBcn5ecX5Oql5OfjpE3JqrFgAkfkZoQQAAAA%3D%3D&config=H4sIAAAAAAAAAy2NTQoCMQxG75J1ERV0MXdw6QFizQyV%2FpFkwFLm7pOO7sL7Hi8dPuJh6lCRhXhc0rLiFyYgn1A8h6rgTDOkvNLm4IVCT46mnGxR5IV0%2BHI9X25GYilCP9tBCjnMbYQT5iUanzEKbdZJ5b0O0EFbpaNwB%2BNBHv%2FleLgDVeaBr6QAAAA%3D

### Expected behavior

The output from v1.2.221 or lower should work fine, simply pass through both of the function declarations as is.

### Actual behavior

error: Duplicate binding
 --> input.jsx:2:10
  |
2 | function test(a, b) {
  |          ^^^^
  |
note: test was declared at here
 --> input.jsx:1:10
  |
1 | function test() {}
  |          ^^^^

### Version

1.2.122+

### Additional context

_No response_

Pull request:
fix(es/lints): Fix incorrect duplicate binding error
swc_ecma_transforms_base:
 - `resolver`: Fix resolving of `const` and `let`.

swc_ecma_lints:
 - `duplicate_bindings`: Don't mark ambient function declararions as a binding. (Closes #3193)


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e0b07cd2b6..32e978e62a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2998,8 +2998,12 @@ dependencies = [
  "swc_atoms",
  "swc_common",
  "swc_ecma_ast",
+ "swc_ecma_codegen",
+ "swc_ecma_parser",
+ "swc_ecma_transforms_base",
  "swc_ecma_utils",
  "swc_ecma_visit",
+ "testing",
 ]
 
 [[package]]
diff --git a/crates/swc_ecma_lints/Cargo.toml b/crates/swc_ecma_lints/Cargo.toml
index 79e60617af..a144eda020 100644
--- a/crates/swc_ecma_lints/Cargo.toml
+++ b/crates/swc_ecma_lints/Cargo.toml
@@ -17,3 +17,9 @@ swc_common = {version = "0.16.0", path = "../swc_common"}
 swc_ecma_ast = {version = "0.62.0", path = "../swc_ecma_ast"}
 swc_ecma_utils = {version = "0.59.0", path = "../swc_ecma_utils"}
 swc_ecma_visit = {version = "0.48.0", path = "../swc_ecma_visit"}
+
+[dev-dependencies]
+swc_ecma_codegen = {version = "0.86.0", path = "../swc_ecma_codegen"}
+swc_ecma_parser = {version = "0.84.0", path = "../swc_ecma_parser"}
+swc_ecma_transforms_base = {version = "0.52.0", path = "../swc_ecma_transforms_base"}
+testing = {version = "0.17.0", path = "../testing"}
diff --git a/crates/swc_ecma_lints/src/rules/duplicate_bindings.rs b/crates/swc_ecma_lints/src/rules/duplicate_bindings.rs
index cdb0acf3a3..799e776fb1 100644
--- a/crates/swc_ecma_lints/src/rules/duplicate_bindings.rs
+++ b/crates/swc_ecma_lints/src/rules/duplicate_bindings.rs
@@ -77,7 +77,9 @@ impl Visit for DuplicateBindings {
     }
 
     fn visit_fn_decl(&mut self, d: &FnDecl) {
-        self.add(&d.ident, false);
+        if d.function.body.is_some() {
+            self.add(&d.ident, false);
+        }
 
         d.visit_children_with(self);
     }
diff --git a/crates/swc_ecma_transforms_base/src/resolver/mod.rs b/crates/swc_ecma_transforms_base/src/resolver/mod.rs
index 2162a0524f..f8bd9b935f 100644
--- a/crates/swc_ecma_transforms_base/src/resolver/mod.rs
+++ b/crates/swc_ecma_transforms_base/src/resolver/mod.rs
@@ -5,11 +5,12 @@ use swc_common::{collections::AHashSet, Mark, SyntaxContext};
 use swc_ecma_ast::*;
 use swc_ecma_utils::{find_ids, Id};
 use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
+use tracing::{debug, span, Level};
 
 #[cfg(test)]
 mod tests;
 
-const LOG: bool = false;
+const LOG: bool = false && cfg!(debug_assertions);
 
 /// See [resolver_with_mark] for docs.
 pub fn resolver() -> impl 'static + Fold + VisitMut {
@@ -119,6 +120,14 @@ impl<'a> Scope<'a> {
             declared_types: Default::default(),
         }
     }
+
+    fn is_declared(&self, symbol: &JsWord) -> bool {
+        if self.declared_symbols.contains(symbol) {
+            return true;
+        }
+
+        self.parent.map_or(false, |p| p.is_declared(symbol))
+    }
 }
 
 /// # Phases
@@ -211,8 +220,8 @@ impl<'a> Resolver<'a> {
     /// Modifies a binding identifier.
     fn modify(&mut self, ident: &mut Ident, kind: Option<VarDeclKind>) {
         if cfg!(debug_assertions) && LOG {
-            eprintln!(
-                "resolver: Binding (type = {}) {}{:?} {:?}",
+            debug!(
+                "Binding (type = {}) {}{:?} {:?}",
                 self.in_type,
                 ident.sym,
                 ident.span.ctxt(),
@@ -233,7 +242,7 @@ impl<'a> Resolver<'a> {
             } else {
                 let span = ident.span.apply_mark(mark);
                 if cfg!(debug_assertions) && LOG {
-                    eprintln!("\t-> {:?}", span.ctxt());
+                    debug!("\t-> {:?}", span.ctxt());
                 }
                 span
             };
@@ -317,7 +326,7 @@ impl<'a> Resolver<'a> {
         } else {
             let span = ident.span.apply_mark(mark);
             if cfg!(debug_assertions) && LOG {
-                eprintln!("\t-> {:?}", span.ctxt());
+                debug!("\t-> {:?}", span.ctxt());
             }
             span
         };
@@ -615,6 +624,12 @@ impl<'a> VisitMut for Resolver<'a> {
     }
 
     fn visit_mut_expr(&mut self, expr: &mut Expr) {
+        let _span = if LOG {
+            Some(span!(Level::ERROR, "visit_mut_expr").entered())
+        } else {
+            None
+        };
+
         self.in_type = false;
         let old = self.ident_type;
         self.ident_type = IdentType::Ref;
@@ -726,8 +741,8 @@ impl<'a> VisitMut for Resolver<'a> {
                 let Ident { span, sym, .. } = i;
 
                 if cfg!(debug_assertions) && LOG {
-                    eprintln!(
-                        "resolver: IdentRef (type = {}) {}{:?}",
+                    debug!(
+                        "IdentRef (type = {}) {}{:?}",
                         self.in_type,
                         sym,
                         span.ctxt()
@@ -742,12 +757,12 @@ impl<'a> VisitMut for Resolver<'a> {
                     let span = span.apply_mark(mark);
 
                     if cfg!(debug_assertions) && LOG {
-                        eprintln!("\t -> {:?}", span.ctxt());
+                        debug!("\t -> {:?}", span.ctxt());
                     }
                     i.span = span;
                 } else {
                     if cfg!(debug_assertions) && LOG {
-                        eprintln!("\t -> Unresolved");
+                        debug!("\t -> Unresolved");
                     }
 
                     let mark = {
@@ -771,7 +786,7 @@ impl<'a> VisitMut for Resolver<'a> {
                     let span = span.apply_mark(mark);
 
                     if cfg!(debug_assertions) && LOG {
-                        eprintln!("\t -> {:?}", span.ctxt());
+                        debug!("\t -> {:?}", span.ctxt());
                     }
 
                     i.span = span;
@@ -917,8 +932,20 @@ impl<'a> VisitMut for Resolver<'a> {
     }
 
     fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
+        let _span = if LOG {
+            Some(span!(Level::ERROR, "visit_mut_stmts").entered())
+        } else {
+            None
+        };
+
         // Phase 1: Handle hoisting
         {
+            let _span = if LOG {
+                Some(span!(Level::ERROR, "hoist").entered())
+            } else {
+                None
+            };
+
             let mut hoister = Hoister {
                 resolver: self,
                 kind: None,
@@ -1386,6 +1413,21 @@ impl VisitMut for Hoister<'_, '_> {
         if self.catch_param_decls.contains(&node.ident.sym) {
             return;
         }
+        let _span = if LOG {
+            Some(span!(Level::ERROR, "Hoister.visit_mut_fn_decl").entered())
+        } else {
+            None
+        };
+
+        if self.in_block {
+            // If we are in nested block, and variable named `foo` is declared, we should
+            // ignore function foo while handling upper scopes.
+            let i = node.ident.clone();
+
+            if self.resolver.current.is_declared(&i.sym) {
+                return;
+            }
+        }
 
         self.resolver.in_type = false;
         self.resolver


Test patch:
diff --git a/crates/swc_ecma_lints/tests/fixture.rs b/crates/swc_ecma_lints/tests/fixture.rs
new file mode 100644
index 0000000000..5a8b00b0e3
--- /dev/null
+++ b/crates/swc_ecma_lints/tests/fixture.rs
@@ -0,0 +1,57 @@
+use std::path::PathBuf;
+
+use swc_common::input::SourceFileInput;
+use swc_ecma_ast::EsVersion;
+use swc_ecma_lints::rules::all;
+use swc_ecma_parser::{lexer::Lexer, Parser, Syntax};
+use swc_ecma_transforms_base::resolver::resolver;
+use swc_ecma_utils::HANDLER;
+use swc_ecma_visit::VisitMutWith;
+
+#[testing::fixture("tests/pass/**/input.js")]
+#[testing::fixture("tests/pass/**/input.ts")]
+fn pass(input: PathBuf) {
+    testing::run_test(false, |cm, handler| {
+        let fm = cm.load_file(&input).unwrap();
+
+        let lexer = Lexer::new(
+            if input.extension().unwrap() == "ts" {
+                Syntax::Typescript(swc_ecma_parser::TsConfig {
+                    ..Default::default()
+                })
+            } else if input.extension().unwrap() == "tsx" {
+                Syntax::Typescript(swc_ecma_parser::TsConfig {
+                    tsx: true,
+                    ..Default::default()
+                })
+            } else {
+                Syntax::Es(swc_ecma_parser::EsConfig {
+                    ..Default::default()
+                })
+            },
+            EsVersion::latest(),
+            SourceFileInput::from(&*fm),
+            None,
+        );
+
+        let mut parser = Parser::new_from(lexer);
+
+        let mut m = parser.parse_module().unwrap();
+        m.visit_mut_with(&mut resolver());
+
+        let rules = all();
+
+        HANDLER.set(&handler, || {
+            for mut rule in rules {
+                rule.lint_module(&m);
+            }
+        });
+
+        if handler.has_errors() {
+            return Err(());
+        }
+
+        Ok(())
+    })
+    .unwrap();
+}
diff --git a/crates/swc_ecma_lints/tests/pass/issue-3193/1/input.ts b/crates/swc_ecma_lints/tests/pass/issue-3193/1/input.ts
new file mode 100644
index 0000000000..ddeea8e27e
--- /dev/null
+++ b/crates/swc_ecma_lints/tests/pass/issue-3193/1/input.ts
@@ -0,0 +1,3 @@
+function test();
+function test(a, b) {
+}
\ No newline at end of file
diff --git a/crates/swc_ecma_lints/tests/pass/vercel/1/input.js b/crates/swc_ecma_lints/tests/pass/vercel/1/input.js
new file mode 100644
index 0000000000..b15210952c
--- /dev/null
+++ b/crates/swc_ecma_lints/tests/pass/vercel/1/input.js
@@ -0,0 +1,25 @@
+
+'use strict';
+
+var n = t(3957);
+function o(e) {
+    const r = e.nextUrl;
+
+
+    if ('/log' !== r.pathname) {
+
+
+        if ('/throw-error-internal' === r.pathname) {
+            function r() {
+                return e();
+            }
+            try {
+                r();
+            } catch (o) {
+                console.error(o);
+            }
+            return new Promise((e, r) => r(new Error('oh no!')));
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/input.ts
new file mode 100644
index 0000000000..85e5a4df72
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/input.ts
@@ -0,0 +1,8 @@
+{
+    let a = 1;
+    const b = 1;
+    var c = 1;
+}
+console.log(a);
+console.log(b);
+console.log(c);
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/output.ts
new file mode 100644
index 0000000000..752c8ab629
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/output.ts
@@ -0,0 +1,7 @@
+{
+    let a__2 = 1;
+    const b__2 = 1;
+    var c = 1;
+}console.log(a);
+console.log(b);
+console.log(c);
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/vercel/1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/vercel/1/input.ts
new file mode 100644
index 0000000000..38cd300630
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/vercel/1/input.ts
@@ -0,0 +1,23 @@
+
+var n = t(3957);
+function o(e) {
+    const r = e.nextUrl;
+
+
+    if ('/log' !== r.pathname) {
+
+
+        if ('/throw-error-internal' === r.pathname) {
+            function r() {
+                return e();
+            }
+            try {
+                r();
+            } catch (o) {
+                console.error(o);
+            }
+            return new Promise((e, r) => r(new Error('oh no!')));
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/vercel/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/vercel/1/output.ts
new file mode 100644
index 0000000000..26a89ffe10
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/vercel/1/output.ts
@@ -0,0 +1,18 @@
+var n = t(3957);
+function o(e__2) {
+    const r__2 = e__2.nextUrl;
+    if ('/log' !== r__2.pathname) {
+        if ('/throw-error-internal' === r__2.pathname) {
+            function r__3() {
+                return e__2();
+            }
+            try {
+                r__3();
+            } catch (o__4) {
+                console.error(o__4);
+            }
+            return new Promise((e__5, r__5)=>r__5(new Error('oh no!'))
+            );
+        }
+    }
+}