From 47f13092df8b9c3c9f777d99c504c00c496e76f2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 25 Jan 2026 07:57:10 +0100 Subject: [PATCH] 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 --- src/ast/P.zig | 31 +++++++++++++++++----- test/bundler/transpiler/transpiler.test.js | 6 +++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/ast/P.zig b/src/ast/P.zig index 0dfe947c85..e322266d75 100644 --- a/src/ast/P.zig +++ b/src/ast/P.zig @@ -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 diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index ba631293ea..a83c185430 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -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}");