mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(transformer/legacy-decorator): preserve accessor type annotation for emitDecoratorMetadata (#21966)
Fixes bug 7 of https://github.com/oxc-project/oxc/issues/21922. ## The bug `@dec accessor x: T` always emitted `design:type = Object` regardless of `T`: ```ts class Entity { @dec accessor name: string = ""; // tsc: design:type = String @dec accessor count: number = 0; // tsc: design:type = Number } ``` `Reflect.getMetadata("design:type", entity, "name")` returned `Object` instead of `String`. Frameworks reading `design:type` (NestJS Swagger, class-validator, TypeORM column metadata) silently degraded for every typed accessor field. ## Cause `lower_accessor_properties` (in `enter_class`) rewrites ```ts @dec accessor x: T = init; ``` into three members — a private storage field, a synthesized getter, and a synthesized setter — and transfers the decorators to the getter so the existing decorator pipeline picks them up. But it dropped the type annotation: the synth getter was constructed with `return_type: NONE`. The metadata pass runs later and takes the getter branch: ```rust if method.kind.is_get() { (self.serialize_type_annotation(method.value.return_type.as_ref(), ctx), None) } ``` `return_type` is `None` because lowering discarded it, so `serialize_type_annotation` correctly fell through to `Object` for what it sees as an untyped getter. The metadata pass's `enter_accessor_property` dispatch exists, but it never fires because the `AccessorProperty` no longer exists by the time traversal reaches the class body. ## Fix Stop dropping the type annotation. In `lower_accessor_properties`: ```rust let decorators = std::mem::replace(&mut accessor.decorators, ctx.ast.vec()); let type_annotation = accessor.type_annotation.take(); ``` Thread `type_annotation` through `create_accessor_method` to the getter (setter passes `None`). To avoid a post-construction patch on the synthesized `MethodDefinition`, `create_class_method` now takes a `return_type: Option<ArenaBox<TSTypeAnnotation>>` parameter and threads it directly into the function's return-type slot. The other caller (`create_class_constructor_with_params`) passes `None`. The lowered AST is now self-consistent — the synth getter for `accessor x: T` reads as a getter that returns `T`, which is what it semantically is, and the existing getter-metadata path serializes it correctly. ## Out of scope (architectural limit) `type Uuid = string; @dec accessor x: Uuid` still emits `Object`, same as for non-accessor properties. Resolving type-alias references to their primitive constituent requires a name-based alias-resolution pass (the same one that `tsc`'s checker provides via `getTypeReferenceSerializationKind`). That's bug 1 of #21922 and a separate change from this fix; flagged here so reviewers don't mistake it for an omission. ## Test plan - [x] Conformance fixture `oxc/metadata/accessor` covers primitives, untyped (correctly stays `Object`), `string[]` → `Array`, `static accessor`, and computed-key (`@dec accessor ["computed"]: number`) - [x] `cargo test -p oxc_transformer` — 31 + 5 passed - [x] `cargo run -p oxc_transform_conformance -- --filter legacy-decorators` — 73 pass / 23 fail (baseline 72/24, +1 new fixture, **0 regressions**) - [x] `cargo run -p oxc_transform_conformance -- --exec` — accessor exec test verifies `Reflect.getMetadata` returns the correct constructor at runtime - [x] `cargo fmt -p oxc_transformer`
This commit is contained in:
1 parent
5bdf4972b0
commit
dd88726b8c
9 files changed
+142
-7
No files matched your search
@@ -48,7 +48,9 @@ mod metadata;
|
||||
use std::borrow::Cow;
|
||||
use std::mem;
|
||||
|
||||
use oxc_allocator::{Address, CloneIn, GetAddress, TakeIn, UnstableAddress, Vec as ArenaVec};
|
||||
use oxc_allocator::{
|
||||
Address, Box as ArenaBox, CloneIn, GetAddress, TakeIn, UnstableAddress, Vec as ArenaVec,
|
||||
};
|
||||
use oxc_ast::{NONE, ast::*};
|
||||
use oxc_ast_visit::{Visit, VisitMut};
|
||||
use oxc_data_structures::stack::NonEmptyStack;
|
||||
@@ -359,6 +361,10 @@ impl<'a> LegacyDecorator<'a> {
|
||||
|
||||
// Transfer decorators to the getter so legacy decorator transform can process them.
|
||||
let decorators = std::mem::replace(&mut accessor.decorators, ctx.ast.vec());
|
||||
// Transfer the type annotation to the getter's return type so that
|
||||
// `emitDecoratorMetadata` can derive `design:type` from it. Without this,
|
||||
// the lowered getter is untyped and `design:type` falls through to `Object`.
|
||||
let type_annotation = accessor.type_annotation.take();
|
||||
|
||||
// For computed keys, duplicate the key expression so getter and setter
|
||||
// each get their own reference:
|
||||
@@ -426,6 +432,7 @@ impl<'a> LegacyDecorator<'a> {
|
||||
storage_name,
|
||||
object_binding,
|
||||
class_scope_id,
|
||||
type_annotation,
|
||||
ctx,
|
||||
));
|
||||
|
||||
@@ -439,6 +446,7 @@ impl<'a> LegacyDecorator<'a> {
|
||||
storage_name,
|
||||
object_binding,
|
||||
class_scope_id,
|
||||
None,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
@@ -467,6 +475,7 @@ impl<'a> LegacyDecorator<'a> {
|
||||
storage_name: Str<'a>,
|
||||
object_binding: Option<&BoundIdentifier<'a>>,
|
||||
class_scope_id: ScopeId,
|
||||
return_type: Option<ArenaBox<'a, TSTypeAnnotation<'a>>>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> ClassElement<'a> {
|
||||
let is_getter = kind == MethodDefinitionKind::Get;
|
||||
@@ -544,6 +553,7 @@ impl<'a> LegacyDecorator<'a> {
|
||||
key,
|
||||
kind,
|
||||
params,
|
||||
return_type,
|
||||
ctx.ast.vec1(body_stmt),
|
||||
computed,
|
||||
is_static,
|
||||
|
||||
@@ -203,6 +203,7 @@ pub fn create_class_constructor_with_params<'a>(
|
||||
),
|
||||
MethodDefinitionKind::Constructor,
|
||||
params,
|
||||
None,
|
||||
stmts,
|
||||
false,
|
||||
false,
|
||||
@@ -217,6 +218,7 @@ pub fn create_class_method<'a>(
|
||||
key: PropertyKey<'a>,
|
||||
kind: MethodDefinitionKind,
|
||||
params: ArenaBox<'a, FormalParameters<'a>>,
|
||||
return_type: Option<ArenaBox<'a, TSTypeAnnotation<'a>>>,
|
||||
stmts: ArenaVec<'a, Statement<'a>>,
|
||||
computed: bool,
|
||||
is_static: bool,
|
||||
@@ -238,7 +240,7 @@ pub fn create_class_method<'a>(
|
||||
NONE,
|
||||
NONE,
|
||||
params,
|
||||
NONE,
|
||||
return_type,
|
||||
Some(ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), stmts)),
|
||||
scope_id,
|
||||
),
|
||||
|
||||
@@ -18,6 +18,9 @@ rebuilt : ScopeId(5): Some(ScopeId(0))
|
||||
Scope parent mismatch:
|
||||
after transform: ScopeId(4): Some(ScopeId(1))
|
||||
rebuilt : ScopeId(7): Some(ScopeId(0))
|
||||
Unresolved reference IDs mismatch for "String":
|
||||
after transform: [ReferenceId(11), ReferenceId(12)]
|
||||
rebuilt : [ReferenceId(18)]
|
||||
|
||||
semantic Error: tasks/coverage/misc/pass/oxc-1288.ts
|
||||
Bindings mismatch:
|
||||
|
||||
@@ -7150,7 +7150,7 @@ Bindings mismatch:
|
||||
after transform: ScopeId(3): []
|
||||
rebuilt : ScopeId(3): ["_class"]
|
||||
Symbol reference IDs mismatch for "_decorateMetadata":
|
||||
after transform: SymbolId(16): [ReferenceId(12), ReferenceId(13), ReferenceId(15), ReferenceId(16), ReferenceId(24), ReferenceId(25)]
|
||||
after transform: SymbolId(16): [ReferenceId(12), ReferenceId(13), ReferenceId(14), ReferenceId(15), ReferenceId(22), ReferenceId(23)]
|
||||
rebuilt : SymbolId(2): []
|
||||
Symbol reference IDs mismatch for "dec":
|
||||
after transform: SymbolId(0): [ReferenceId(1), ReferenceId(2), ReferenceId(3), ReferenceId(4)]
|
||||
@@ -7159,7 +7159,7 @@ Symbol scope ID mismatch for "_class":
|
||||
after transform: SymbolId(8): ScopeId(0)
|
||||
rebuilt : SymbolId(14): ScopeId(3)
|
||||
Unresolved references mismatch:
|
||||
after transform: ["DecoratorContext", "Object", "require"]
|
||||
after transform: ["DecoratorContext", "require"]
|
||||
rebuilt : ["require"]
|
||||
|
||||
semantic Error: tasks/coverage/typescript/tests/cases/compiler/esNextWeakRefs_IterableWeakMap.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
commit: 6402dbbf
|
||||
|
||||
Passed: 228/376
|
||||
Passed: 229/377
|
||||
|
||||
# All Passed:
|
||||
* babel-plugin-transform-class-static-block
|
||||
@@ -647,7 +647,7 @@ x Output mismatch
|
||||
x Output mismatch
|
||||
|
||||
|
||||
# legacy-decorators (8/95)
|
||||
# legacy-decorators (9/96)
|
||||
* oxc/accessor/input.ts
|
||||
x Output mismatch
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ commit: 6402dbbf
|
||||
|
||||
node: v24.14.0
|
||||
|
||||
Passed: 14 of 16 (87.50%)
|
||||
Passed: 15 of 17 (88.24%)
|
||||
|
||||
Failures:
|
||||
|
||||
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
function dec() {}
|
||||
|
||||
class Entity {
|
||||
@dec accessor name: string = "";
|
||||
@dec accessor count: number = 0;
|
||||
@dec accessor flag: boolean = false;
|
||||
@dec accessor untyped = "x";
|
||||
@dec accessor list: string[] = [];
|
||||
@dec static accessor sName: string = "";
|
||||
@dec accessor ["computed"]: number = 0;
|
||||
}
|
||||
|
||||
const e = new Entity();
|
||||
expect(Reflect.getMetadata("design:type", e, "name")).toBe(String);
|
||||
expect(Reflect.getMetadata("design:type", e, "count")).toBe(Number);
|
||||
expect(Reflect.getMetadata("design:type", e, "flag")).toBe(Boolean);
|
||||
expect(Reflect.getMetadata("design:type", e, "untyped")).toBe(Object);
|
||||
expect(Reflect.getMetadata("design:type", e, "list")).toBe(Array);
|
||||
expect(Reflect.getMetadata("design:type", Entity, "sName")).toBe(String);
|
||||
expect(Reflect.getMetadata("design:type", e, "computed")).toBe(Number);
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
function dec() {}
|
||||
|
||||
class Entity {
|
||||
@dec accessor name: string = "";
|
||||
@dec accessor count: number = 0;
|
||||
@dec accessor flag: boolean = false;
|
||||
@dec accessor untyped = "x";
|
||||
@dec accessor list: string[] = [];
|
||||
@dec static accessor sName: string = "";
|
||||
@dec accessor ["computed"]: number = 0;
|
||||
}
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
function dec() {}
|
||||
class Entity {
|
||||
#_name_accessor_storage = "";
|
||||
get name() {
|
||||
return this.#_name_accessor_storage;
|
||||
}
|
||||
set name(value) {
|
||||
this.#_name_accessor_storage = value;
|
||||
}
|
||||
#_count_accessor_storage = 0;
|
||||
get count() {
|
||||
return this.#_count_accessor_storage;
|
||||
}
|
||||
set count(value) {
|
||||
this.#_count_accessor_storage = value;
|
||||
}
|
||||
#_flag_accessor_storage = false;
|
||||
get flag() {
|
||||
return this.#_flag_accessor_storage;
|
||||
}
|
||||
set flag(value) {
|
||||
this.#_flag_accessor_storage = value;
|
||||
}
|
||||
#_untyped_accessor_storage = "x";
|
||||
get untyped() {
|
||||
return this.#_untyped_accessor_storage;
|
||||
}
|
||||
set untyped(value) {
|
||||
this.#_untyped_accessor_storage = value;
|
||||
}
|
||||
#_list_accessor_storage = [];
|
||||
get list() {
|
||||
return this.#_list_accessor_storage;
|
||||
}
|
||||
set list(value) {
|
||||
this.#_list_accessor_storage = value;
|
||||
}
|
||||
static #_sName_accessor_storage = "";
|
||||
static get sName() {
|
||||
return Entity.#_sName_accessor_storage;
|
||||
}
|
||||
static set sName(value) {
|
||||
Entity.#_sName_accessor_storage = value;
|
||||
}
|
||||
#_computed_computed_accessor_storage = 0;
|
||||
get ["computed"]() {
|
||||
return this.#_computed_computed_accessor_storage;
|
||||
}
|
||||
set ["computed"](value) {
|
||||
this.#_computed_computed_accessor_storage = value;
|
||||
}
|
||||
}
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", String),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity.prototype, "name", null);
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", Number),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity.prototype, "count", null);
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", Boolean),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity.prototype, "flag", null);
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", Object),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity.prototype, "untyped", null);
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", Array),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity.prototype, "list", null);
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", String),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity, "sName", null);
|
||||
babelHelpers.decorate([
|
||||
dec,
|
||||
babelHelpers.decorateMetadata("design:type", Number),
|
||||
babelHelpers.decorateMetadata("design:paramtypes", [])
|
||||
], Entity.prototype, "computed", null);
|
||||
Reference in new issue
Block a user