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
8 changes: 8 additions & 0 deletions crates/oxc_parser/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
218 changes: 134 additions & 84 deletions crates/oxc_parser/src/jsx/mod.rs
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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,
Expand All @@ -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`
Expand All @@ -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)
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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<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)));
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());
}
// <ident open element
if kind == Kind::Ident || kind.is_any_keyword() {
return Some(JSXChild::Element(self.parse_jsx_element(span, true)));
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,
),
));
}
// </ close fragment
if kind == Kind::Slash {
self.rewind(checkpoint);
return None;
// text
Kind::JSXText => {
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 `</` 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);
}
// 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: </name>
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),
)
}
}

Expand Down
14 changes: 11 additions & 3 deletions tasks/coverage/snapshots/parser_typescript.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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</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;`?
Expand Down
Loading