fix(transpiler): avoid private name collisions in auto-accessor lowering

Generated backing field names for computed auto-accessors (#a, #b, ...)
now skip names already used by existing private fields in the class.
Private names are not subject to automatic renaming, so collisions
would produce duplicate private name errors at runtime.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jarred Sumner
2026-01-25 07:57:10 +01:00
parent 351a2ffa4a
commit 47f13092df
2 changed files with 30 additions and 7 deletions

View File

@@ -4883,13 +4883,30 @@ pub fn NewParser_(
break :blk std.fmt.allocPrint(p.allocator, "#{s}", .{str.data}) catch unreachable;
}
}
// Computed key -> counter-based name
const offset: u8 = @intCast(@min(auto_accessor_count, 25));
const name = std.fmt.allocPrint(p.allocator, "#{c}", .{
@as(u8, 'a' + offset),
}) catch unreachable;
auto_accessor_count += 1;
break :blk name;
// Computed key -> counter-based name, skipping any
// that collide with existing private names in the class
// (private names are not subject to automatic renaming).
while (true) : (auto_accessor_count += 1) {
const offset: u8 = @intCast(@min(auto_accessor_count, 25));
const candidate = std.fmt.allocPrint(p.allocator, "#{c}", .{
@as(u8, 'a' + offset),
}) catch unreachable;
var collides = false;
for (properties) |other| {
if (other.key != null and other.key.?.data == .e_private_identifier) {
const other_name = p.loadNameFromRef(other.key.?.data.e_private_identifier.ref);
if (strings.eql(candidate, other_name)) {
collides = true;
break;
}
}
}
if (!collides) {
auto_accessor_count += 1;
break :blk candidate;
}
}
unreachable;
};
// Create backing field symbol

View File

@@ -885,6 +885,12 @@ function foo() {}
exp("class Foo { declare accessor x: number }", "class Foo {\n}");
exp("abstract class Foo { abstract accessor x: number }", "class Foo {\n}");
// Computed backing field skips existing private names to avoid collisions
exp(
"class Foo { accessor [x]; #a = 1 }",
"var _a;\n\nclass Foo {\n #b;\n get [_a = x]() {\n return this.#b;\n }\n set [_a](_) {\n this.#b = _;\n }\n #a = 1;\n}",
);
// Contextual keyword edge cases (not auto-accessor syntax)
exp("class Foo { accessor() {} }", "class Foo {\n accessor() {}\n}");
exp("class Foo { accessor = 1 }", "class Foo {\n accessor = 1;\n}");