fix(transformer/decorators): avoid accessor storage name collisions (#21106)

## Summary

- Use raw property/identifier names instead of `get_var_name_from_node` (which strips leading underscores) when generating private backing field names for accessor properties
- For non-computed keys, use `PropertyKey::name()` which preserves leading underscores
- For computed keys, match on `Expression::Identifier` to get the raw name directly
- No counters, maps, or helper functions needed — collisions are avoided at the source

This prevents `accessor prop` and `accessor _prop` from producing the same `#_prop_accessor_storage` name, which caused a runtime `TypeError: Cannot initialize the same private elements twice on an object`.

Closes #20946
Supersedes #21023

## Test plan

- [x] Added test fixture covering both non-computed (`prop`/`_prop`) and computed (`[prop]`/`[_prop]`) accessor name collisions
- [x] All existing accessor tests pass unchanged
- [x] `cargo test -p oxc_transformer` passes
- [x] Full `cargo run -p oxc_transform_conformance` passes
- [x] `cargo clippy` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Dunqing committed 2026-04-08 02:22:25 +00:00
1 parent 0d678345aa
commit eab13b39e3
6 files changed
+95 -37

No files matched your search

@@ -45,6 +45,7 @@
mod metadata;
use std::borrow::Cow;
use std::mem;
use oxc_allocator::{Address, CloneIn, GetAddress, TakeIn, UnstableAddress, Vec as ArenaVec};
@@ -369,20 +370,26 @@ impl<'a> LegacyDecorator<'a> {
let key_expr = accessor.key.into_expression();
let (assignment, reference) = duplicate_expression(key_expr, true, ctx);
// Use raw identifier name when possible to avoid collisions from
// `get_var_name_from_node` stripping leading underscores.
let key_name: Cow<'_, str> = match &reference {
Expression::Identifier(ident) => Cow::Borrowed(ident.name.as_str()),
_ => Cow::Owned(get_var_name_from_node(&reference)),
};
let getter_key = PropertyKey::from(assignment);
let setter_key = PropertyKey::from(reference);
let storage_name = ctx.ast.str_from_strs_array([
"_",
&get_var_name_from_node(&setter_key),
"_computed_accessor_storage",
]);
let storage_name =
ctx.ast.str_from_strs_array(["_", &key_name, "_computed_accessor_storage"]);
(storage_name, getter_key, setter_key)
} else {
let storage_name = ctx.ast.str_from_strs_array([
"_",
&get_var_name_from_node(&accessor.key),
"_accessor_storage",
]);
// Use `name()` to get the raw property name, avoiding `get_var_name_from_node`
// which strips leading underscores (e.g. `prop` and `_prop` both become "prop").
let key_name = accessor
.key
.name()
.unwrap_or_else(|| Cow::Owned(get_var_name_from_node(&accessor.key)));
let storage_name =
ctx.ast.str_from_strs_array(["_", &key_name, "_accessor_storage"]);
let getter_key = accessor.key.clone_in(ctx.ast.allocator);
let setter_key = accessor.key.clone_in(ctx.ast.allocator);
(storage_name, getter_key, setter_key)
@@ -19823,8 +19823,8 @@ rebuilt : ScopeId(0): ["C1", "_a_accessor_storage", "_b_accessor_storage"
semantic Error: tasks/coverage/typescript/tests/cases/conformance/classes/propertyMemberDeclarations/autoAccessorAllowedModifiers.ts
Bindings mismatch:
after transform: ScopeId(0): ["C1", "C2", "C3", "_08_accessor_storage", "_C1_brand", "_a_accessor_storage", "_b_accessor_storage", "_c_accessor_storage", "_classPrivateFieldGet", "_classPrivateFieldInitSpec", "_classPrivateFieldSet", "_classPrivateMethodInitSpec", "_d_accessor_storage", "_e_accessor_storage", "_f_accessor_storage", "_g_accessor_storage", "_get_j", "_h_accessor_storage", "_i_accessor_storage", "_i_accessor_storage2", "_j_accessor_storage", "_k_accessor_storage", "_m_computed_accessor_storage", "_n_accessor_storage", "_set_j"]
rebuilt : ScopeId(0): ["C1", "C2", "_08_accessor_storage", "_C1_brand", "_a_accessor_storage", "_b_accessor_storage", "_c_accessor_storage", "_classPrivateFieldGet", "_classPrivateFieldInitSpec", "_classPrivateFieldSet", "_classPrivateMethodInitSpec", "_d_accessor_storage", "_e_accessor_storage", "_f_accessor_storage", "_g_accessor_storage", "_get_j", "_h_accessor_storage", "_i_accessor_storage", "_i_accessor_storage2", "_j_accessor_storage", "_k_accessor_storage", "_m_computed_accessor_storage", "_n_accessor_storage", "_set_j"]
after transform: ScopeId(0): ["C1", "C2", "C3", "_108_accessor_storage", "_C1_brand", "_a_accessor_storage", "_b_accessor_storage", "_c_accessor_storage", "_classPrivateFieldGet", "_classPrivateFieldInitSpec", "_classPrivateFieldSet", "_classPrivateMethodInitSpec", "_d_accessor_storage", "_e_accessor_storage", "_f_accessor_storage", "_g_accessor_storage", "_get_j", "_h_accessor_storage", "_i_accessor_storage", "_i_accessor_storage2", "_j_accessor_storage", "_k_accessor_storage", "_m_computed_accessor_storage", "_n_accessor_storage", "_set_j"]
rebuilt : ScopeId(0): ["C1", "C2", "_108_accessor_storage", "_C1_brand", "_a_accessor_storage", "_b_accessor_storage", "_c_accessor_storage", "_classPrivateFieldGet", "_classPrivateFieldInitSpec", "_classPrivateFieldSet", "_classPrivateMethodInitSpec", "_d_accessor_storage", "_e_accessor_storage", "_f_accessor_storage", "_g_accessor_storage", "_get_j", "_h_accessor_storage", "_i_accessor_storage", "_i_accessor_storage2", "_j_accessor_storage", "_k_accessor_storage", "_m_computed_accessor_storage", "_n_accessor_storage", "_set_j"]
semantic Error: tasks/coverage/typescript/tests/cases/conformance/classes/propertyMemberDeclarations/overrideInterfaceProperty.ts
Reference symbol mismatch for "Mup":
@@ -1,6 +1,6 @@
commit: 91b4ce32
Passed: 210/346
Passed: 210/347
# All Passed:
* babel-plugin-transform-class-static-block
@@ -555,38 +555,29 @@ x Output mismatch
x Output mismatch
# legacy-decorators (8/93)
# legacy-decorators (8/94)
* oxc/accessor/input.ts
x Output mismatch
* oxc/accessor-name-collision/input.ts
Bindings mismatch:
after transform: ScopeId(0): ["C", "_a", "_foo$bar", "_foo$bar2", "a", "dec", "foo"]
rebuilt : ScopeId(0): ["C", "_a", "_foo$bar", "_foo$bar2"]
Reference symbol mismatch for "a":
after transform: SymbolId(4) "a"
after transform: ScopeId(0): ["Foo", "_prop", "_prop2", "_prop3", "prop", "property"]
rebuilt : ScopeId(0): ["Foo", "_prop", "_prop2", "_prop3", "prop"]
Reference symbol mismatch for "property":
after transform: SymbolId(0) "property"
rebuilt : <None>
Reference symbol mismatch for "a":
after transform: SymbolId(4) "a"
Reference symbol mismatch for "property":
after transform: SymbolId(0) "property"
rebuilt : <None>
Reference symbol mismatch for "foo":
after transform: SymbolId(5) "foo"
Reference symbol mismatch for "property":
after transform: SymbolId(0) "property"
rebuilt : <None>
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
rebuilt : <None>
Reference symbol mismatch for "dec":
after transform: SymbolId(0) "dec"
Reference symbol mismatch for "property":
after transform: SymbolId(0) "property"
rebuilt : <None>
Unresolved references mismatch:
after transform: ["PropertyDescriptor", "babelHelpers"]
rebuilt : ["a", "babelHelpers", "dec", "foo"]
rebuilt : ["babelHelpers", "property"]
* oxc/accessor-with-class-properties/input.ts
Bindings mismatch:
@@ -0,0 +1,18 @@
declare function property(): (target: any, propertyKey: string, desc: PropertyDescriptor) => void;
const prop = 'foo'
const _prop = 'bar'
export class Foo {
@property()
accessor prop!: string;
@property()
accessor _prop!: string;
@property()
accessor [prop]!: string;
@property()
accessor [_prop]!: string;
}
@@ -0,0 +1,5 @@
{
"plugins": [
"transform-legacy-decorator"
]
}
@@ -0,0 +1,37 @@
var _prop2, _prop3;
const prop = "foo";
const _prop = "bar";
export class Foo {
#_prop_accessor_storage;
get prop() {
return this.#_prop_accessor_storage;
}
set prop(value) {
this.#_prop_accessor_storage = value;
}
#__prop_accessor_storage;
get _prop() {
return this.#__prop_accessor_storage;
}
set _prop(value) {
this.#__prop_accessor_storage = value;
}
#_prop_computed_accessor_storage;
get [_prop2 = prop]() {
return this.#_prop_computed_accessor_storage;
}
set [prop](value) {
this.#_prop_computed_accessor_storage = value;
}
#__prop_computed_accessor_storage;
get [_prop3 = _prop]() {
return this.#__prop_computed_accessor_storage;
}
set [_prop](value) {
this.#__prop_computed_accessor_storage = value;
}
}
babelHelpers.decorate([property()], Foo.prototype, "prop", null);
babelHelpers.decorate([property()], Foo.prototype, "_prop", null);
babelHelpers.decorate([property()], Foo.prototype, _prop2, null);
babelHelpers.decorate([property()], Foo.prototype, _prop3, null);