From d2cf5603bfe522b4252cc3e4c282c7df435720e2 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Sat, 8 Nov 2025 01:14:57 +0000 Subject: [PATCH] Fix panic when parsing declare global with type annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When parsing TypeScript code with `declare global { ... }` blocks, identifiers followed by colons (like `TIMER: NodeJS.Timeout;`) were incorrectly treated as labeled statements instead of type annotations. This caused the parser to push a `.label` scope during the parse pass, but when visiting the AST later, arrow functions expected `.function_args` scopes, resulting in a "Scope mismatch while visiting" panic. The fix checks if we're in a TypeScript declare context before treating an identifier followed by a colon as a labeled statement. If we are in a declare context, we skip the type annotation and return a TypeScript node instead of creating a label scope. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ast/parseStmt.zig | 11 ++++ test/js/bun/transpiler/declare-global.test.ts | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 test/js/bun/transpiler/declare-global.test.ts diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig index 8f946e079b..45c0a0b427 100644 --- a/src/ast/parseStmt.zig +++ b/src/ast/parseStmt.zig @@ -1185,6 +1185,17 @@ pub fn ParseStmt( switch (expr.data) { .e_identifier => |ident| { if (p.lexer.token == .t_colon and !opts.hasDecorators()) { + // In TypeScript declare contexts, "identifier: Type" is a type annotation, not a label + if (comptime is_typescript_enabled) { + if (opts.is_typescript_declare) { + // Skip the colon and type annotation + try p.lexer.next(); + try p.skipTypeScriptType(.lowest); + try p.lexer.expectOrInsertSemicolon(); + return p.s(S.TypeScript{}, loc); + } + } + _ = try p.pushScopeForParsePass(.label, loc); defer p.popScope(); diff --git a/test/js/bun/transpiler/declare-global.test.ts b/test/js/bun/transpiler/declare-global.test.ts new file mode 100644 index 0000000000..5ecff87f7e --- /dev/null +++ b/test/js/bun/transpiler/declare-global.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("declare global with type annotation should not crash", async () => { + using dir = tempDir("declare-global-test", { + "test.ts": ` +declare global { + TIMER: NodeJS.Timeout; +} + +if (globalThis.TIMER) clearInterval(globalThis.TIMER); +globalThis.TIMER = setInterval(() => console.log("Started"), 1000); + +setTimeout(() => { + clearInterval(globalThis.TIMER); + console.log("SUCCESS"); + process.exit(0); +}, 100); +`, + }); + + 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(stderr).not.toContain("panic"); + expect(stderr).not.toContain("Scope mismatch"); + expect(stdout).toContain("SUCCESS"); + expect(exitCode).toBe(0); +}); + +test("declare global with multiple type annotations", async () => { + using dir = tempDir("declare-global-multi", { + "test.ts": ` +declare global { + FOO: string; + BAR: number; + BAZ: () => void; +} + +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(stderr).not.toContain("panic"); + expect(stderr).not.toContain("Scope mismatch"); + expect(stdout).toContain("SUCCESS"); + expect(exitCode).toBe(0); +});