Repository navigation
js_parser: parse export default @dec class as a class declaration
#44723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -720,25 +720,31 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
|
|
||
| // ── Public API ─────────────────────────────────────── | ||
|
|
||
| /// `default_export` is the binding of an `export default class` statement, | ||
| /// the one class statement that can have no name. Such a class is named | ||
| /// "default", and takes the binding as its name only when class decorators | ||
| /// have to rebind it. | ||
| pub(crate) fn lower_standard_decorators_stmt( | ||
| &mut self, | ||
| stmt: Stmt, | ||
| default_export: Option<js_ast::LocRef>, | ||
| out: &mut BumpVec<'a, Stmt>, | ||
| ) { | ||
| let mut s_class = match stmt.data { | ||
| js_ast::StmtData::SClass(c) => c, | ||
| _ => unreachable!(), | ||
| }; | ||
| let lowered = self.lower_class_body(&mut s_class.class, stmt.loc, None); | ||
| let name_from_context = default_export.map(|_| js_ast::ClauseItem::DEFAULT_ALIAS); | ||
| let lowered = self.lower_class_body(&mut s_class.class, stmt.loc, name_from_context); | ||
| out.extend(lowered.temps); | ||
| let Some(decorators) = lowered.class_decorators else { | ||
| out.push(stmt); | ||
| return; | ||
| }; | ||
| let name = s_class | ||
| let name = *s_class | ||
| .class | ||
| .class_name | ||
| .expect("a class statement has a name"); | ||
| .get_or_insert_with(|| default_export.expect("a class statement has a name")); | ||
|
Comment on lines
+744
to
+747
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Users who transpile (not bundle) Why this was flaggedInput: a JS or TS module Verification: Trigger: a module run without a bundle containing |
||
| let decorated = self.use_ref(decorators.decorated, name.loc); | ||
| let rebind = self.assign_to(name.ref_, decorated, name.loc); | ||
| out.push(self.expr_stmt(decorators.evaluate, stmt.loc)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7452,9 +7452,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| // Standard decorator lowering path (for both JS and TS files) | ||
| if s_class.class.should_lower_standard_decorators { | ||
| // `lower_standard_decorators_stmt` takes an out-param Vec; wrap to | ||
| // keep this function's slice contract. | ||
| // keep this function's slice contract. `s_export_default` calls | ||
| // it itself, so a class that arrives here has a name. | ||
|
Comment on lines
+7455
to
+7456
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| let mut out = BumpVec::<Stmt>::new_in(self.arena); | ||
| self.lower_standard_decorators_stmt(stmt, &mut out); | ||
| self.lower_standard_decorators_stmt(stmt, None, &mut out); | ||
| return out.into_bump_slice_mut(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1584,8 +1584,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| )); | ||
| } | ||
|
|
||
| // "@" is in the lookahead set that rules out an expression after | ||
| // "export default": a decorator here starts a class declaration. | ||
|
Comment on lines
+1587
to
+1588
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| if p.lexer.token == T::TFunction | ||
| || p.lexer.token == T::TClass | ||
| || p.lexer.token == T::TAt | ||
| || p.lexer.is_contextual_keyword(b"interface") | ||
| { | ||
| let mut _opts = ParseStatementOptions { | ||
|
|
@@ -1594,6 +1597,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| lexical_decl: LexicalDecl::AllowAll, | ||
| ..Default::default() | ||
| }; | ||
| let errors_before_decorators = | ||
| (p.lexer.token == T::TAt).then(|| p.log().errors); | ||
| let stmt = p.parse_stmt(&mut _opts)?; | ||
|
|
||
| let default_name: LocRef = 'default_name_getter: { | ||
|
|
@@ -1623,11 +1628,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| // declaration: the nested statement came back as an | ||
| // expression statement ("export default interface = 2", | ||
| // "export default interface => 1") or a labeled statement | ||
| // ("export default interface: 0"). None of these can be a | ||
| // default export value, so report a syntax error instead of | ||
| // building an S.ExportDefault that the visit and print | ||
| // passes don't support. | ||
| // ("export default interface: 0"). Decorators that no class | ||
| // follows end here too ("export default @dec abstract = 1"). | ||
| // None of these can be a default export value, so report a | ||
| // syntax error instead of building an S.ExportDefault that the | ||
| // visit and print passes don't support. | ||
|
Comment on lines
+1631
to
+1635
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| _ => { | ||
| // `t_at` has reported the token that is not a class. | ||
| if errors_before_decorators | ||
| .is_some_and(|errors| p.log().errors > errors) | ||
| { | ||
| return Err(crate::Error::SyntaxError); | ||
| } | ||
| let r = | ||
| js_lexer::range_of_identifier(p.source, p.real_loc(stmt.loc)); | ||
| p.log().add_range_error_fmt( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -798,30 +798,32 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| replace_expr, | ||
| ) = entry | ||
| { | ||
| // The class is discarded. Lowering it would put it back | ||
| // in `data.value`. | ||
|
Comment on lines
+801
to
+802
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| data.value = js_ast::StmtOrExpr::Expr(replace_expr); | ||
| stmts.push(*stmt); | ||
| } else { | ||
| let _ = p.inject_replacement_export( | ||
| stmts, | ||
| Ref::NONE, | ||
| bun_ast::Loc::EMPTY, | ||
| &entry, | ||
| ); | ||
| restore_dead!(); | ||
| record_on_exit!(); | ||
| return Ok(()); | ||
| } | ||
| restore_dead!(); | ||
| record_on_exit!(); | ||
| return Ok(()); | ||
| } | ||
|
|
||
| if !data.default_name.ref_.is_symbol() { | ||
| data.default_name = p.create_default_name(stmt.loc); | ||
| } | ||
|
|
||
| // We only inject a name into classes when decorator lowering | ||
| // needs one: legacy TS decorators (`has_decorators`) or | ||
| // standard decorator lowering, which also covers classes with | ||
| // only auto-accessor fields and no decorators. | ||
| // The legacy TS decorator lowering reads `class_name`, so an | ||
| // anonymous class takes the default export's symbol as its name. | ||
| // The standard lowering takes that symbol as an argument. | ||
|
Comment on lines
+822
to
+824
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| if class.class.has_decorators | ||
| || class.class.should_lower_standard_decorators | ||
| && !class.class.should_lower_standard_decorators | ||
| { | ||
| if class.class.class_name.is_none() | ||
| || class.class.class_name.unwrap().ref_.is_empty() | ||
|
|
@@ -833,7 +835,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| // Lower the class (handles both TS legacy and standard decorators). | ||
| // Standard decorator lowering may produce prefix statements | ||
| // (variable declarations) before the class statement. | ||
| let class_stmts = p.lower_class(js_ast::StmtOrExpr::Stmt(s2_copy)); | ||
| let class_stmts = if class.class.should_lower_standard_decorators { | ||
| let mut lowered: StmtList<'a> = BumpVec::new_in(p.arena); | ||
| p.lower_standard_decorators_stmt( | ||
| s2_copy, | ||
| Some(data.default_name), | ||
| &mut lowered, | ||
| ); | ||
| lowered.into_bump_slice_mut() | ||
| } else { | ||
| p.lower_class(js_ast::StmtOrExpr::Stmt(s2_copy)) | ||
| }; | ||
|
|
||
| // Find the s_class statement in the returned list | ||
| let mut class_stmt_idx: usize = 0; | ||
|
|
@@ -847,13 +859,36 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool, const SEMA: bool> | |
| // Emit any prefix statements before the export default | ||
| stmts.extend_from_slice(&class_stmts[0..class_stmt_idx]); | ||
|
|
||
| let after_class = &class_stmts[class_stmt_idx + 1..]; | ||
| if p.options.features.server_components.wraps_exports() | ||
| && !after_class.is_empty() | ||
| { | ||
| // Decorator lowering assigns to the class by name after it, so | ||
| // the class stays a declaration and the export wraps its binding. | ||
|
Comment on lines
+866
to
+867
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| let name = class | ||
| .class | ||
| .class_name | ||
| .expect("decorator lowering names the class"); | ||
| stmts.push(class_stmts[class_stmt_idx]); | ||
| stmts.extend_from_slice(after_class); | ||
| p.record_usage(name.ref_); | ||
| data.value = js_ast::StmtOrExpr::Expr( | ||
| p.wrap_value_for_server_component_reference( | ||
| Expr::init_identifier(name.ref_, name.loc), | ||
| b"default", | ||
| ), | ||
| ); | ||
| stmts.push(*stmt); | ||
| restore_dead!(); | ||
| record_on_exit!(); | ||
| return Ok(()); | ||
|
Comment on lines
+863
to
+884
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Production Bake builds of a "use client" or "use server" module with a decorated Why this was flaggedTrigger: a Bake production build, a module under "use server" (src/bundler/ParseTask.rs:2588) or "use client" without a separate SSR graph (src/bundler/ParseTask.rs:2585), containing Verification: src/js_parser/visit/visit_stmt.rs:872-880 pushes the lowered class as a standalone declaration and sets |
||
| } | ||
|
|
||
| data.value = js_ast::StmtOrExpr::Stmt(class_stmts[class_stmt_idx]); | ||
| stmts.push(*stmt); | ||
|
|
||
| // Emit any suffix statements after the export default | ||
| if class_stmt_idx + 1 < class_stmts.len() { | ||
| stmts.extend_from_slice(&class_stmts[class_stmt_idx + 1..]); | ||
| } | ||
| stmts.extend_from_slice(after_class); | ||
|
|
||
| if p.options.features.server_components.wraps_exports() { | ||
| // `data.value` is mutated *after* pushing `stmt`; the pushed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,7 +64,9 @@ bun_core::declare_scope!(cache, visible); | |
| /// `onResolve` rewrote (`namespace:path`). Older entries request the bare path, and | ||
| /// the cache-HIT path reinstates #33904 for them. | ||
| /// Version 34: An import that a plugin `onResolve` answers is printed as it is written. | ||
| const EXPECTED_VERSION: u32 = 34; | ||
| /// Version 35: `export default @dec class` is a class declaration, and standard decorator | ||
| /// lowering names an anonymous `export default class` "default". | ||
|
Comment on lines
+67
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| const EXPECTED_VERSION: u32 = 35; | ||
|
|
||
| /// Source files smaller than this are not written to / read from the on-disk | ||
| /// transpiler cache. Originally 50 KiB, which excluded almost every file in a | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code