Repository: swc-project/swc (rust)

Issue:
Parameter decorator handling is buggy when a parameter has the same name as the decorator function
### Describe the bug

When parameter decorators are used _and_ one of the parameters has the same name as the decorator, the emit can be incorrect in a couple of ways.

#### Example 1

Input:
```ts
import { field } from "../validation/decorators";
class C {
	constructor(
		@field("a") readonly field: string,
		@field("b") readonly b: string,
	) {}
}
```

In this example, the import gets rewritten to `field1`, but only one invocation of `field()` is rewritten, leading to a runtime error.

Output:
```ts
var _class;
import { field as field1 } from "../validation/decorators";
var _dec = typeof Reflect !== "undefined" && typeof Reflect.metadata === "function" && Reflect.metadata("design:paramtypes", [
    String,
    String
]), _dec1 = typeof Reflect !== "undefined" && typeof Reflect.metadata === "function" && Reflect.metadata("design:type", Function), _dec2 = function(target, key) {
    return field("b")(target, undefined, 1);
}, _dec3 = function(target, key) {
    return field1("a")(target, undefined, 0);
};
let C = _class = _dec3(_class = _dec2(_class = _dec1(_class = _dec((_class = class C {
    constructor(field, b){
        this.field = field;
        this.b = b;
    }
}) || _class) || _class) || _class) || _class) || _class;
```

#### Example 2
If the first parameter's decorator is removed, the import gets totally erased.

Input:
```ts
import { field } from "../validation/decorators";
class C {
	constructor(
		readonly field: string,
		@field("b") readonly b: string,
	) {}
}
```

Output:
```ts
var _class;
var _dec = typeof Reflect !== "undefined" && typeof Reflect.metadata === "function" && Reflect.metadata("design:paramtypes", [
    String,
    String
]), _dec1 = typeof Reflect !== "undefined" && typeof Reflect.metadata === "function" && Reflect.metadata("design:type", Function), _dec2 = function(target, key) {
    return field("b")(target, undefined, 1);
};
let C = _class = _dec2(_class = _dec1(_class = _dec((_class = class C {
    constructor(field, b){
        this.field = field;
        this.b = b;
    }
}) || _class) || _class) || _class) || _class;
export { };

```



### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true
    },
    "target": "es2022",
    "loose": false,
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "minify": {
      "compress": false,
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "isModule": true
}
```


### Playground link

https://play.swc.rs/?version=1.2.151&code=H4sIAAAAAAAAA03MQQrDIBBG4bWe4sdVAiXZt5tCTzJREwTjlNEWgnj3hmbj%2Bn28sL9ZCirW4KNDwyq8w0zT%2FKUYHJXAaXbeslBhyeahbaSc8ULVynLKRT72LINW6vl%2FDIbMCPHkOMXj%2Bt5xupC2W6eWXi2dGFGbbj8fAIdEmQAAAA%3D%3D&config=H4sIAAAAAAAAA02OMQ6DMAxF7%2BKZoWLowNyVQ1jBoFRJHNlGaoR69yYUEJtl%2F%2Fe%2BN3irg2GDjKIkbdKSDD8wgJVM6sRngw5M62rGoNTBRI4FjUVhMFnpW88oC1mFSPtH31cgMCtdiAkmnVliawi0oCuv0%2FKX3LQjGU5oeNmjT34uDXUcs5DqJY6YlnD2fFuWp7Uttv3%2F%2FaMn3BwH53U8gnvHDyjyl4kIAQAA

### Expected behavior

In the first example, both invocations of `@field()` should be rewritten to `field1`.

In the second example, the import needs to be retained (and possibly still rewritten).

### Actual behavior

_No response_

### Version

1.2.152

### Additional context

_No response_

Pull request:
fix(es/resolver): Fix handling of decorators
**Description:**

**Related issue (if exists):**


 - Closes #3960

Patch:
diff --git a/crates/swc_ecma_codegen/src/typescript.rs b/crates/swc_ecma_codegen/src/typescript.rs
index 1938604545..2ed7f42c0f 100644
--- a/crates/swc_ecma_codegen/src/typescript.rs
+++ b/crates/swc_ecma_codegen/src/typescript.rs
@@ -638,6 +638,10 @@ where
 
         self.emit_accessibility(n.accessibility)?;
 
+        for dec in &n.decorators {
+            emit!(dec);
+        }
+
         if n.is_override {
             keyword!("override");
             space!();
diff --git a/crates/swc_ecma_transforms_base/src/resolver/mod.rs b/crates/swc_ecma_transforms_base/src/resolver/mod.rs
index 94c657b813..170b730a48 100644
--- a/crates/swc_ecma_transforms_base/src/resolver/mod.rs
+++ b/crates/swc_ecma_transforms_base/src/resolver/mod.rs
@@ -565,6 +565,8 @@ impl<'a> VisitMut for Resolver<'a> {
     fn visit_mut_class_decl(&mut self, n: &mut ClassDecl) {
         self.modify(&mut n.ident, None);
 
+        n.class.decorators.visit_mut_with(self);
+
         // Create a child scope. The class name is only accessible within the class.
         let child_mark = Mark::fresh(Mark::root());
 
@@ -597,6 +599,10 @@ impl<'a> VisitMut for Resolver<'a> {
     fn visit_mut_class_method(&mut self, m: &mut ClassMethod) {
         m.key.visit_mut_with(self);
 
+        for p in m.function.params.iter_mut() {
+            p.decorators.visit_mut_with(self);
+        }
+
         {
             let child_mark = Mark::fresh(Mark::root());
 
@@ -631,6 +637,17 @@ impl<'a> VisitMut for Resolver<'a> {
     fn visit_mut_constructor(&mut self, c: &mut Constructor) {
         let child_mark = Mark::fresh(Mark::root());
 
+        for p in c.params.iter_mut() {
+            match p {
+                ParamOrTsParamProp::TsParamProp(p) => {
+                    p.decorators.visit_mut_with(self);
+                }
+                ParamOrTsParamProp::Param(p) => {
+                    p.decorators.visit_mut_with(self);
+                }
+            }
+        }
+
         // Child folder
         let mut folder = Resolver::new(
             Scope::new(ScopeKind::Fn, child_mark, Some(&self.current)),
@@ -696,6 +713,8 @@ impl<'a> VisitMut for Resolver<'a> {
     fn visit_mut_fn_decl(&mut self, node: &mut FnDecl) {
         // We don't fold this as Hoister handles this.
 
+        node.function.decorators.visit_mut_with(self);
+
         {
             let child_mark = Mark::fresh(Mark::root());
 
@@ -710,6 +729,8 @@ impl<'a> VisitMut for Resolver<'a> {
     }
 
     fn visit_mut_fn_expr(&mut self, e: &mut FnExpr) {
+        e.function.decorators.visit_mut_with(self);
+
         let child_mark = Mark::fresh(Mark::root());
 
         // Child folder
@@ -789,6 +810,10 @@ impl<'a> VisitMut for Resolver<'a> {
     }
 
     fn visit_mut_ident(&mut self, i: &mut Ident) {
+        if i.span.ctxt != SyntaxContext::empty() {
+            return;
+        }
+
         match self.ident_type {
             IdentType::Binding => self.modify(i, None),
             IdentType::Ref => {


Test patch:
diff --git a/crates/swc_ecma_transforms_base/tests/fixture.rs b/crates/swc_ecma_transforms_base/tests/fixture.rs
index 3b9c740c1b..684e748b94 100644
--- a/crates/swc_ecma_transforms_base/tests/fixture.rs
+++ b/crates/swc_ecma_transforms_base/tests/fixture.rs
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
 use swc_common::{chain, sync::Lrc, Mark, SourceMap, SyntaxContext};
 use swc_ecma_ast::*;
 use swc_ecma_codegen::Emitter;
-use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax};
+use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax, TsConfig};
 use swc_ecma_transforms_base::{
     fixer::fixer,
     resolver::{resolver_with_mark, ts_resolver},
@@ -90,15 +90,22 @@ fn test_resolver(input: PathBuf) {
 
 #[fixture("tests/ts-resolver/**/input.ts")]
 fn test_ts_resolver(input: PathBuf) {
-    run(Syntax::Typescript(Default::default()), &input, || {
-        let top_level_mark = Mark::fresh(Mark::root());
-
-        chain!(
-            ts_resolver(top_level_mark),
-            as_folder(TsHygiene { top_level_mark }),
-            fixer(None)
-        )
-    });
+    run(
+        Syntax::Typescript(TsConfig {
+            decorators: true,
+            ..Default::default()
+        }),
+        &input,
+        || {
+            let top_level_mark = Mark::fresh(Mark::root());
+
+            chain!(
+                ts_resolver(top_level_mark),
+                as_folder(TsHygiene { top_level_mark }),
+                fixer(None)
+            )
+        },
+    );
 }
 
 struct TsHygiene {
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/1/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/1/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/1/input.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/1/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/1/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/1/output.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/2/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/2/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/2/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/2/input.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/2/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/2/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1517/2/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1517/2/output.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/1/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/1/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/1/input.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/1/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/1/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/1/output.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/2/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/2/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/2/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/2/input.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/2/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/2/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1653/2/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1653/2/output.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1869/case1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1869/case1/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1869/case1/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1869/case1/input.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1869/case1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1869/case1/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/1869/case1/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/1869/case1/output.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/2021/1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/2021/1/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/2021/1/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/2021/1/input.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issue/2021/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/2021/1/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue/2021/1/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/2021/1/output.ts
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/issues/2627/1/input.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/input.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/2627/1/input.ts
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/issues/2627/1/output.ts
similarity index 100%
rename from crates/swc_ecma_transforms_base/tests/ts-resolver/issue-2627/1/output.ts
rename to crates/swc_ecma_transforms_base/tests/ts-resolver/issues/2627/1/output.ts
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/1/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/1/input.ts
new file mode 100644
index 0000000000..4906f00edb
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/1/input.ts
@@ -0,0 +1,7 @@
+import { field } from "../validation/decorators";
+class C {
+    constructor(
+        @field("a") readonly field: string,
+        @field("b") readonly b: string,
+    ) { }
+}
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/1/output.ts
new file mode 100644
index 0000000000..83a0db41a4
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/1/output.ts
@@ -0,0 +1,6 @@
+import { field } from "../validation/decorators";
+class C {
+    constructor(@field("a")
+    readonly field__2: string, @field("b")
+    readonly b__2: string){}
+}
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/2/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/2/input.ts
new file mode 100644
index 0000000000..be1f50473c
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/2/input.ts
@@ -0,0 +1,7 @@
+import { field } from "../validation/decorators";
+class C {
+    method(
+        @field("a") readonly field: string,
+        @field("b") readonly b: string,
+    ) { }
+}
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/2/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/2/output.ts
new file mode 100644
index 0000000000..fb6579d7fc
--- /dev/null
+++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/issues/3960/2/output.ts
@@ -0,0 +1,8 @@
+import { field } from "../validation/decorators";
+class C {
+    method(
+    @field("a")
+    field__2: string, 
+    @field("b")
+    b__2: string) {}
+}