diff --git a/src/ast/parseSuffix.zig b/src/ast/parseSuffix.zig
index 642b4df123..d69bd48dab 100644
--- a/src/ast/parseSuffix.zig
+++ b/src/ast/parseSuffix.zig
@@ -827,24 +827,25 @@ pub fn ParseSuffix(
const optional_chain = &optional_chain_;
while (true) {
if (p.lexer.loc().start == p.after_arrow_body_loc.start) {
- while (true) {
- switch (p.lexer.token) {
- .t_comma => {
- if (level.gte(.comma)) {
- break;
- }
+ defer left_and_out.* = left_value;
+ next_token: switch (p.lexer.token) {
+ .t_comma => {
+ if (level.gte(.comma)) {
+ return;
+ }
- try p.lexer.next();
- left.* = p.newExpr(E.Binary{
- .op = .bin_comma,
- .left = left.*,
- .right = try p.parseExpr(.comma),
- }, left.loc);
- },
- else => {
- break;
- },
- }
+ try p.lexer.next();
+ left.* = p.newExpr(E.Binary{
+ .op = .bin_comma,
+ .left = left.*,
+ .right = try p.parseExpr(.comma),
+ }, left.loc);
+
+ continue :next_token p.lexer.token;
+ },
+ else => {
+ return;
+ },
}
}
diff --git a/test/bundler/transpiler/scope-mismatch-panic.test.ts b/test/bundler/transpiler/scope-mismatch-panic.test.ts
new file mode 100644
index 0000000000..720fd48349
--- /dev/null
+++ b/test/bundler/transpiler/scope-mismatch-panic.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, test } from "bun:test";
+import { bunEnv, bunExe, tempDir } from "harness";
+
+describe("scope mismatch panic regression test", () => {
+ test("should not panic with scope mismatch when arrow function is followed by array literal", async () => {
+ // This test reproduces the exact panic that was fixed
+ // The bug caused: "panic(main thread): Scope mismatch while visiting"
+
+ using dir = tempDir("scope-mismatch", {
+ "index.tsx": `
+const Layout = () => {
+ return (
+
+
+ )
+}
+
+['1', 'p'].forEach(i =>
+ app.get(\`/\${i === 'home' ? '' : i}\`, c => c.html(
+
+ Hello {i}
+
+ ))
+)`,
+ });
+
+ // With the bug, this would panic with "Scope mismatch while visiting"
+ // With the fix, it should fail with a normal ReferenceError for 'app'
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "index.tsx"],
+ env: bunEnv,
+ cwd: String(dir),
+ stderr: "pipe",
+ stdout: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ // The key assertion: should NOT panic with scope mismatch
+ expect(stderr).not.toContain("panic");
+ expect(stderr).not.toContain("Scope mismatch");
+
+ // Should fail with a normal error instead (ReferenceError for undefined 'app')
+ expect(stderr).toContain("ReferenceError");
+ expect(stderr).toContain("app is not defined");
+ expect(exitCode).not.toBe(0);
+ });
+
+ test("should not panic with simpler arrow function followed by array", async () => {
+ using dir = tempDir("scope-mismatch-simple", {
+ "test.js": `
+const fn = () => {
+ return 1
+}
+['a', 'b'].forEach(x => console.log(x))`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "test.js"],
+ env: bunEnv,
+ cwd: String(dir),
+ stderr: "pipe",
+ stdout: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ // Should not panic
+ expect(stderr).not.toContain("panic");
+ expect(stderr).not.toContain("Scope mismatch");
+
+ // Should successfully execute
+ expect(stdout).toBe("a\nb\n");
+ expect(exitCode).toBe(0);
+ });
+
+ test("correctly rejects direct indexing into block body arrow function", async () => {
+ using dir = tempDir("scope-mismatch-reject", {
+ "test.js": `const fn = () => {return 1}['x']`,
+ });
+
+ await using proc = Bun.spawn({
+ cmd: [bunExe(), "test.js"],
+ env: bunEnv,
+ cwd: String(dir),
+ stderr: "pipe",
+ stdout: "pipe",
+ });
+
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+
+ // Should fail with a parse error, not a panic
+ expect(stderr).not.toContain("panic");
+ expect(stderr).not.toContain("Scope mismatch");
+ expect(stderr).toContain("error"); // Parse error or similar
+ expect(exitCode).not.toBe(0);
+ });
+});
diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js
index 72ec3fd6ed..615f769a39 100644
--- a/test/bundler/transpiler/transpiler.test.js
+++ b/test/bundler/transpiler/transpiler.test.js
@@ -3566,3 +3566,132 @@ it("Bun.Transpiler.transform stack overflows", async () => {
const transpiler = new Bun.Transpiler();
expect(async () => await transpiler.transform(code)).toThrow(`Maximum call stack size exceeded`);
});
+
+describe("arrow function parsing after const declaration (scope mismatch bug)", () => {
+ const transpiler = new Bun.Transpiler({ loader: "tsx" });
+
+ it("reproduces the original scope mismatch bug with JSX", () => {
+ // This is the exact pattern that caused the scope mismatch panic
+ const code = `
+const Layout = () => {
+ return (
+
+
+ )
+}
+
+['1', 'p'].forEach(i =>
+ app.get(\`/\${i === 'home' ? '' : i}\`, c => c.html(
+
+ Hello {i}
+
+ ))
+)`;
+
+ // Without the fix, this would parse the array as indexing into the arrow function
+ // causing a scope mismatch panic when visiting the AST
+ const result = transpiler.transformSync(code);
+
+ // The correct parse should have the array literal as a separate statement
+ expect(result).toContain("forEach");
+ // The bug would incorrectly parse as: })["1", "p"]
+ expect(result).not.toContain(')["');
+ expect(result).not.toContain('}["');
+ });
+
+ it("correctly parses array literal on next line after block body arrow function", () => {
+ const code = `const Layout = () => {
+ return 1
+}
+['1', 'p'].forEach(i => console.log(i))`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("forEach");
+ // The bug would cause the array to be parsed as indexing: Layout[...
+ expect(result).not.toContain(')["');
+ });
+
+ it("correctly parses JSX arrow function followed by array literal", () => {
+ const code = `const Layout = () => {
+ return (
+
+
+ )
+}
+
+['1', 'p'].forEach(i => console.log(i))`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("forEach");
+ expect(result).not.toContain("Layout[");
+ });
+
+ it("rejects indexing directly into block body arrow function without parens", () => {
+ const code = `const Layout = () => {return 1}['x']`;
+
+ // Should throw a parse error - either "Parse error" or the more specific message
+ expect(() => transpiler.transformSync(code)).toThrow();
+ });
+
+ it("allows indexing into parenthesized arrow function", () => {
+ const code = `const x = (() => {return {a: 1}})['a']`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain('["a"]');
+ });
+
+ it("correctly handles expression body arrow functions", () => {
+ const code = `const Layout = () => 1
+['1', 'p'].forEach(i => console.log(i))`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("forEach");
+ });
+
+ it("correctly handles arrow function with comma operator", () => {
+ const code = `const a = () => {return 1}, b = 2`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("b = 2");
+ });
+
+ it("correctly handles multiple arrow functions in const declaration", () => {
+ const code = `const a = () => {return 1}, b = () => {return 2}
+['1', '2'].forEach(x => console.log(x))`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("forEach");
+ expect(result).not.toContain("b[");
+ });
+
+ it("preserves intentional array access with explicit semicolon", () => {
+ const code = `const Layout = () => {return 1};
+['1', 'p'].forEach(i => console.log(i))`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("forEach");
+ expect(result).not.toContain("Layout[");
+ });
+
+ it("handles nested arrow functions correctly", () => {
+ const code = `const outer = () => {
+ const inner = () => {
+ return 1
+ }
+ return inner
+}
+['test'].forEach(x => x)`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("forEach");
+ });
+
+ it("handles arrow function followed by object literal", () => {
+ const code = `const fn = () => {return 1}
+({a: 1, b: 2}).a`;
+
+ const result = transpiler.transformSync(code);
+ expect(result).toContain("a: 1");
+ expect(result).not.toContain("fn(");
+ });
+});