Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/js_parser/parse/parse_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bun_collections::VecExt;
use crate::Error;
use crate::lexer::{self as js_lexer, T};
use crate::p::P;
use crate::parser::{ParseStatementOptions, Ref, ScopeOrder};
use crate::parser::{FnOrArrowDataParse, ParseStatementOptions, Ref, ScopeOrder};
use bun_alloc::{ArenaVec as BumpVec, ArenaVecExt as _};
use bun_ast::expr::EFlags;
use bun_ast::flags;
Expand Down Expand Up @@ -195,7 +195,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

let old_has_non_local_export_declare_inside_namespace =
p.has_non_local_export_declare_inside_namespace;
let old_fn_or_arrow_data = p.fn_or_arrow_data_parse.clone();
p.has_non_local_export_declare_inside_namespace = false;
p.fn_or_arrow_data_parse = FnOrArrowDataParse {
is_this_disallowed: true,
is_return_disallowed: true,
// parse_fn.rs reads is_top_level to consume a react-hooks
// suppression after a namespace member function; every other
// consumer is gated on allow_await == AllowExpr (AllowIdent here).
is_top_level: old_fn_or_arrow_data.is_top_level,
..Default::default()
};
Comment thread
robobun marked this conversation as resolved.

// Parse the statements inside the namespace
let mut stmts: BumpVec<'_, Stmt> = BumpVec::new_in(p.arena);
Expand Down Expand Up @@ -229,6 +239,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.has_non_local_export_declare_inside_namespace;
p.has_non_local_export_declare_inside_namespace =
old_has_non_local_export_declare_inside_namespace;
p.fn_or_arrow_data_parse = old_fn_or_arrow_data;

// Add any exported members from this namespace's body as members of the
// associated namespace object.
Expand Down Expand Up @@ -568,6 +579,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

p.lexer.expect(T::TOpenBrace)?;

let old_fn_or_arrow_data = p.fn_or_arrow_data_parse.clone();
p.fn_or_arrow_data_parse = FnOrArrowDataParse {
is_this_disallowed: true,
// See the namespace body: preserve is_top_level for parse_fn.rs's
// react-hooks suppression consume.
is_top_level: old_fn_or_arrow_data.is_top_level,
..Default::default()
};

// Parse the body
let mut values: BumpVec<'_, EnumValue> = BumpVec::new_in(p.arena);
while p.lexer.token != T::TCloseBrace {
Expand Down Expand Up @@ -628,6 +648,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.lexer.next()?;
}

p.fn_or_arrow_data_parse = old_fn_or_arrow_data;

if !opts.is_typescript_declare {
// Avoid a collision with the enum closure argument variable if the
// enum exports a symbol with the same name as the enum itself:
Expand Down
26 changes: 26 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,32 @@ describe("bundler", () => {
},
});

itBundled("react-compiler/SuppressionInsideTSNamespaceDoesNotLeak", {
files: {
"/entry.tsx": /* tsx */ `
namespace N {
export function Foo() {
// eslint-disable-next-line react-hooks/rules-of-hooks
useState();
}
}
export function Component({ name }: { name: string }) {
return <div>Hello {name}</div>;
}
`,
},
reactCompiler: true,
backend: "cli",
external: ["react", "react/compiler-runtime", "react/jsx-runtime", "react/jsx-dev-runtime"],
onAfterBundle(api) {
const out = api.readFile("/out.js");
// The next-line suppression inside the namespace member must be consumed
// there and not bail the compiler out of the sibling Component.
expect(out).toContain("react/compiler-runtime");
expect(out).toMatch(/\b_c\(\d+\)/);
},
});

// Stub react packages shared by the unbound-ref regression tests below.
const stubReact = {
"/node_modules/react/index.js": /* js */ `
Expand Down
90 changes: 90 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,96 @@ describe("Bun.Transpiler", () => {
err("enum [] { a }", 'Expected identifier but found "["');
});

it("rejects yield/await/this/super in enum initializers", () => {
const err = ts.expectParseError;
const exp = ts.expectPrinted_;

// The enum body is lowered into an arrow IIFE, so an enclosing function's
// generator/async context must not leak into initializer expressions.
err("function *f() { enum x { y = yield 1 } }", 'Cannot use "yield" outside a generator function');
err("async function f() { enum x { y = await 1 } }", '"await" can only be used inside an "async" function');
err("async function f() { const enum x { y = await 1 } }", '"await" can only be used inside an "async" function');
err("let g = async () => { enum x { y = await 1 } }", '"await" can only be used inside an "async" function');
err("enum x { y = await 1 }", '"await" can only be used inside an "async" function');
err("enum x { y = this }", 'Cannot use "this" here');
err("enum x { y = () => this }", 'Cannot use "this" here');
err("class C { m() { enum x { y = this } } }", 'Cannot use "this" here');
err("class C extends B { m() { enum x { y = super.foo } } }", 'Unexpected "super"');
err("class C extends B { constructor() { super(); enum x { y = super() } } }", 'Unexpected "super"');
err("class C { static { enum x { y = super.foo } } }", 'Unexpected "super"');
err("declare enum x { y = await 1 }", '"await" can only be used inside an "async" function');
err("declare enum x { y = this }", 'Cannot use "this" here');

// A nested function establishes its own context, so these remain valid.
exp(
"function *f() { enum x { y = (function*() { yield 1 })() } }",
'function* f() {\n var x;\n ((x) => {\n x[x["y"] = function* () {\n yield 1;\n }()] = "y";\n })(x ||= {});\n}',
);
exp(
"async function f() { enum x { y = (async () => await 1)() } }",
'async function f() {\n var x;\n ((x) => {\n x[x["y"] = (async () => await 1)()] = "y";\n })(x ||= {});\n}',
);
exp(
"enum x { y = (function() { return this })() }",
'var x;\n((x) => {\n x[x["y"] = function() {\n return this;\n }()] = "y";\n})(x ||= {})',
);
// The enclosing context is restored after the body: sibling statements
// keep their yield/await/super permissions.
exp(
"function *f() { enum x { y = 1 } yield 1; }",
'function* f() {\n var x;\n ((x) => {\n x[x["y"] = 1] = "y";\n })(x ||= {});\n yield 1;\n}',
);
exp(
"async function f() { enum x { y = 1 } await 1; }",
'async function f() {\n var x;\n ((x) => {\n x[x["y"] = 1] = "y";\n })(x ||= {});\n await 1;\n}',
);
exp(
"class C extends B { m() { enum x { y = 1 } super.foo(); } }",
'class C extends B {\n m() {\n var x;\n ((x) => {\n x[x["y"] = 1] = "y";\n })(x ||= {});\n super.foo();\n }\n}',
);
});

it("rejects await/this/return in namespace bodies", () => {
const err = ts.expectParseError;
const exp = ts.expectPrinted_;

err("namespace x { export const y = await 1; }", '"await" can only be used inside an "async" function');
err("namespace x { await 1; }", '"await" can only be used inside an "async" function');
err("namespace x { return 1; }", "A return statement cannot be used here");
err("namespace x { return; }", "A return statement cannot be used here");
err("namespace x { const y: string = this; }", 'Cannot use "this" here');
err("namespace x { export const y = () => this; }", 'Cannot use "this" here');
err("namespace x.y { return 1; }", "A return statement cannot be used here");
err("module x { return 1; }", "A return statement cannot be used here");
err("declare namespace x { export const y = this; }", 'Cannot use "this" here');
err("namespace x { for await (const y of []); }", 'Cannot use "await" outside an async function');

// The namespace body lowers into a non-async arrow, where "await" is a
// valid binding identifier; the module-level reserved-word rule must
// not leak into it.
exp("namespace x { let await = 1; }", "var x;\n((x) => {\n let await = 1;\n})(x ||= {})");
exp(
"namespace x { export function f() { return 1; } }",
"var x;\n((x) => {\n function f() {\n return 1;\n }\n x.f = f;\n})(x ||= {})",
);
exp(
"namespace x { export const y = async () => await 1; }",
"var x;\n((x) => {\n x.y = async () => await 1;\n})(x ||= {})",
);
// Class methods and fields introduce their own "this" binding.
exp(
"namespace x { export class C { m() { return this } } }",
"var x;\n((x) => {\n\n class C {\n m() {\n return this;\n }\n }\n x.C = C;\n})(x ||= {})",
);
exp(
"namespace x { export class C { f = this } }",
"var x;\n((x) => {\n\n class C {\n f = this;\n }\n x.C = C;\n})(x ||= {})",
);
// The enclosing context is restored after the body: top-level await is
// still accepted immediately after a namespace.
exp("namespace x { export const y = 1; } await 1;", "var x;\n((x) => {\n x.y = 1;\n})(x ||= {});\nawait 1");
});

it("doesn't crash with functions assigned to enum values", () => {
const exp = ts.expectPrinted_;

Expand Down
Loading