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:
Dylan Conway
2025-10-06 00:37:29 -07:00
committed by GitHub
parent a9c0ec63e8
commit d292dcad26
2 changed files with 118 additions and 2 deletions

View File

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

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