From 6bff64ee77841b848bb4289d2d85e5940c3547c9 Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Fri, 25 Apr 2025 01:30:21 +0000 Subject: [PATCH] feat(transformer/typescript): support `removeClassFieldsWithoutInitializer` option (#10576) * close #9192 * close #10491 We've discussed adding `removeClassFieldsWithoutInitializer` option to support removing class fields without an initializer in https://github.com/oxc-project/oxc/pull/10491#issuecomment-2823195571. This is used to align the`TypeScript`'s `useDefineForClassFields: false` option. --- .../src/typescript/annotations.rs | 13 ++++--- .../oxc_transformer/src/typescript/options.rs | 36 +++++++++++++++++++ napi/transform/src/transformer.rs | 2 ++ .../snapshots/oxc.snap.md | 6 ++-- .../input.ts | 6 ++++ .../options.json | 3 ++ .../output.js | 5 +++ .../use-define-for-class-fields/input.ts | 4 +++ .../use-define-for-class-fields/options.json | 14 ++++++++ .../use-define-for-class-fields/output.js | 5 +++ .../input.ts | 6 ++++ .../options.json | 11 ++++++ .../output.js | 5 +++ 13 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/input.ts create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/input.ts create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/output.js create mode 100644 tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/input.ts create mode 100644 tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/options.json create mode 100644 tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/output.js diff --git a/crates/oxc_transformer/src/typescript/annotations.rs b/crates/oxc_transformer/src/typescript/annotations.rs index 1cbe9264a8171..55a247283d2ab 100644 --- a/crates/oxc_transformer/src/typescript/annotations.rs +++ b/crates/oxc_transformer/src/typescript/annotations.rs @@ -27,6 +27,7 @@ pub struct TypeScriptAnnotations<'a, 'ctx> { has_jsx_fragment: bool, jsx_element_import_name: String, jsx_fragment_import_name: String, + remove_class_fields_without_initializer: bool, } impl<'a, 'ctx> TypeScriptAnnotations<'a, 'ctx> { @@ -52,6 +53,8 @@ impl<'a, 'ctx> TypeScriptAnnotations<'a, 'ctx> { has_jsx_fragment: false, jsx_element_import_name, jsx_fragment_import_name, + remove_class_fields_without_initializer: options + .remove_class_fields_without_initializer, } } } @@ -227,11 +230,11 @@ impl<'a> Traverse<'a> for TypeScriptAnnotations<'a, '_> { && !method.value.is_typescript_syntax() } ClassElement::PropertyDefinition(prop) => { - if prop.declare { - false - } else { - matches!(prop.r#type, PropertyDefinitionType::PropertyDefinition) - } + matches!(prop.r#type, PropertyDefinitionType::PropertyDefinition) + && !prop.declare + && !(self.remove_class_fields_without_initializer + && prop.value.is_none() + && prop.decorators.is_empty()) } ClassElement::AccessorProperty(prop) => { matches!(prop.r#type, AccessorPropertyType::AccessorProperty) diff --git a/crates/oxc_transformer/src/typescript/options.rs b/crates/oxc_transformer/src/typescript/options.rs index 1c8e7dfb6218c..0d2688ccdadc9 100644 --- a/crates/oxc_transformer/src/typescript/options.rs +++ b/crates/oxc_transformer/src/typescript/options.rs @@ -44,6 +44,41 @@ pub struct TypeScriptOptions { #[serde(default = "default_as_true")] pub allow_declare_fields: bool, + /// When enabled, class fields without initializers are removed. + /// + /// For example: + /// ```ts + /// class Foo { + /// x: number; + /// y: number = 0; + /// } + /// ``` + /// // transform into + /// ```js + /// class Foo { + /// x: number; + /// } + /// ``` + /// + /// The option is used to align with the behavior of TypeScript's `useDefineForClassFields: false` option. + /// When you want to enable this, you also need to set [`crate::CompilerAssumptions::set_public_class_fields`] + /// to `true`. The `set_public_class_fields: true` + `remove_class_fields_without_initializer: true` is + /// equivalent to `useDefineForClassFields: false` in TypeScript. + /// + /// When `set_public_class_fields` is true and class-properties plugin is enabled, the above example transforms into: + /// + /// ```js + /// class Foo { + /// constructor() { + /// this.y = 0; + /// } + /// } + /// ``` + /// + /// Defaults to `false`. + #[serde(default)] + pub remove_class_fields_without_initializer: bool, + /// Unused. pub optimize_const_enums: bool, @@ -66,6 +101,7 @@ impl Default for TypeScriptOptions { only_remove_type_imports: false, allow_namespaces: default_as_true(), allow_declare_fields: default_as_true(), + remove_class_fields_without_initializer: false, optimize_const_enums: false, rewrite_import_extensions: None, } diff --git a/napi/transform/src/transformer.rs b/napi/transform/src/transformer.rs index 791c97d9114c5..588893f65790e 100644 --- a/napi/transform/src/transformer.rs +++ b/napi/transform/src/transformer.rs @@ -252,6 +252,8 @@ impl From for oxc::transformer::TypeScriptOptions { allow_namespaces: options.allow_namespaces.unwrap_or(ops.allow_namespaces), allow_declare_fields: options.allow_declare_fields.unwrap_or(ops.allow_declare_fields), optimize_const_enums: false, + // TODO: Implement + remove_class_fields_without_initializer: false, rewrite_import_extensions: options.rewrite_import_extensions.and_then(|value| { match value { Either::A(v) => { diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index f620abcacefb5..a5f47aa3be17a 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: 578ac4df -Passed: 150/246 +Passed: 153/249 # All Passed: * babel-plugin-transform-class-static-block @@ -44,7 +44,7 @@ after transform: SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), R rebuilt : SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), ReferenceId(10)] -# babel-plugin-transform-typescript (4/20) +# babel-plugin-transform-typescript (6/22) * class-property-definition/input.ts Unresolved references mismatch: after transform: ["const"] @@ -424,7 +424,7 @@ after transform: SymbolId(4): ScopeId(1) rebuilt : SymbolId(5): ScopeId(4) -# legacy-decorators (2/71) +# legacy-decorators (3/72) * oxc/metadata/bound-type-reference/input.ts Symbol reference IDs mismatch for "BoundTypeReference": after transform: SymbolId(0): [ReferenceId(1), ReferenceId(3), ReferenceId(4), ReferenceId(5), ReferenceId(6)] diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/input.ts b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/input.ts new file mode 100644 index 0000000000000..119414b89652e --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/input.ts @@ -0,0 +1,6 @@ +class Cls { + x: number; + y = 1; + @dce + z: string; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/options.json new file mode 100644 index 0000000000000..b01ff94088f4b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-typescript", { "removeClassFieldsWithoutInitializer": true }]] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/output.js new file mode 100644 index 0000000000000..672b242e37288 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/remove-class-properties-without-initializer/output.js @@ -0,0 +1,5 @@ +class Cls { + y = 1; + @dce + z; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/input.ts b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/input.ts new file mode 100644 index 0000000000000..d19ad8a30daca --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/input.ts @@ -0,0 +1,4 @@ +class Cls { + x: number; + y = 1; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/options.json new file mode 100644 index 0000000000000..bcf2bdc13d919 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/options.json @@ -0,0 +1,14 @@ +{ + "assumptions": { + "setPublicClassFields": true + }, + "plugins": [ + "transform-class-properties", + [ + "transform-typescript", + { + "removeClassFieldsWithoutInitializer": true + } + ] + ] +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/output.js new file mode 100644 index 0000000000000..a284b7df2bbb9 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/use-define-for-class-fields/output.js @@ -0,0 +1,5 @@ +class Cls { + constructor() { + this.y = 1; + } +} diff --git a/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/input.ts b/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/input.ts new file mode 100644 index 0000000000000..119414b89652e --- /dev/null +++ b/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/input.ts @@ -0,0 +1,6 @@ +class Cls { + x: number; + y = 1; + @dce + z: string; +} diff --git a/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/options.json b/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/options.json new file mode 100644 index 0000000000000..e80c1897ff30c --- /dev/null +++ b/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/options.json @@ -0,0 +1,11 @@ +{ + "plugins": [ + "transform-legacy-decorator", + [ + "transform-typescript", + { + "removeClassFieldsWithoutInitializer": true + } + ] + ] +} diff --git a/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/output.js b/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/output.js new file mode 100644 index 0000000000000..2457e36768f45 --- /dev/null +++ b/tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/with-typescript-remove-class-properties-without-initializer/output.js @@ -0,0 +1,5 @@ +class Cls { + y = 1; + z; +} +babelHelpers.decorate([dce], Cls.prototype, "z", void 0);