mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(parser): typescript module parsing bug (#23284)
### What does this PR do? A bug in our typescript parser was causing `module.foo = foo` to parse as a typescript namespace. If it didn't end with a semicolon and there's a statement on the next line it would cause a syntax error. Example: ```ts module.foo = foo foo.foo = foo ``` fixes #22929 fixes #22883 ### How did you verify your code works? Added a regression test
This commit is contained in:
@@ -1223,8 +1223,9 @@ pub fn ParseStmt(
|
||||
// "module Foo {}"
|
||||
// "declare module 'fs' {}"
|
||||
// "declare module 'fs';"
|
||||
if (((opts.is_module_scope or opts.is_namespace_scope) and (p.lexer.token == .t_identifier or
|
||||
(p.lexer.token == .t_string_literal and opts.is_typescript_declare))))
|
||||
if (!p.lexer.has_newline_before and
|
||||
(opts.is_module_scope or opts.is_namespace_scope) and
|
||||
(p.lexer.token == .t_identifier or (p.lexer.token == .t_string_literal and opts.is_typescript_declare)))
|
||||
{
|
||||
return p.parseTypeScriptNamespaceStmt(loc, opts);
|
||||
}
|
||||
|
||||
115
test/regression/issue/22929-module-extensions-asi.test.ts
Normal file
115
test/regression/issue/22929-module-extensions-asi.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdtempSync, writeFileSync } from "fs";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
test("Module._extensions should not break ASI (automatic semicolon insertion)", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-asi-"));
|
||||
|
||||
// Create a module without semicolons that relies on ASI
|
||||
const moduleWithoutSemi = join(dir, "module-no-semi.js");
|
||||
writeFileSync(
|
||||
moduleWithoutSemi,
|
||||
`function f() {}
|
||||
module.exports = f
|
||||
f.f = f`,
|
||||
);
|
||||
|
||||
// Create a test file that hooks Module._extensions
|
||||
const testFile = join(dir, "test.js");
|
||||
writeFileSync(
|
||||
testFile,
|
||||
`
|
||||
const Module = require("module");
|
||||
const orig = Module._extensions[".js"];
|
||||
|
||||
// Hook Module._extensions[".js"] - commonly done by transpiler libraries
|
||||
Module._extensions[".js"] = (m, f) => {
|
||||
return orig(m, f);
|
||||
};
|
||||
|
||||
// This should work without parse errors
|
||||
const result = require("./module-no-semi.js");
|
||||
if (typeof result !== 'function') {
|
||||
throw new Error('Expected function but got ' + typeof result);
|
||||
}
|
||||
if (result.f !== result) {
|
||||
throw new Error('Expected result.f === result');
|
||||
}
|
||||
console.log('SUCCESS');
|
||||
`,
|
||||
);
|
||||
|
||||
// Run the test
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), testFile],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should not have parse errors
|
||||
expect(stderr).not.toContain("Expected '{'");
|
||||
expect(stderr).not.toContain("Unexpected end of file");
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.trim()).toBe("SUCCESS");
|
||||
});
|
||||
|
||||
test("Module._extensions works with modules that have semicolons", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-semi-"));
|
||||
|
||||
// Create a module with semicolons
|
||||
const moduleWithSemi = join(dir, "module-with-semi.js");
|
||||
writeFileSync(
|
||||
moduleWithSemi,
|
||||
`function g() { return 42; }
|
||||
module.exports = g;
|
||||
g.g = g;`,
|
||||
);
|
||||
|
||||
// Create a test file that hooks Module._extensions
|
||||
const testFile = join(dir, "test.js");
|
||||
writeFileSync(
|
||||
testFile,
|
||||
`
|
||||
const Module = require("module");
|
||||
const orig = Module._extensions[".js"];
|
||||
|
||||
Module._extensions[".js"] = (m, f) => {
|
||||
return orig(m, f);
|
||||
};
|
||||
|
||||
// This should also work with semicolons
|
||||
const result = require("./module-with-semi.js");
|
||||
if (typeof result !== 'function') {
|
||||
throw new Error('Expected function but got ' + typeof result);
|
||||
}
|
||||
if (result() !== 42) {
|
||||
throw new Error('Expected result() === 42');
|
||||
}
|
||||
if (result.g !== result) {
|
||||
throw new Error('Expected result.g === result');
|
||||
}
|
||||
console.log('SUCCESS');
|
||||
`,
|
||||
);
|
||||
|
||||
// Run the test
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), testFile],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should work correctly
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.trim()).toBe("SUCCESS");
|
||||
});
|
||||
Reference in New Issue
Block a user