Repository: swc-project/swc (rust)

Issue:
Anonymous class TS method decorator broken
### Describe the bug

SWC fails to apply legacy TS method decorators for anonymous classes.

### Input code

```typescript
const foo = class { @foo foo(v){return v} }
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true
    },
    "target": "es5",
    "loose": false,
    "minify": {
      "compress": false,
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "isModule": true
}
```


### Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.3.74&code=H4sIAAAAAAAAA0vOzysuUUjLz1ewVUjOSSwuVqhWcABxgVijTLO6KLWktChPoaxWoRYAm3SIQysAAAA%3D&config=H4sIAAAAAAAAA2WPQQ7DIAwE73lF5HOv7aF%2F6CMQcSIqwMh2pEZR%2Fl4CCW3VG97Bu96163t4ioV7v%2BZnHpJhQW5zVmSJal5ZAV0SimWXFC4nVdnRaLxg0wa0xEaJJSPlGQvYKgc1PKHufijXwwg8keCvEQQX3bh8n2IpJEaRv8Rg4uTbfo3rjkgINMwFHhX3GjX%2BBp9PZ1gzBiePc7OU2N6FM0VqLgEAAA%3D%3D

### SWC Info output

_No response_

### Expected behavior

This should not error with `_class is not defined`

### Actual behavior

Errors with `_class is not defined`

### Version

1.3.74

### Additional context

_No response_

Pull request description:
fix(es/decorators): Fix a regression about class expressions
**Related issue:**

 - Closes #8095

Patch:
diff --git a/crates/swc_ecma_transforms_proposal/src/decorators/legacy/mod.rs b/crates/swc_ecma_transforms_proposal/src/decorators/legacy/mod.rs
index cabada213e..ef28673e0c 100644
--- a/crates/swc_ecma_transforms_proposal/src/decorators/legacy/mod.rs
+++ b/crates/swc_ecma_transforms_proposal/src/decorators/legacy/mod.rs
@@ -30,6 +30,7 @@ pub(super) fn new(metadata: bool) -> TscDecorator {
         class_name: Default::default(),
         constructor_exprs: Default::default(),
         exports: Default::default(),
+        assign_class_expr_to: Default::default(),
     }
 }
 
@@ -49,6 +50,8 @@ pub(super) struct TscDecorator {
     constructor_exprs: Vec<Box<Expr>>,
 
     exports: Vec<ExportSpecifier>,
+
+    assign_class_expr_to: Option<Ident>,
 }
 
 impl TscDecorator {
@@ -291,14 +294,49 @@ impl VisitMut for TscDecorator {
         self.class_name = old;
     }
 
+    fn visit_mut_expr(&mut self, e: &mut Expr) {
+        e.visit_mut_children_with(self);
+
+        if let Some(var_name) = self.assign_class_expr_to.take() {
+            *e = Expr::Assign(AssignExpr {
+                span: DUMMY_SP,
+                op: op!("="),
+                left: var_name.into(),
+                right: Box::new(e.take()),
+            });
+        }
+    }
+
     fn visit_mut_class_expr(&mut self, n: &mut ClassExpr) {
         let old = self.class_name.take();
+
         if contains_decorator(n) && n.ident.is_none() {
-            n.ident = Some(private_ident!("_class"));
-        }
+            let ident = private_ident!("_$class");
 
+            let var_name = private_ident!("_class");
+
+            self.vars.push(VarDeclarator {
+                span: DUMMY_SP,
+                name: Pat::Ident(var_name.clone().into()),
+                init: None,
+                definite: Default::default(),
+            });
+
+            self.class_name = Some(var_name.clone());
+            n.ident = Some(ident);
+
+            n.visit_mut_children_with(self);
+
+            self.class_name = old;
+
+            self.assign_class_expr_to = Some(var_name);
+
+            return;
+        }
         if let Some(ident) = &n.ident {
-            self.class_name = Some(ident.clone());
+            if self.class_name.is_none() {
+                self.class_name = Some(ident.clone());
+            }
         }
 
         n.visit_mut_children_with(self);
@@ -405,6 +443,46 @@ impl VisitMut for TscDecorator {
 
     fn visit_mut_module_item(&mut self, module_item: &mut ModuleItem) {
         match module_item {
+            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
+                decl: DefaultDecl::Class(c),
+                ..
+            })) => {
+                c.visit_mut_with(self);
+
+                if self.assign_class_expr_to.is_none() {
+                    if let Some(var_name) = c.ident.clone() {
+                        self.vars.push(VarDeclarator {
+                            span: DUMMY_SP,
+                            name: Pat::Ident(var_name.clone().into()),
+                            init: None,
+                            definite: Default::default(),
+                        });
+
+                        self.assign_class_expr_to = Some(var_name);
+                    }
+                }
+
+                if let Some(var_name) = self.assign_class_expr_to.take() {
+                    *module_item = ModuleItem::Stmt(
+                        Expr::Assign(AssignExpr {
+                            span: DUMMY_SP,
+                            op: op!("="),
+                            left: var_name.clone().into(),
+                            right: Box::new(Expr::Class(c.take())),
+                        })
+                        .into_stmt(),
+                    );
+
+                    self.exports
+                        .push(ExportSpecifier::Named(ExportNamedSpecifier {
+                            span: DUMMY_SP,
+                            orig: ModuleExportName::Ident(var_name),
+                            exported: Some(ModuleExportName::Ident(quote_ident!("default"))),
+                            is_type_only: Default::default(),
+                        }));
+                }
+            }
+
             ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(n)) => {
                 let export_decl_span = n.span;
 


Test patch:
diff --git a/crates/swc/tests/fixture/issues-2xxx/2086/output/index.ts b/crates/swc/tests/fixture/issues-2xxx/2086/output/index.ts
index 1383be4172..0b4a16ef99 100644
--- a/crates/swc/tests/fixture/issues-2xxx/2086/output/index.ts
+++ b/crates/swc/tests/fixture/issues-2xxx/2086/output/index.ts
@@ -1,1 +1,1 @@
-export default class t{#t;constructor(t){this.#t=t}#e(){setTimeout(()=>{this.#t.textContent="TESTED"},1e3)}run(){this.#e()}}
+var t;t=class{#t;constructor(t){this.#t=t}#e(){setTimeout(()=>{this.#t.textContent="TESTED"},1e3)}run(){this.#e()}};export{t as default};
diff --git a/crates/swc/tests/fixture/issues-4xxx/4063/1/output/index.ts b/crates/swc/tests/fixture/issues-4xxx/4063/1/output/index.ts
index 0d75197fe5..3d98d3b0af 100644
--- a/crates/swc/tests/fixture/issues-4xxx/4063/1/output/index.ts
+++ b/crates/swc/tests/fixture/issues-4xxx/4063/1/output/index.ts
@@ -17,13 +17,14 @@ define([
     function state() {}
     class Controller {
     }
-    class _class extends Controller {
+    var _class;
+    _class = class _$class extends Controller {
         onChange() {}
         constructor(...args){
             super(...args);
             _define_property._(this, "isTest", false);
         }
-    }
+    };
     _ts_decorate._([
         state
     ], _class.prototype, "isTest", void 0);
diff --git a/crates/swc/tests/fixture/issues-4xxx/4127/1/output/index.ts b/crates/swc/tests/fixture/issues-4xxx/4127/1/output/index.ts
index cd641dd9a9..79c0f0319c 100644
--- a/crates/swc/tests/fixture/issues-4xxx/4127/1/output/index.ts
+++ b/crates/swc/tests/fixture/issues-4xxx/4127/1/output/index.ts
@@ -12,7 +12,8 @@ const _ts_decorate = require("@swc/helpers/_/_ts_decorate");
 function test(constructor) {
     console.log(constructor);
 }
-let _class = class _class {
+var _class;
+_class = class _$class {
 };
 _class = _ts_decorate._([
     test
diff --git a/crates/swc/tests/fixture/issues-4xxx/4127/2/output/index.ts b/crates/swc/tests/fixture/issues-4xxx/4127/2/output/index.ts
index 0e94c0077e..933630d1de 100644
--- a/crates/swc/tests/fixture/issues-4xxx/4127/2/output/index.ts
+++ b/crates/swc/tests/fixture/issues-4xxx/4127/2/output/index.ts
@@ -2,7 +2,8 @@ import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
 function test(constructor) {
     console.log(constructor);
 }
-let _class = class _class {
+var _class;
+_class = class _$class {
 };
 _class = _ts_decorate([
     test
diff --git a/crates/swc/tests/fixture/issues-5xxx/5626/output/index.ts b/crates/swc/tests/fixture/issues-5xxx/5626/output/index.ts
index 89b75e4842..aebcd882fd 100644
--- a/crates/swc/tests/fixture/issues-5xxx/5626/output/index.ts
+++ b/crates/swc/tests/fixture/issues-5xxx/5626/output/index.ts
@@ -5,13 +5,14 @@ import { _ as _ts_metadata } from "@swc/helpers/_/_ts_metadata";
 function Prop() {
     return function() {};
 }
-var Example = function Example() {
+var Example;
+Example = function Example() {
     "use strict";
     _class_call_check(this, Example);
     _define_property(this, "prop", void 0);
 };
-export { Example as default };
 _ts_decorate([
     Prop(),
     _ts_metadata("design:type", typeof BigInt === "undefined" ? Object : BigInt)
 ], Example.prototype, "prop", void 0);
+export { Example as default };
diff --git a/crates/swc/tests/fixture/issues-6xxx/6984/1/output/index.js b/crates/swc/tests/fixture/issues-6xxx/6984/1/output/index.js
index 7f232e2b38..277633eb2c 100644
--- a/crates/swc/tests/fixture/issues-6xxx/6984/1/output/index.js
+++ b/crates/swc/tests/fixture/issues-6xxx/6984/1/output/index.js
@@ -5,7 +5,8 @@ import { _ as _create_super } from "@swc/helpers/_/_create_super";
 import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
 import React from "react";
 import { withRouter } from "react-router-dom";
-var App = /*#__PURE__*/ function(_React_Component) {
+var App;
+App = /*#__PURE__*/ function(_React_Component) {
     "use strict";
     _inherits(App, _React_Component);
     var _super = _create_super(App);
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es2020/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8095/es2020/input/.swcrc
new file mode 100644
index 0000000000..48e9549571
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es2020/input/.swcrc
@@ -0,0 +1,20 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es2020",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es2020/input/1.js b/crates/swc/tests/fixture/issues-8xxx/8095/es2020/input/1.js
new file mode 100644
index 0000000000..0d2248715d
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es2020/input/1.js
@@ -0,0 +1,1 @@
+const foo = class { @foo foo(v) { return v } }
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es2020/output/1.js b/crates/swc/tests/fixture/issues-8xxx/8095/es2020/output/1.js
new file mode 100644
index 0000000000..176b673405
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es2020/output/1.js
@@ -0,0 +1,10 @@
+import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
+var _class;
+const foo = _class = class _$class {
+    foo(v) {
+        return v;
+    }
+};
+_ts_decorate([
+    foo
+], _class.prototype, "foo", null);
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es2022/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8095/es2022/input/.swcrc
new file mode 100644
index 0000000000..79deba5407
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es2022/input/.swcrc
@@ -0,0 +1,20 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es2022",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es2022/input/1.js b/crates/swc/tests/fixture/issues-8xxx/8095/es2022/input/1.js
new file mode 100644
index 0000000000..0d2248715d
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es2022/input/1.js
@@ -0,0 +1,1 @@
+const foo = class { @foo foo(v) { return v } }
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es2022/output/1.js b/crates/swc/tests/fixture/issues-8xxx/8095/es2022/output/1.js
new file mode 100644
index 0000000000..176b673405
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es2022/output/1.js
@@ -0,0 +1,10 @@
+import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
+var _class;
+const foo = _class = class _$class {
+    foo(v) {
+        return v;
+    }
+};
+_ts_decorate([
+    foo
+], _class.prototype, "foo", null);
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es5/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8095/es5/input/.swcrc
new file mode 100644
index 0000000000..bda84228d8
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es5/input/.swcrc
@@ -0,0 +1,20 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es5",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es5/input/1.js b/crates/swc/tests/fixture/issues-8xxx/8095/es5/input/1.js
new file mode 100644
index 0000000000..0d2248715d
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es5/input/1.js
@@ -0,0 +1,1 @@
+const foo = class { @foo foo(v) { return v } }
diff --git a/crates/swc/tests/fixture/issues-8xxx/8095/es5/output/1.js b/crates/swc/tests/fixture/issues-8xxx/8095/es5/output/1.js
new file mode 100644
index 0000000000..81280871b3
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8095/es5/output/1.js
@@ -0,0 +1,22 @@
+import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";
+import { _ as _create_class } from "@swc/helpers/_/_create_class";
+import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
+var _class;
+var foo = _class = /*#__PURE__*/ function() {
+    "use strict";
+    function _$class() {
+        _class_call_check(this, _$class);
+    }
+    _create_class(_$class, [
+        {
+            key: "foo",
+            value: function foo(v) {
+                return v;
+            }
+        }
+    ]);
+    return _$class;
+}();
+_ts_decorate([
+    foo
+], _class.prototype, "foo", null);
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/class-decorators/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/class-decorators/output.ts
index f41d99f64d..9453e3c00b 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/class-decorators/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/class-decorators/output.ts
@@ -1,4 +1,5 @@
-let A = class A {
+var A;
+A = class A {
 };
 A = _ts_decorate([
     dec
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/method-decorators/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/method-decorators/output.ts
index ca70b9aaaf..077135ac8f 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/method-decorators/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/decl-to-expression/method-decorators/output.ts
@@ -1,6 +1,7 @@
-export default class A {
+var A;
+A = class A {
     foo() {}
-}
+};
 _ts_decorate([
     dec
 ], A.prototype, "foo", null);
@@ -10,3 +11,4 @@ class B {
 _ts_decorate([
     dec
 ], B.prototype, "foo", null);
+export { A as default };
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/879/1/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/879/1/output.ts
index 327e3d7bde..cbc3ca94b1 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/879/1/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/879/1/output.ts
@@ -1,6 +1,8 @@
-export default class X {
+var X;
+X = class X {
     prop: string = "";
-}
+};
 _ts_decorate([
     networked
 ], X.prototype, "prop", void 0);
+export { X as default };
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-10264/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-10264/output.ts
index 60aa8e84b0..0cd07d6532 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-10264/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-10264/output.ts
@@ -1,5 +1,6 @@
 function myDecorator(decoratee) {}
-let _class = class _class {
+var _class;
+_class = class _$class {
 };
 _class = _ts_decorate([
     myDecorator
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-8041/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-8041/output.ts
index 272c9f69e7..faea353218 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-8041/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/regression-8041/output.ts
@@ -1,6 +1,8 @@
-export default class _class {
+var _class;
+_class = class _$class {
     bar() {}
-}
+};
 _ts_decorate([
     foo
 ], _class.prototype, "bar", null);
+export { _class as default };