From 8cc412a91c12d9ee16f7356b836766ef4440f2bf Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 8 Nov 2025 01:57:52 +0000 Subject: [PATCH] Fix crash when parsing invalid declare syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When parsing TypeScript code with invalid `declare` syntax like `declare{}`, `declare{}_=>_`, or `declare {};()=>{};`, the parser would create invalid AST structures that caused "Scope mismatch while visiting" panics during the visit pass. The issue was that `declare` must be followed by a valid declaration keyword (class, function, var, const, let, enum, namespace, module, interface, type, abstract, async, or global), but the parser wasn't validating this. When followed by invalid tokens like `{`, the parser would create `.block` scopes that didn't match the expected scope types for subsequent constructs like arrow functions. This fix adds validation to ensure `declare` is only followed by valid declaration keywords, reporting a proper syntax error for invalid cases instead of crashing with a scope mismatch panic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ast/parseStmt.zig | 19 ++ .../js/bun/transpiler/declare-invalid.test.ts | 182 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 test/js/bun/transpiler/declare-invalid.test.ts 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); +});