From b79bef892fcb53537b4c3eb45f5058cc0b131742 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 21 Feb 2026 23:33:48 +0000 Subject: [PATCH] fix(parser): allow `accessor` keyword with `experimentalDecorators: true` The `accessor` keyword (TC39 auto-accessors proposal) was incorrectly gated behind the `standard_decorators` feature flag, causing it to be rejected as a syntax error when `experimentalDecorators: true` was set in tsconfig.json. TypeScript supports `accessor` in both decorator modes. Two changes: - Remove `standard_decorators` guard from accessor keyword recognition in the parser so it's always recognized in class bodies. - Set `should_lower_standard_decorators` when auto-accessors are present regardless of the decorator mode, since auto-accessors always need the standard lowering path (WeakMap + getter/setter transformation). Closes #27335 Co-Authored-By: Claude --- src/ast/parse.zig | 2 +- src/ast/parseProperty.zig | 5 +- test/regression/issue/27335.test.ts | 140 ++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 test/regression/issue/27335.test.ts diff --git a/src/ast/parse.zig b/src/ast/parse.zig index 2be1cd78f073..61e88a6f8a71 100644 --- a/src/ast/parse.zig +++ b/src/ast/parse.zig @@ -209,7 +209,7 @@ pub fn Parse( .body_loc = body_loc, .properties = properties.items, .has_decorators = has_any_decorators, - .should_lower_standard_decorators = p.options.features.standard_decorators and (has_any_decorators or has_auto_accessor), + .should_lower_standard_decorators = has_auto_accessor or (p.options.features.standard_decorators and has_any_decorators), }; } diff --git a/src/ast/parseProperty.zig b/src/ast/parseProperty.zig index 010765fa603b..0dce1117ae6a 100644 --- a/src/ast/parseProperty.zig +++ b/src/ast/parseProperty.zig @@ -300,8 +300,9 @@ pub fn ParseProperty( } }, .p_accessor => { - // "accessor" keyword for auto-accessor fields (TC39 standard decorators) - if (opts.is_class and p.options.features.standard_decorators and + // "accessor" keyword for auto-accessor fields (TC39 proposal) + // Always recognized in classes regardless of decorator mode + if (opts.is_class and (js_lexer.PropertyModifierKeyword.List.get(raw) orelse .p_static) == .p_accessor) { kind = .auto_accessor; diff --git a/test/regression/issue/27335.test.ts b/test/regression/issue/27335.test.ts new file mode 100644 index 000000000000..8a6661137bd7 --- /dev/null +++ b/test/regression/issue/27335.test.ts @@ -0,0 +1,140 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +// https://github.com/oven-sh/bun/issues/27335 +// The `accessor` keyword should work in TypeScript classes even when +// `experimentalDecorators: true` is set in tsconfig.json. + +test("accessor keyword works with experimentalDecorators: true", async () => { + using dir = tempDir("issue-27335", { + "tsconfig.json": JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + }, + }), + "main.ts": ` +class Person { + public accessor name: string = "John"; +} + +const p = new Person(); +console.log(p.name); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("John\n"); + expect(exitCode).toBe(0); +}); + +test("accessor keyword works with various modifiers and experimentalDecorators", async () => { + using dir = tempDir("issue-27335-modifiers", { + "tsconfig.json": JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + }, + }), + "main.ts": ` +class Foo { + accessor x = 1; + public accessor y = 2; + private accessor z = 3; + static accessor w = 4; + + getZ() { return this.z; } +} + +const f = new Foo(); +console.log(f.x); +console.log(f.y); +console.log(f.getZ()); +console.log(Foo.w); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("1\n2\n3\n4\n"); + expect(exitCode).toBe(0); +}); + +test("accessor keyword works without experimentalDecorators (standard mode)", async () => { + using dir = tempDir("issue-27335-standard", { + "tsconfig.json": JSON.stringify({ + compilerOptions: {}, + }), + "main.ts": ` +class Person { + public accessor name: string = "John"; +} + +const p = new Person(); +console.log(p.name); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("John\n"); + expect(exitCode).toBe(0); +}); + +test("accessor with experimental decorators on other members", async () => { + using dir = tempDir("issue-27335-mixed", { + "tsconfig.json": JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + }, + }), + "main.ts": ` +function log(target: any, key: string) { + // simple experimental decorator +} + +class MyClass { + @log + greet() { return "hello"; } + + accessor count: number = 42; +} + +const obj = new MyClass(); +console.log(obj.greet()); +console.log(obj.count); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.ts"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("hello\n42\n"); + expect(exitCode).toBe(0); +});