fix(parser): scope mismatch bug from parseSuffix (#23073)

### What does this PR do?
esbuild returns `left` from the inner loop. This PR matches this
behavior. Before it was breaking out of the inner loop and continuing
through the outer loop, potentially parsing too far.

fixes #22013
fixes #22384

### How did you verify your code works?
Added some tests.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Dylan Conway
2025-09-29 03:03:18 -07:00
committed by GitHub
parent f6e722b594
commit 9c75db45fa
3 changed files with 245 additions and 17 deletions

View File

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

View File

@@ -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 (
<html>
</html>
)
}
['1', 'p'].forEach(i =>
app.get(\`/\${i === 'home' ? '' : i}\`, c => c.html(
<Layout selected={i}>
Hello {i}
</Layout>
))
)`,
});
// 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);
});
});

View File

@@ -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 (
<html>
</html>
)
}
['1', 'p'].forEach(i =>
app.get(\`/\${i === 'home' ? '' : i}\`, c => c.html(
<Layout selected={i}>
Hello {i}
</Layout>
))
)`;
// 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 (
<html>
</html>
)
}
['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(");
});
});