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); +});