Skip to content

Commit bea18eb

Browse files
Dunqingclaude
andcommitted
fix(transformer): preserve accessor type annotation for emitDecoratorMetadata
`@dec accessor x: T` previously emitted `design:type = Object` regardless of `T`. Lowering rewrites the accessor into a private storage field plus a synthesized getter/setter pair and transfers the decorators to the getter, but it dropped the type annotation. The metadata pass then read the synth getter's empty `return_type` and fell through to `Object` for every typed accessor field — including primitives the metadata serializer handles correctly elsewhere. Route `accessor.type_annotation` onto the synth getter's `return_type` so the existing getter path serializes it as `design:type`. The synth setter (no decorators, no metadata) keeps `None`. `Reflect.getMetadata("design:type", ...)` now returns the correct constructor for `string` / `number` / `boolean` / etc. Refs #21922 (bug 7). Co-Authored-By: Claude <[email protected]>
1 parent 5adca29 commit bea18eb

4 files changed

Lines changed: 107 additions & 3 deletions

File tree

  • crates/oxc_transformer/src/decorator/legacy
  • tasks/transform_conformance/tests/legacy-decorators/test/fixtures/oxc/metadata/accessor

crates/oxc_transformer/src/decorator/legacy/mod.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ mod metadata;
4848
use std::borrow::Cow;
4949
use std::mem;
5050

51-
use oxc_allocator::{Address, CloneIn, GetAddress, TakeIn, UnstableAddress, Vec as ArenaVec};
51+
use oxc_allocator::{
52+
Address, Box as ArenaBox, CloneIn, GetAddress, TakeIn, UnstableAddress, Vec as ArenaVec,
53+
};
5254
use oxc_ast::{NONE, ast::*};
5355
use oxc_ast_visit::{Visit, VisitMut};
5456
use oxc_data_structures::stack::NonEmptyStack;
@@ -359,6 +361,10 @@ impl<'a> LegacyDecorator<'a> {
359361

360362
// Transfer decorators to the getter so legacy decorator transform can process them.
361363
let decorators = std::mem::replace(&mut accessor.decorators, ctx.ast.vec());
364+
// Transfer the type annotation to the getter's return type so that
365+
// `emitDecoratorMetadata` can derive `design:type` from it. Without this,
366+
// the lowered getter is untyped and `design:type` falls through to `Object`.
367+
let type_annotation = accessor.type_annotation.take();
362368

363369
// For computed keys, duplicate the key expression so getter and setter
364370
// each get their own reference:
@@ -426,6 +432,7 @@ impl<'a> LegacyDecorator<'a> {
426432
storage_name,
427433
object_binding,
428434
class_scope_id,
435+
type_annotation,
429436
ctx,
430437
));
431438

@@ -439,6 +446,7 @@ impl<'a> LegacyDecorator<'a> {
439446
storage_name,
440447
object_binding,
441448
class_scope_id,
449+
None,
442450
ctx,
443451
));
444452
}
@@ -467,6 +475,7 @@ impl<'a> LegacyDecorator<'a> {
467475
storage_name: Str<'a>,
468476
object_binding: Option<&BoundIdentifier<'a>>,
469477
class_scope_id: ScopeId,
478+
return_type: Option<ArenaBox<'a, TSTypeAnnotation<'a>>>,
470479
ctx: &mut TraverseCtx<'a>,
471480
) -> ClassElement<'a> {
472481
let is_getter = kind == MethodDefinitionKind::Get;
@@ -539,7 +548,7 @@ impl<'a> LegacyDecorator<'a> {
539548
(params, stmt)
540549
};
541550

542-
create_class_method(
551+
let mut element = create_class_method(
543552
decorators,
544553
key,
545554
kind,
@@ -549,7 +558,13 @@ impl<'a> LegacyDecorator<'a> {
549558
is_static,
550559
scope_id,
551560
ctx,
552-
)
561+
);
562+
if let Some(return_type) = return_type
563+
&& let ClassElement::MethodDefinition(method) = &mut element
564+
{
565+
method.value.return_type = Some(return_type);
566+
}
567+
element
553568
}
554569

555570
/// Helper method to handle a decorated class element (method, property, or accessor).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import "reflect-metadata";
2+
3+
function dec() {}
4+
5+
class Entity {
6+
@dec accessor name: string = "";
7+
@dec accessor count: number = 0;
8+
@dec accessor flag: boolean = false;
9+
@dec accessor untyped = "x";
10+
@dec static accessor sName: string = "";
11+
}
12+
13+
const e = new Entity();
14+
expect(Reflect.getMetadata("design:type", e, "name")).toBe(String);
15+
expect(Reflect.getMetadata("design:type", e, "count")).toBe(Number);
16+
expect(Reflect.getMetadata("design:type", e, "flag")).toBe(Boolean);
17+
expect(Reflect.getMetadata("design:type", e, "untyped")).toBe(Object);
18+
expect(Reflect.getMetadata("design:type", Entity, "sName")).toBe(String);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare function dec(target: any, propertyKey: string): void;
2+
3+
class Entity {
4+
@dec accessor name: string = "";
5+
@dec accessor count: number = 0;
6+
@dec accessor flag: boolean = false;
7+
@dec accessor untyped = "x";
8+
@dec static accessor sName: string = "";
9+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
class Entity {
2+
#_name_accessor_storage = "";
3+
get name() {
4+
return this.#_name_accessor_storage;
5+
}
6+
set name(value) {
7+
this.#_name_accessor_storage = value;
8+
}
9+
#_count_accessor_storage = 0;
10+
get count() {
11+
return this.#_count_accessor_storage;
12+
}
13+
set count(value) {
14+
this.#_count_accessor_storage = value;
15+
}
16+
#_flag_accessor_storage = false;
17+
get flag() {
18+
return this.#_flag_accessor_storage;
19+
}
20+
set flag(value) {
21+
this.#_flag_accessor_storage = value;
22+
}
23+
#_untyped_accessor_storage = "x";
24+
get untyped() {
25+
return this.#_untyped_accessor_storage;
26+
}
27+
set untyped(value) {
28+
this.#_untyped_accessor_storage = value;
29+
}
30+
static #_sName_accessor_storage = "";
31+
static get sName() {
32+
return Entity.#_sName_accessor_storage;
33+
}
34+
static set sName(value) {
35+
Entity.#_sName_accessor_storage = value;
36+
}
37+
}
38+
babelHelpers.decorate([
39+
dec,
40+
babelHelpers.decorateMetadata("design:type", String),
41+
babelHelpers.decorateMetadata("design:paramtypes", [])
42+
], Entity.prototype, "name", null);
43+
babelHelpers.decorate([
44+
dec,
45+
babelHelpers.decorateMetadata("design:type", Number),
46+
babelHelpers.decorateMetadata("design:paramtypes", [])
47+
], Entity.prototype, "count", null);
48+
babelHelpers.decorate([
49+
dec,
50+
babelHelpers.decorateMetadata("design:type", Boolean),
51+
babelHelpers.decorateMetadata("design:paramtypes", [])
52+
], Entity.prototype, "flag", null);
53+
babelHelpers.decorate([
54+
dec,
55+
babelHelpers.decorateMetadata("design:type", Object),
56+
babelHelpers.decorateMetadata("design:paramtypes", [])
57+
], Entity.prototype, "untyped", null);
58+
babelHelpers.decorate([
59+
dec,
60+
babelHelpers.decorateMetadata("design:type", String),
61+
babelHelpers.decorateMetadata("design:paramtypes", [])
62+
], Entity, "sName", null);

0 commit comments

Comments
 (0)