perf(parser): try hybrid parsing for jsx children and closing element/fragments (#18789)

+1-2% on parsing JSX files.

<img width="713" height="363" alt="Screenshot 2026-02-03 at 10 25 55 PM" src="https://github.com/user-attachments/assets/693c725d-460c-4bb0-bae4-4e85c44efd53" />

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
This commit is contained in:
camchenry committed 2026-02-05 04:35:44 +00:00
1 parent eb7d6d9a41
commit f78c5251fd
3 files changed
+155 -89

No files matched your search

+8
View File
@@ -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")
+136 -86
View File
@@ -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));
}
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;
}
// <ident open nested element
if kind == Kind::Ident || kind.is_any_keyword() {
children.push(JSXChild::Element(self.parse_jsx_element(span, true)));
continue;
}
// </ closing tag - parse it inline and return
if kind == Kind::Slash {
self.bump_any(); // bump `/`
let closing = self.parse_jsx_closing_inline(span, in_jsx_child);
return (children, closing);
}
// Unexpected token after `<`
return (children, self.unexpected());
}
Kind::LCurly => {
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,
),
));
}
// text
Kind::JSXText => {
children.push(JSXChild::Text(self.parse_jsx_text()));
}
_ => {
// Unexpected token in JSX children
return (children, self.unexpected());
}
}
}
children
}
/// `JSXChild` :
/// `JSXText`
/// `JSXElement`
/// `JSXFragment`
/// { `JSXChildExpression_opt` }
fn parse_jsx_child(&mut self) -> Option<JSXChild<'a>> {
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)));
}
// <ident open element
if kind == Kind::Ident || kind.is_any_keyword() {
return Some(JSXChild::Element(self.parse_jsx_element(span, true)));
}
// </ close fragment
if kind == Kind::Slash {
self.rewind(checkpoint);
return None;
}
self.unexpected()
/// Parses the closing element or fragment after `</` has been consumed.
fn parse_jsx_closing_inline(
&mut self,
open_angle_span: u32,
in_jsx_child: bool,
) -> JSXClosing<'a> {
if self.at(Kind::RAngle) {
// Closing fragment: </>
if in_jsx_child {
self.expect_jsx_child(Kind::RAngle);
} else {
self.expect(Kind::RAngle);
}
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),
))
JSXClosing::Fragment(self.ast.jsx_closing_fragment(self.end_span(open_angle_span)))
} else {
// Closing element: </name>
let name = self.parse_jsx_element_name();
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::Element(
self.ast.alloc_jsx_closing_element(self.end_span(open_angle_span), name),
)
}
}
@@ -20867,15 +20867,23 @@ 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</div> // 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 `&gt;`?
╭─[typescript/tests/cases/conformance/jsx/tsxGenericArrowFunctionParsing.tsx:8:17]
7 │ // This is an element