Compare commits

...

2 Commits

Author SHA1 Message Date
Jarred Sumner
2c756d9cb8 Merge branch 'main' into claude/fix-declare-invalid-syntax-crash 2025-11-10 11:20:38 -08:00
Claude Bot
8cc412a91c Fix crash when parsing invalid declare syntax
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 <noreply@anthropic.com>
2025-11-08 01:57:52 +00:00
2 changed files with 201 additions and 0 deletions

View File

@@ -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| {

View File

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