mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
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:
@@ -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;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
98
test/bundler/transpiler/scope-mismatch-panic.test.ts
Normal file
98
test/bundler/transpiler/scope-mismatch-panic.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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(");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user