diff --git a/crates/oxc_parser/src/diagnostics.rs b/crates/oxc_parser/src/diagnostics.rs index c4bf822e2126e..4e67f4a2c8869 100644 --- a/crates/oxc_parser/src/diagnostics.rs +++ b/crates/oxc_parser/src/diagnostics.rs @@ -750,6 +750,14 @@ pub fn jsx_element_no_match(span: Span, span1: Span, name: &str) -> OxcDiagnosti ]) } +#[cold] +pub fn jsx_fragment_no_match(opening_span: Span, closing_span: Span) -> OxcDiagnostic { + OxcDiagnostic::error("Expected corresponding closing tag for JSX fragment.").with_labels([ + closing_span.primary_label("Expected ``"), + opening_span.label("Opened here"), + ]) +} + #[cold] pub fn cover_initialized_name(span: Span) -> OxcDiagnostic { OxcDiagnostic::error("Invalid assignment in object literal") diff --git a/crates/oxc_parser/src/jsx/mod.rs b/crates/oxc_parser/src/jsx/mod.rs index ec22202b05c5e..da2ab25d520cf 100644 --- a/crates/oxc_parser/src/jsx/mod.rs +++ b/crates/oxc_parser/src/jsx/mod.rs @@ -1,11 +1,25 @@ //! [JSX](https://facebook.github.io/jsx) -use oxc_allocator::{Box, Dummy, Vec}; +use oxc_allocator::{Allocator, Box, Dummy, Vec}; use oxc_ast::ast::*; use oxc_span::{Atom, GetSpan, Span}; use crate::{ParserImpl, diagnostics, lexer::Kind}; +/// Represents either a closing JSX element or fragment. +enum JSXClosing<'a> { + /// [`JSXClosingElement`] + Element(Box<'a, JSXClosingElement<'a>>), + /// [`JSXClosingFragment`] + Fragment(JSXClosingFragment), +} + +impl<'a> Dummy<'a> for JSXClosing<'a> { + fn dummy(allocator: &'a Allocator) -> Self { + JSXClosing::Fragment(Dummy::dummy(allocator)) + } +} + impl<'a> ParserImpl<'a> { pub(crate) fn parse_jsx_expression(&mut self) -> Expression<'a> { let span = self.start_span(); @@ -25,8 +39,18 @@ impl<'a> ParserImpl<'a> { fn parse_jsx_fragment(&mut self, span: u32, in_jsx_child: bool) -> Box<'a, JSXFragment<'a>> { self.expect_jsx_child(Kind::RAngle); let opening_fragment = self.ast.jsx_opening_fragment(self.end_span(span)); - let children = self.parse_jsx_children(); - let closing_fragment = self.parse_jsx_closing_fragment(in_jsx_child); + let (children, closing) = self.parse_jsx_children_and_closing(in_jsx_child); + let closing_fragment = match closing { + JSXClosing::Fragment(f) => f, + JSXClosing::Element(e) => { + // Got a closing element when expecting a closing fragment + self.error(diagnostics::jsx_fragment_no_match( + opening_fragment.span, + e.name.span(), + )); + self.ast.jsx_closing_fragment(e.span) + } + }; self.ast.alloc_jsx_fragment( self.end_span(span), opening_fragment, @@ -35,19 +59,6 @@ impl<'a> ParserImpl<'a> { ) } - /// - fn parse_jsx_closing_fragment(&mut self, in_jsx_child: bool) -> JSXClosingFragment { - let span = self.start_span(); - self.expect(Kind::LAngle); - self.expect(Kind::Slash); - if in_jsx_child { - self.expect_jsx_child(Kind::RAngle); - } else { - self.expect(Kind::RAngle); - } - self.ast.jsx_closing_fragment(self.end_span(span)) - } - /// `JSXElement` : /// `JSXSelfClosingElement` /// `JSXOpeningElement` `JSXChildren_opt` `JSXClosingElement` @@ -59,15 +70,27 @@ impl<'a> ParserImpl<'a> { let (children, closing_element) = if self_closing { (self.ast.vec(), None) } else { - let children = self.parse_jsx_children(); - let closing_element = self.parse_jsx_closing_element(in_jsx_child); - if !Self::jsx_element_name_eq(&opening_element.name, &closing_element.name) { - self.error(diagnostics::jsx_element_no_match( - opening_element.name.span(), - closing_element.name.span(), - opening_element.name.span().source_text(self.source_text), - )); - } + let (children, closing) = self.parse_jsx_children_and_closing(in_jsx_child); + let closing_element = match closing { + JSXClosing::Element(e) => { + if !Self::jsx_element_name_eq(&opening_element.name, &e.name) { + self.error(diagnostics::jsx_element_no_match( + opening_element.name.span(), + e.name.span(), + opening_element.name.span().source_text(self.source_text), + )); + } + e + } + JSXClosing::Fragment(f) => { + // Got a closing fragment when expecting a closing element + return self.fatal_error(diagnostics::jsx_element_no_match( + opening_element.name.span(), + f.span, + opening_element.name.span().source_text(self.source_text), + )); + } + }; (children, Some(closing_element)) }; self.ast.alloc_jsx_element(self.end_span(span), opening_element, children, closing_element) @@ -102,19 +125,6 @@ impl<'a> ParserImpl<'a> { (elem, self_closing) } - fn parse_jsx_closing_element(&mut self, in_jsx_child: bool) -> Box<'a, JSXClosingElement<'a>> { - let span = self.start_span(); - self.expect(Kind::LAngle); - self.expect(Kind::Slash); - let name = self.parse_jsx_element_name(); - if in_jsx_child { - self.expect_jsx_child(Kind::RAngle); - } else { - self.expect(Kind::RAngle); - } - self.ast.alloc_jsx_closing_element(self.end_span(span), name) - } - /// `JSXElementName` : /// `JSXIdentifier` /// `JSXNamespacedName` @@ -218,61 +228,101 @@ impl<'a> ParserImpl<'a> { /// `JSXChildren` : /// `JSXChild` `JSXChildren_opt` - fn parse_jsx_children(&mut self) -> Vec<'a, JSXChild<'a>> { + /// Parses children and the closing element/fragment in one pass. + /// Returns `(children, closing)` where closing is either a `JSXClosingElement` or `JSXClosingFragment`. + fn parse_jsx_children_and_closing( + &mut self, + in_jsx_child: bool, + ) -> (Vec<'a, JSXChild<'a>>, JSXClosing<'a>) { let mut children = self.ast.vec(); - while self.fatal_error.is_none() { - if let Some(child) = self.parse_jsx_child() { - children.push(child); - } else { - break; + loop { + if self.fatal_error.is_some() { + // Return dummy closing fragment on fatal error + let closing = self.ast.jsx_closing_fragment(self.cur_token().span()); + return (children, JSXClosing::Fragment(closing)); } - } - children - } - /// `JSXChild` : - /// `JSXText` - /// `JSXElement` - /// `JSXFragment` - /// { `JSXChildExpression_opt` } - fn parse_jsx_child(&mut self) -> Option> { - match self.cur_kind() { - Kind::LAngle => { - let span = self.start_span(); - let checkpoint = self.checkpoint(); - self.bump_any(); // bump `<` - let kind = self.cur_kind(); - // <> open fragment - if kind == Kind::RAngle { - return Some(JSXChild::Fragment(self.parse_jsx_fragment(span, true))); + match self.cur_kind() { + Kind::LAngle => { + let span = self.start_span(); + self.bump_any(); // bump `<` + let kind = self.cur_kind(); + + // <> open nested fragment + if kind == Kind::RAngle { + children.push(JSXChild::Fragment(self.parse_jsx_fragment(span, true))); + continue; + } + + // { + let span_start = self.start_span(); + self.bump_any(); // bump `{` + + // {...expr} + if self.eat(Kind::Dot3) { + children.push(JSXChild::Spread(self.parse_jsx_spread_child(span_start))); + continue; + } + // {expr} + children.push(JSXChild::ExpressionContainer( + self.parse_jsx_expression_container( + span_start, /* in_jsx_child */ true, + ), + )); } - // { + children.push(JSXChild::Text(self.parse_jsx_text())); + } + _ => { + // Unexpected token in JSX children + return (children, self.unexpected()); } - self.unexpected() } - Kind::LCurly => { - let span_start = self.start_span(); - self.bump_any(); // bump `{` + } + } - // {...expr} - if self.eat(Kind::Dot3) { - return Some(JSXChild::Spread(self.parse_jsx_spread_child(span_start))); - } - // {expr} - Some(JSXChild::ExpressionContainer( - self.parse_jsx_expression_container(span_start, /* in_jsx_child */ true), - )) + /// Parses the closing element or fragment after ` JSXClosing<'a> { + if self.at(Kind::RAngle) { + // Closing fragment: + if in_jsx_child { + self.expect_jsx_child(Kind::RAngle); + } else { + self.expect(Kind::RAngle); } - // text - Kind::JSXText => Some(JSXChild::Text(self.parse_jsx_text())), - _ => self.unexpected(), + JSXClosing::Fragment(self.ast.jsx_closing_fragment(self.end_span(open_angle_span))) + } else { + // Closing element: + let name = self.parse_jsx_element_name(); + if in_jsx_child { + self.expect_jsx_child(Kind::RAngle); + } else { + self.expect(Kind::RAngle); + } + JSXClosing::Element( + self.ast.alloc_jsx_closing_element(self.end_span(open_angle_span), name), + ) } } diff --git a/tasks/coverage/snapshots/parser_typescript.snap b/tasks/coverage/snapshots/parser_typescript.snap index 2e27d96c9d270..9de5882760d1f 100644 --- a/tasks/coverage/snapshots/parser_typescript.snap +++ b/tasks/coverage/snapshots/parser_typescript.snap @@ -20867,13 +20867,21 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va · ─ ╰──── - × Expected `>` but found `Identifier` + × Expected corresponding closing tag for JSX fragment. ╭─[typescript/tests/cases/conformance/jsx/tsxFragmentErrors.tsx:9:7] 8 │ 9 │ <>hi // Error - · ─┬─ - · ╰── `>` expected + · ─┬ ─┬─ + · │ ╰── Expected `` + · ╰── Opened here + 10 │ + ╰──── + + × Unexpected token + ╭─[typescript/tests/cases/conformance/jsx/tsxFragmentErrors.tsx:11:2] 10 │ + 11 │ <>eof // Error + · ─ ╰──── × Unexpected token. Did you mean `{'>'}` or `>`?