Fix panic when parsing declare global with type annotations

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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-11-08 01:14:57 +00:00
parent 6f8138b6e4
commit d2cf5603bf
2 changed files with 76 additions and 0 deletions

View File

@@ -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();

View File

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