mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 19:08:50 +00:00
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>
This commit is contained in:
@@ -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| {
|
||||
|
||||
182
test/js/bun/transpiler/declare-invalid.test.ts
Normal file
182
test/js/bun/transpiler/declare-invalid.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user