From f78c5251fd08c2d88064f337d83a1f4a6f16cf20 Mon Sep 17 00:00:00 2001 From: camchenry <1514176+camchenry@users.noreply.github.com> Date: Thu, 5 Feb 2026 04:35:44 +0000 Subject: [PATCH] perf(parser): try hybrid parsing for jsx children and closing element/fragments (#18789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit +1-2% on parsing JSX files. Screenshot 2026-02-03 at 10 25 55 PM Previously, we were rewinding the parser state whenever we hit a closing fragment and then calling a different function to continue parsing the applicable type of closing tag. Now, we do all of the children and closing tag parsing in one function. This allows us to never rewind the parser state which should be faster. It also allows us to provide slightly better diagnostics as we know what state we are in. Diagnostic changes are expected, it's a little bit more similar to TypeScript's diagnostics: https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEA3W8BKIpgC4C55QDsBPAbgCgyAeAPgAsBLSgemHsWviafgFEYYA9jAoUaIAQDN40rr35CgA --- crates/oxc_parser/src/diagnostics.rs | 8 + crates/oxc_parser/src/jsx/mod.rs | 218 +++++++++++------- .../coverage/snapshots/parser_typescript.snap | 14 +- 3 files changed, 153 insertions(+), 87 deletions(-) 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 `>`?