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("); + }); +});