mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
fix(transpiler): fix export default class crash and accessor ! parsing with decorators
Fix two bugs in ES decorator lowering: 1. `export default class` with decorators crashed because the code assumed the first statement from lowerClass was always s_class, but standard decorator lowering can produce prefix variable declarations. 2. `accessor field!: Type` (definite assignment assertion) failed to parse because the `!` operator was only recognized for `kind == .normal`, excluding `auto_accessor`. Also fix DCE check panic on non-s_class statements in export default. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3768,7 +3768,10 @@ pub fn NewParser_(
|
||||
}
|
||||
},
|
||||
else => {
|
||||
Output.panic("Unexpected type in export default", .{});
|
||||
// Standard decorator lowering can produce non-class
|
||||
// statements as the export default value; conservatively
|
||||
// assume they have side effects.
|
||||
return false;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -421,7 +421,7 @@ pub fn ParseProperty(
|
||||
try p.lexer.next();
|
||||
} else if (p.lexer.token == .t_exclamation and
|
||||
!p.lexer.has_newline_before and
|
||||
kind == .normal and
|
||||
(kind == .normal or kind == .auto_accessor) and
|
||||
!opts.is_async and
|
||||
!opts.is_generator)
|
||||
{
|
||||
|
||||
@@ -461,17 +461,29 @@ pub fn VisitStmt(
|
||||
}
|
||||
}
|
||||
|
||||
// This is to handle TS decorators, mostly.
|
||||
// Lower the class (handles both TS legacy and standard decorators).
|
||||
// Standard decorator lowering may produce prefix statements
|
||||
// (variable declarations) before the class statement.
|
||||
var class_stmts = p.lowerClass(.{ .stmt = s2 });
|
||||
bun.assert(class_stmts[0].data == .s_class);
|
||||
|
||||
if (class_stmts.len > 1) {
|
||||
data.value.stmt = class_stmts[0];
|
||||
stmts.append(stmt.*) catch {};
|
||||
stmts.appendSlice(class_stmts[1..]) catch {};
|
||||
} else {
|
||||
data.value.stmt = class_stmts[0];
|
||||
stmts.append(stmt.*) catch {};
|
||||
// Find the s_class statement in the returned list
|
||||
var class_stmt_idx: usize = 0;
|
||||
for (class_stmts, 0..) |cs, idx| {
|
||||
if (cs.data == .s_class) {
|
||||
class_stmt_idx = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit any prefix statements before the export default
|
||||
stmts.appendSlice(class_stmts[0..class_stmt_idx]) catch {};
|
||||
|
||||
data.value.stmt = class_stmts[class_stmt_idx];
|
||||
stmts.append(stmt.*) catch {};
|
||||
|
||||
// Emit any suffix statements after the export default
|
||||
if (class_stmt_idx + 1 < class_stmts.len) {
|
||||
stmts.appendSlice(class_stmts[class_stmt_idx + 1 ..]) catch {};
|
||||
}
|
||||
|
||||
if (p.options.features.server_components.wrapsExports()) {
|
||||
|
||||
@@ -472,4 +472,147 @@ describe("ES Decorators", () => {
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("export default class", () => {
|
||||
test("export default class with method decorator", async () => {
|
||||
using dir = tempDir("es-dec-export-default", {
|
||||
"entry.js": `
|
||||
import Cls from "./mod.js";
|
||||
const c = new Cls();
|
||||
console.log(c.foo());
|
||||
`,
|
||||
"mod.js": `
|
||||
function dec(target, ctx) { return target; }
|
||||
export default class {
|
||||
@dec foo() { return 42; }
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "entry.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
expect(filterStderr(rawStderr)).toBe("");
|
||||
expect(stdout).toBe("42\n");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("export default class with class decorator", async () => {
|
||||
using dir = tempDir("es-dec-export-default-cls", {
|
||||
"entry.js": `
|
||||
import Cls from "./mod.js";
|
||||
const c = new Cls();
|
||||
console.log(c.value);
|
||||
`,
|
||||
"mod.js": `
|
||||
function addValue(cls, ctx) {
|
||||
return class extends cls { value = "decorated"; };
|
||||
}
|
||||
@addValue export default class {}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "entry.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
expect(filterStderr(rawStderr)).toBe("");
|
||||
expect(stdout).toBe("decorated\n");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("export default named class with decorator", async () => {
|
||||
using dir = tempDir("es-dec-export-default-named", {
|
||||
"entry.js": `
|
||||
import Cls from "./mod.js";
|
||||
const c = new Cls();
|
||||
console.log(c.foo());
|
||||
`,
|
||||
"mod.js": `
|
||||
function dec(target, ctx) { return target; }
|
||||
export default class MyClass {
|
||||
@dec foo() { return "named"; }
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "entry.js"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
expect(filterStderr(rawStderr)).toBe("");
|
||||
expect(stdout).toBe("named\n");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("accessor with TypeScript annotations", () => {
|
||||
test("accessor with definite assignment assertion (!)", async () => {
|
||||
using dir = tempDir("es-dec-accessor-bang", {
|
||||
"tsconfig.json": JSON.stringify({ compilerOptions: {} }),
|
||||
"test.ts": `
|
||||
function dec(target: any, ctx: any) { return target; }
|
||||
class Foo {
|
||||
@dec accessor child!: string;
|
||||
}
|
||||
const f = new Foo();
|
||||
f.child = "hello";
|
||||
console.log(f.child);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.ts"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
expect(filterStderr(rawStderr)).toBe("");
|
||||
expect(stdout).toBe("hello\n");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("accessor with optional marker (?)", async () => {
|
||||
using dir = tempDir("es-dec-accessor-optional", {
|
||||
"tsconfig.json": JSON.stringify({ compilerOptions: {} }),
|
||||
"test.ts": `
|
||||
function dec(target: any, ctx: any) { return target; }
|
||||
class Foo {
|
||||
@dec accessor child?: string;
|
||||
}
|
||||
const f = new Foo();
|
||||
console.log(f.child);
|
||||
f.child = "world";
|
||||
console.log(f.child);
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.ts"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
expect(filterStderr(rawStderr)).toBe("");
|
||||
expect(stdout).toBe("undefined\nworld\n");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user