Repository: swc-project/swc (rust)

Issue:
Decorated and regular class properties are translated in a wrong order
### Describe the bug

According to Playground demo, swc breaks the definition order of class fields: all the undecorated fields go first, and the decorated ones go last. This behavior leads to some problems if the undecorated field depends on the decorated field value in its class constructor.

TSC compiler doesn't change the order of fields in a constructor.

### Input code

_No response_

### Config

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


### Playground link

https://play.swc.rs/?version=1.3.35&code=H4sIAAAAAAAAA0vOSSwuVnBUqOZSUHDITypOLSpLTMpJVUhUsFUoKSpNteYCSiQBOWmJOcUQXnJ%2BXjFQKrkkv0hDE6yxlquWCwBWMAoXSQAAAA%3D%3D&config=H4sIAAAAAAAAA02Ouw6DMAxF%2F8Uzazswd%2BUjrGBQqiSObCM1Qvx7Ex6lm%2BV77mOFtzroV8goStIuLcnwAz1YyaROfDbowLS%2BJgxKHYzkWNBYFHqThbYqo8xk1UT6qHRgVvrx0Sc%2FlZbtOGYh1VvCNIeLbDmCSSeW2OhAM7ryutqOsr%2F6gQxHNDxXVHvkcWlx6z5%2Bn%2FOE7V5wtnodTnA3fgHD3DNVBQEAAA%3D%3D

### Expected behavior

Fields are translated in an order they were declared.

### Actual behavior

Order of fields is changed.

### Version

1.3.35

### Additional context

_No response_

Pull request description:
fix(es/decorator): Preserve class prop order
<!-- 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):**
- fix #6951 

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 b02153fe75..0a0366b334 100644
--- a/crates/swc_ecma_transforms_proposal/src/decorators/legacy/mod.rs
+++ b/crates/swc_ecma_transforms_proposal/src/decorators/legacy/mod.rs
@@ -20,10 +20,9 @@ enum EnumKind {
     Num,
 }
 
-pub(super) fn new(metadata: bool, use_define_for_class_fields: bool) -> TscDecorator {
+pub(super) fn new(metadata: bool) -> TscDecorator {
     TscDecorator {
         metadata,
-        use_define_for_class_fields,
         enums: Default::default(),
         vars: Default::default(),
         appended_exprs: Default::default(),
@@ -36,7 +35,6 @@ pub(super) fn new(metadata: bool, use_define_for_class_fields: bool) -> TscDecor
 
 pub(super) struct TscDecorator {
     metadata: bool,
-    use_define_for_class_fields: bool,
 
     enums: AHashMap<JsWord, EnumKind>,
 
@@ -352,28 +350,6 @@ impl VisitMut for TscDecorator {
                     key.as_arg(),
                     undefined(DUMMY_SP).as_arg(),
                 );
-
-                if !self.use_define_for_class_fields && !c.is_static {
-                    if let Some(init) = c.value.take() {
-                        self.constructor_exprs
-                            .push(Box::new(Expr::Assign(AssignExpr {
-                                span: c.span,
-                                op: op!("="),
-                                left: PatOrExpr::Expr(Box::new(Expr::Member(MemberExpr {
-                                    span: DUMMY_SP,
-                                    obj: Box::new(Expr::This(ThisExpr { span: DUMMY_SP })),
-                                    prop: match &c.key {
-                                        PropName::Ident(i) => MemberProp::Ident(i.clone()),
-                                        _ => MemberProp::Computed(ComputedPropName {
-                                            span: DUMMY_SP,
-                                            expr: Box::new(prop_name_to_expr_value(c.key.clone())),
-                                        }),
-                                    },
-                                }))),
-                                right: init,
-                            })));
-                    }
-                }
             }
         }
     }
diff --git a/crates/swc_ecma_transforms_proposal/src/decorators/mod.rs b/crates/swc_ecma_transforms_proposal/src/decorators/mod.rs
index 171b449dcf..e858ebd958 100644
--- a/crates/swc_ecma_transforms_proposal/src/decorators/mod.rs
+++ b/crates/swc_ecma_transforms_proposal/src/decorators/mod.rs
@@ -57,10 +57,7 @@ mod legacy;
 /// ```
 pub fn decorators(c: Config) -> impl Fold {
     if c.legacy {
-        Either::Left(as_folder(self::legacy::new(
-            c.emit_metadata,
-            c.use_define_for_class_fields,
-        )))
+        Either::Left(as_folder(self::legacy::new(c.emit_metadata)))
     } else {
         if c.emit_metadata {
             unimplemented!("emitting decorator metadata while using new proposal")
diff --git a/crates/swc_ecma_transforms_typescript/src/strip.rs b/crates/swc_ecma_transforms_typescript/src/strip.rs
index 21aaddacb4..4b0131f528 100644
--- a/crates/swc_ecma_transforms_typescript/src/strip.rs
+++ b/crates/swc_ecma_transforms_typescript/src/strip.rs
@@ -65,7 +65,7 @@ pub struct Config {
     /// When running `tsc` with configuration `"target": "<ES6-ES2020>",
     /// "useDefineForClassFields": true`, TS class fields are transformed to
     /// `Object.defineProperty()` statements. You must additionally apply the
-    /// `swc_ecmascript::transforms::compat::es2022::class_properties()` pass to
+    /// [swc_ecma_transforms_compat::es2022::class_properties()] pass to
     /// get this backward-compatible output.
     #[serde(default)]
     pub use_define_for_class_fields: bool,


Test patch:
diff --git a/crates/swc/tests/exec.rs b/crates/swc/tests/exec.rs
index 8cb8b0eb5f..2deeeeb0b3 100644
--- a/crates/swc/tests/exec.rs
+++ b/crates/swc/tests/exec.rs
@@ -113,6 +113,29 @@ fn init_helpers() -> Arc<PathBuf> {
 }
 
 fn create_matrix(entry: &Path) -> Vec<Options> {
+    // use_define_for_class_fields: false
+    // force to use [[Set]] instead of [[Define]]
+    // EsVersion should be lower than EsVersion::Es2022
+    let force_set_class_field = entry
+        .parent()
+        .map(|parent| parent.join(".swcrc"))
+        .and_then(|path| fs::read_to_string(path).ok())
+        .and_then(|content| {
+            jsonc_parser::parse_to_serde_value(
+                &content,
+                &jsonc_parser::ParseOptions {
+                    allow_comments: true,
+                    allow_trailing_commas: true,
+                    allow_loose_object_property_names: false,
+                },
+            )
+            .ok()?
+        })
+        .and_then(|content| serde_json::from_value::<Config>(content).ok())
+        .and_then(|config| config.jsc.transform.into_inner())
+        .map(|c| c.use_define_for_class_fields == false.into())
+        .unwrap_or(false);
+
     [
         EsVersion::Es2022,
         EsVersion::Es2021,
@@ -125,6 +148,7 @@ fn create_matrix(entry: &Path) -> Vec<Options> {
         EsVersion::Es5,
     ]
     .into_iter()
+    .filter(|e| !force_set_class_field || e < &EsVersion::Es2022)
     .matrix(|| {
         let default_es = Syntax::Es(EsConfig {
             ..Default::default()
diff --git a/crates/swc/tests/fixture/issues-3xxx/3686/1/output/index.ts b/crates/swc/tests/fixture/issues-3xxx/3686/1/output/index.ts
index 25a9dd7e1a..6f18979d86 100644
--- a/crates/swc/tests/fixture/issues-3xxx/3686/1/output/index.ts
+++ b/crates/swc/tests/fixture/issues-3xxx/3686/1/output/index.ts
@@ -10,10 +10,7 @@ const _tsDecorate = require("@swc/helpers/lib/_ts_decorate.js").default;
 const CD = ()=>{};
 const PD = ()=>{};
 let ServiceError = class ServiceError1 extends Error {
-    constructor(...args){
-        super(...args);
-        this.code = ServiceError.Code.badResponse;
-    }
+    code = ServiceError.Code.badResponse;
     name = "ServiceError.BadResponse";
 };
 _tsDecorate([
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/1/input/.swcrc b/crates/swc/tests/fixture/issues-6xxx/6951/1/input/.swcrc
new file mode 100644
index 0000000000..dd0192930e
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/1/input/.swcrc
@@ -0,0 +1,24 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es5",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        },
+        "transform": {
+            "legacyDecorator": true,
+            "decoratorMetadata": true
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/1/input/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/1/input/index.ts
new file mode 100644
index 0000000000..b9963a16ed
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/1/input/index.ts
@@ -0,0 +1,9 @@
+class A {
+    @observable a = true;
+
+    b = false;
+
+    @foo static c = 1;
+
+    constructor() {}
+}
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/1/output/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/1/output/index.ts
new file mode 100644
index 0000000000..734109078c
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/1/output/index.ts
@@ -0,0 +1,15 @@
+import _class_call_check from "@swc/helpers/src/_class_call_check.mjs";
+import _ts_decorate from "@swc/helpers/src/_ts_decorate.mjs";
+var A = function A() {
+    "use strict";
+    _class_call_check(this, A);
+    this.a = true;
+    this.b = false;
+};
+A.c = 1;
+_ts_decorate([
+    observable
+], A.prototype, "a", void 0);
+_ts_decorate([
+    foo
+], A, "c", void 0);
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/2/input/.swcrc b/crates/swc/tests/fixture/issues-6xxx/6951/2/input/.swcrc
new file mode 100644
index 0000000000..81201c6d4d
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/2/input/.swcrc
@@ -0,0 +1,24 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es2022",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        },
+        "transform": {
+            "legacyDecorator": true,
+            "decoratorMetadata": true
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/2/input/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/2/input/index.ts
new file mode 100644
index 0000000000..b9963a16ed
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/2/input/index.ts
@@ -0,0 +1,9 @@
+class A {
+    @observable a = true;
+
+    b = false;
+
+    @foo static c = 1;
+
+    constructor() {}
+}
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/2/output/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/2/output/index.ts
new file mode 100644
index 0000000000..f1e79df1d0
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/2/output/index.ts
@@ -0,0 +1,13 @@
+import _ts_decorate from "@swc/helpers/src/_ts_decorate.mjs";
+class A {
+    a = true;
+    b = false;
+    static c = 1;
+    constructor(){}
+}
+_ts_decorate([
+    observable
+], A.prototype, "a", void 0);
+_ts_decorate([
+    foo
+], A, "c", void 0);
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/3/input/.swcrc b/crates/swc/tests/fixture/issues-6xxx/6951/3/input/.swcrc
new file mode 100644
index 0000000000..7a6270cc10
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/3/input/.swcrc
@@ -0,0 +1,25 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es2015",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        },
+        "transform": {
+            "legacyDecorator": true,
+            "decoratorMetadata": true,
+            "useDefineForClassFields": true
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/3/input/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/3/input/index.ts
new file mode 100644
index 0000000000..b9963a16ed
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/3/input/index.ts
@@ -0,0 +1,9 @@
+class A {
+    @observable a = true;
+
+    b = false;
+
+    @foo static c = 1;
+
+    constructor() {}
+}
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/3/output/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/3/output/index.ts
new file mode 100644
index 0000000000..a498294045
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/3/output/index.ts
@@ -0,0 +1,15 @@
+import _define_property from "@swc/helpers/src/_define_property.mjs";
+import _ts_decorate from "@swc/helpers/src/_ts_decorate.mjs";
+class A {
+    constructor(){
+        _define_property(this, "a", true);
+        _define_property(this, "b", false);
+    }
+}
+_define_property(A, "c", 1);
+_ts_decorate([
+    observable
+], A.prototype, "a", void 0);
+_ts_decorate([
+    foo
+], A, "c", void 0);
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/4/input/.swcrc b/crates/swc/tests/fixture/issues-6xxx/6951/4/input/.swcrc
new file mode 100644
index 0000000000..02b4aeba42
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/4/input/.swcrc
@@ -0,0 +1,25 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "typescript",
+            "tsx": false,
+            "decorators": true
+        },
+        "target": "es5",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        },
+        "transform": {
+            "legacyDecorator": true,
+            "decoratorMetadata": true,
+            "useDefineForClassFields": true
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/4/input/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/4/input/index.ts
new file mode 100644
index 0000000000..b9963a16ed
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/4/input/index.ts
@@ -0,0 +1,9 @@
+class A {
+    @observable a = true;
+
+    b = false;
+
+    @foo static c = 1;
+
+    constructor() {}
+}
diff --git a/crates/swc/tests/fixture/issues-6xxx/6951/4/output/index.ts b/crates/swc/tests/fixture/issues-6xxx/6951/4/output/index.ts
new file mode 100644
index 0000000000..36ffec360e
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-6xxx/6951/4/output/index.ts
@@ -0,0 +1,16 @@
+import _class_call_check from "@swc/helpers/src/_class_call_check.mjs";
+import _define_property from "@swc/helpers/src/_define_property.mjs";
+import _ts_decorate from "@swc/helpers/src/_ts_decorate.mjs";
+var A = function A() {
+    "use strict";
+    _class_call_check(this, A);
+    _define_property(this, "a", true);
+    _define_property(this, "b", false);
+};
+_define_property(A, "c", 1);
+_ts_decorate([
+    observable
+], A.prototype, "a", void 0);
+_ts_decorate([
+    foo
+], A, "c", void 0);
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-metadata/issues/3979/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-metadata/issues/3979/output.ts
index 870f525a22..9701797896 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-metadata/issues/3979/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-metadata/issues/3979/output.ts
@@ -1,8 +1,7 @@
 class A {
-    x;
+    x = 1;
     constructor();
     constructor(){
-        this.x = 1;
         console.log(123);
     }
 }
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/1/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/1/output.ts
index 93515d0f4a..d12280ef05 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/1/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/1/output.ts
@@ -1,11 +1,10 @@
 class Store {
     constructor(){
-        this.doSomething = ()=>{
-            console.log("run");
-        };
         this.doSomething();
     }
-    doSomething;
+    doSomething = ()=>{
+        console.log("run");
+    };
 }
 __decorate([
     action
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/2/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/2/output.ts
index f7904daa25..c61438efe0 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/2/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/1913/2/output.ts
@@ -1,12 +1,11 @@
 class Store extends BaseStore {
     constructor(){
         super();
-        this.doSomething = ()=>{
-            console.log("run");
-        };
         this.doSomething();
     }
-    doSomething;
+    doSomething = ()=>{
+        console.log("run");
+    };
 }
 __decorate([
     action
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/1/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/1/output.ts
index c423718db9..82c475b5e0 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/1/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/1/output.ts
@@ -1,10 +1,6 @@
 export class Example {
-    constructor(){
-        this.bar = "1";
-        this.baz = "2";
-    }
-    bar;
-    baz;
+    bar = "1";
+    baz = "2";
 }
 __decorate([
     foo()
diff --git a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/2/output.ts b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/2/output.ts
index fb8f70e347..f79ba5f183 100644
--- a/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/2/output.ts
+++ b/crates/swc_ecma_transforms/tests/fixture/legacy-only/issues/591/2/output.ts
@@ -1,10 +1,6 @@
 class Example {
-    constructor(){
-        this.bar = "1";
-        this.baz = "2";
-    }
-    bar;
-    baz;
+    bar = "1";
+    baz = "2";
 }
 __decorate([
     foo()
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 6552552fd2..8b61351b70 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,8 +1,5 @@
 export default class X {
-    constructor(){
-        this.prop = "";
-    }
-    prop: string;
+    prop: string = "";
 }
 __decorate([
     networked
diff --git a/crates/swc_ecma_transforms_optimization/tests/simplify_dce.rs b/crates/swc_ecma_transforms_optimization/tests/simplify_dce.rs
index bcccc835aa..ca1e58a41b 100644
--- a/crates/swc_ecma_transforms_optimization/tests/simplify_dce.rs
+++ b/crates/swc_ecma_transforms_optimization/tests/simplify_dce.rs
@@ -466,9 +466,7 @@ test!(
     "
 
     export default class X {
-        constructor(){
-            this.anything = 0;
-        }
+        anything = 0;
         x() {
             const localVar = aFunctionSomewhere();
             return localVar;