diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig
index 8f946e079b..d715e4607f 100644
--- a/src/ast/parseStmt.zig
+++ b/src/ast/parseStmt.zig
@@ -1270,6 +1270,25 @@ pub fn ParseStmt(
return p.s(S.TypeScript{}, loc);
}
+ // Validate that "declare" is followed by a valid declaration
+ // Valid: class, function, var, const, let, enum, namespace, module, interface, type, abstract
+ const is_valid_declare_token = switch (p.lexer.token) {
+ .t_class, .t_function, .t_var, .t_const, .t_enum => true,
+ .t_identifier => p.lexer.isContextualKeyword("let") or
+ p.lexer.isContextualKeyword("namespace") or
+ p.lexer.isContextualKeyword("module") or
+ p.lexer.isContextualKeyword("interface") or
+ p.lexer.isContextualKeyword("type") or
+ p.lexer.isContextualKeyword("abstract") or
+ p.lexer.isContextualKeyword("async"),
+ else => false,
+ };
+
+ if (!is_valid_declare_token) {
+ try p.lexer.unexpected();
+ return error.SyntaxError;
+ }
+
// "declare const x: any"
const stmt = try p.parseStmt(opts);
if (opts.ts_decorators) |decs| {
diff --git a/test/js/bun/transpiler/declare-invalid.test.ts b/test/js/bun/transpiler/declare-invalid.test.ts
new file mode 100644
index 0000000000..50b038de66
--- /dev/null
+++ b/test/js/bun/transpiler/declare-invalid.test.ts
@@ -0,0 +1,182 @@
+import { expect, test } from "bun:test";
+import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
+
+test("declare followed by block should error instead of crash", async () => {
+ using dir = tempDir("declare-block-test", {
+ "test.ts": `declare{}`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "build", "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(`
+ "1 | declare{}
+ ^
+ error: Unexpected {
+ at
/test.ts:1:8"
+ `);
+ expect(exitCode).toBe(1);
+});
+
+test("declare block followed by arrow function should error", async () => {
+ using dir = tempDir("declare-block-arrow", {
+ "test.ts": `declare{}_=>_`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "build", "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(`
+ "1 | declare{}_=>_
+ ^
+ error: Unexpected {
+ at /test.ts:1:8"
+ `);
+ expect(exitCode).toBe(1);
+});
+
+test("declare empty block followed by arrow function should error", async () => {
+ using dir = tempDir("declare-empty-block-arrow", {
+ "test.ts": `declare {};()=>{};`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "build", "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(`
+ "1 | declare {};()=>{};
+ ^
+ error: Unexpected {
+ at /test.ts:1:9"
+ `);
+ expect(exitCode).toBe(1);
+});
+
+test("declare multiple blocks followed by arrow should error", async () => {
+ using dir = tempDir("declare-multi-blocks", {
+ "test.ts": `declare{}{;}()=>{};`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "build", "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(`
+ "1 | declare{}{;}()=>{};
+ ^
+ error: Unexpected {
+ at /test.ts:1:8"
+ `);
+ expect(exitCode).toBe(1);
+});
+
+test("declare followed by semicolon should error", async () => {
+ using dir = tempDir("declare-semicolon", {
+ "test.ts": `declare;`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "build", "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(`
+ "1 | declare;
+ ^
+ error: Unexpected ;
+ at /test.ts:1:8"
+ `);
+ expect(exitCode).toBe(1);
+});
+
+test("declare followed by number should error", async () => {
+ using dir = tempDir("declare-number", {
+ "test.ts": `declare 123;`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "build", "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(normalizeBunSnapshot(stderr, dir)).toMatchInlineSnapshot(`
+ "1 | declare 123;
+ ^
+ error: Unexpected 123
+ at /test.ts:1:9"
+ `);
+ expect(exitCode).toBe(1);
+});
+
+test("valid declare statements should still work", async () => {
+ using dir = tempDir("declare-valid", {
+ "test.ts": `
+declare const x: number;
+declare let y: string;
+declare var z: boolean;
+declare class Foo {}
+declare function bar(): void;
+declare enum Baz { A, B }
+declare namespace N {}
+declare module M {}
+declare interface I {}
+declare type T = string;
+declare abstract class Abstract {}
+declare global {
+ const GLOBAL: string;
+}
+
+console.log("SUCCESS");
+`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "test.ts"],
+ env: bunEnv,
+ cwd: String(dir),
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ expect(stdout).toContain("SUCCESS");
+ expect(exitCode).toBe(0);
+});