mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 12:51:57 +00:00
474 lines
18 KiB
Rust
474 lines
18 KiB
Rust
//! [JSX](https://facebook.github.io/jsx)
|
|
|
|
#![allow(clippy::missing_errors_doc)]
|
|
|
|
use oxc_allocator::{Box, Vec};
|
|
use oxc_ast::ast::*;
|
|
use oxc_diagnostics::Result;
|
|
use oxc_span::{Atom, GetSpan, Span};
|
|
|
|
use crate::{diagnostics, lexer::Kind, Context, ParserImpl};
|
|
|
|
impl<'a> ParserImpl<'a> {
|
|
pub(crate) fn parse_jsx_expression(&mut self) -> Result<Expression<'a>> {
|
|
if self.peek_at(Kind::RAngle) {
|
|
self.parse_jsx_fragment(false).map(Expression::JSXFragment)
|
|
} else {
|
|
self.parse_jsx_element(false).map(Expression::JSXElement)
|
|
}
|
|
}
|
|
|
|
/// `JSXFragment` :
|
|
/// < > `JSXChildren_opt` < / >
|
|
fn parse_jsx_fragment(&mut self, in_jsx_child: bool) -> Result<Box<'a, JSXFragment<'a>>> {
|
|
let span = self.start_span();
|
|
let opening_fragment = self.parse_jsx_opening_fragment(span)?;
|
|
let children = self.parse_jsx_children()?;
|
|
let closing_fragment = self.parse_jsx_closing_fragment(in_jsx_child)?;
|
|
Ok(self.ast.alloc_jsx_fragment(
|
|
self.end_span(span),
|
|
opening_fragment,
|
|
closing_fragment,
|
|
children,
|
|
))
|
|
}
|
|
|
|
/// <>
|
|
fn parse_jsx_opening_fragment(&mut self, span: Span) -> Result<JSXOpeningFragment> {
|
|
self.expect(Kind::LAngle)?;
|
|
self.expect_jsx_child(Kind::RAngle)?;
|
|
Ok(self.ast.jsx_opening_fragment(self.end_span(span)))
|
|
}
|
|
|
|
/// </>
|
|
fn parse_jsx_closing_fragment(&mut self, in_jsx_child: bool) -> Result<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)?;
|
|
}
|
|
Ok(self.ast.jsx_closing_fragment(self.end_span(span)))
|
|
}
|
|
|
|
/// `JSXElement` :
|
|
/// `JSXSelfClosingElement`
|
|
/// `JSXOpeningElement` `JSXChildren_opt` `JSXClosingElement`
|
|
/// `in_jsx_child`:
|
|
/// used for telling `JSXClosingElement` to parse the next jsx child or not
|
|
/// true when inside jsx element, false when at top level expression
|
|
fn parse_jsx_element(&mut self, in_jsx_child: bool) -> Result<Box<'a, JSXElement<'a>>> {
|
|
let span = self.start_span();
|
|
let opening_element = self.parse_jsx_opening_element(span, in_jsx_child)?;
|
|
let children =
|
|
if opening_element.self_closing { self.ast.vec() } else { self.parse_jsx_children()? };
|
|
let closing_element = if opening_element.self_closing {
|
|
None
|
|
} else {
|
|
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),
|
|
));
|
|
}
|
|
Some(closing_element)
|
|
};
|
|
Ok(self.ast.alloc_jsx_element(
|
|
self.end_span(span),
|
|
opening_element,
|
|
closing_element,
|
|
children,
|
|
))
|
|
}
|
|
|
|
/// `JSXOpeningElement` :
|
|
/// < `JSXElementName` `JSXAttributes_opt` >
|
|
fn parse_jsx_opening_element(
|
|
&mut self,
|
|
span: Span,
|
|
in_jsx_child: bool,
|
|
) -> Result<Box<'a, JSXOpeningElement<'a>>> {
|
|
self.expect(Kind::LAngle)?;
|
|
let name = self.parse_jsx_element_name()?;
|
|
// <Component<TsType> for tsx
|
|
let type_parameters = if self.is_ts { self.try_parse_type_arguments()? } else { None };
|
|
let attributes = self.parse_jsx_attributes()?;
|
|
let self_closing = self.eat(Kind::Slash);
|
|
if !self_closing || in_jsx_child {
|
|
self.expect_jsx_child(Kind::RAngle)?;
|
|
} else {
|
|
self.expect(Kind::RAngle)?;
|
|
}
|
|
Ok(self.ast.alloc_jsx_opening_element(
|
|
self.end_span(span),
|
|
self_closing,
|
|
name,
|
|
attributes,
|
|
type_parameters,
|
|
))
|
|
}
|
|
|
|
fn parse_jsx_closing_element(
|
|
&mut self,
|
|
in_jsx_child: bool,
|
|
) -> Result<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)?;
|
|
}
|
|
Ok(self.ast.alloc_jsx_closing_element(self.end_span(span), name))
|
|
}
|
|
|
|
/// `JSXElementName` :
|
|
/// `JSXIdentifier`
|
|
/// `JSXNamespacedName`
|
|
/// `JSXMemberExpression`
|
|
fn parse_jsx_element_name(&mut self) -> Result<JSXElementName<'a>> {
|
|
let span = self.start_span();
|
|
let identifier = self.parse_jsx_identifier()?;
|
|
|
|
// <namespace:property />
|
|
if self.eat(Kind::Colon) {
|
|
let property = self.parse_jsx_identifier()?;
|
|
return Ok(self.ast.jsx_element_name_jsx_namespaced_name(
|
|
self.end_span(span),
|
|
identifier,
|
|
property,
|
|
));
|
|
}
|
|
|
|
// <member.foo.bar />
|
|
if self.at(Kind::Dot) {
|
|
return self
|
|
.parse_jsx_member_expression(span, identifier)
|
|
.map(JSXElementName::MemberExpression);
|
|
}
|
|
|
|
// References begin with a capital letter, `_` or `$` e.g. `<Foo>`, `<_foo>`, `<$foo>`.
|
|
// https://babeljs.io/repl#?code_lz=DwMQ9mAED0B8DcAoYAzCMHIPpqnJwAJLhkkA&presets=react
|
|
// The identifier has already been checked to be valid, so when first char is ASCII, it can only
|
|
// be `a-z`, `A-Z`, `_` or `$`. But compiler doesn't know that, so we can help it create faster
|
|
// code by taking that invariant into account.
|
|
// `b < b'a'` matches `A-Z`, `_` and `$`.
|
|
// Use a fast path for common case of ASCII characters, to avoid the more expensive
|
|
// `char::is_uppercase` in most cases.
|
|
let name = identifier.name.as_str();
|
|
let is_reference = match name.as_bytes()[0] {
|
|
b if b.is_ascii() => b < b'a',
|
|
_ => name.chars().next().unwrap().is_uppercase(),
|
|
};
|
|
|
|
let element_name = if is_reference {
|
|
let identifier = self.ast.identifier_reference(identifier.span, identifier.name);
|
|
JSXElementName::IdentifierReference(self.ast.alloc(identifier))
|
|
} else if name == "this" {
|
|
JSXElementName::ThisExpression(
|
|
self.ast.alloc(self.ast.this_expression(identifier.span)),
|
|
)
|
|
} else {
|
|
JSXElementName::Identifier(self.ast.alloc(identifier))
|
|
};
|
|
Ok(element_name)
|
|
}
|
|
|
|
/// `JSXMemberExpression` :
|
|
/// `JSXIdentifier` . `JSXIdentifier`
|
|
/// `JSXMemberExpression` . `JSXIdentifier`
|
|
fn parse_jsx_member_expression(
|
|
&mut self,
|
|
span: Span,
|
|
object: JSXIdentifier<'a>,
|
|
) -> Result<Box<'a, JSXMemberExpression<'a>>> {
|
|
let mut object = if object.name == "this" {
|
|
let object = self.ast.this_expression(object.span);
|
|
JSXMemberExpressionObject::ThisExpression(self.ast.alloc(object))
|
|
} else {
|
|
let object = self.ast.identifier_reference(object.span, object.name);
|
|
JSXMemberExpressionObject::IdentifierReference(self.ast.alloc(object))
|
|
};
|
|
|
|
let mut span = span;
|
|
let mut property = None;
|
|
|
|
while self.eat(Kind::Dot) && !self.at(Kind::Eof) {
|
|
// <foo.bar.baz>
|
|
if let Some(prop) = property {
|
|
object =
|
|
self.ast.jsx_member_expression_object_jsx_member_expression(span, object, prop);
|
|
}
|
|
|
|
// <foo.bar>
|
|
let ident = self.parse_jsx_identifier()?;
|
|
// `<foo.bar- />` is a syntax error.
|
|
if ident.name.contains('-') {
|
|
return Err(diagnostics::unexpected_token(ident.span));
|
|
}
|
|
property = Some(ident);
|
|
span = self.end_span(span);
|
|
}
|
|
|
|
if let Some(property) = property {
|
|
return Ok(self.ast.alloc_jsx_member_expression(self.end_span(span), object, property));
|
|
}
|
|
|
|
Err(self.unexpected())
|
|
}
|
|
|
|
/// `JSXChildren` :
|
|
/// `JSXChild` `JSXChildren_opt`
|
|
fn parse_jsx_children(&mut self) -> Result<Vec<'a, JSXChild<'a>>> {
|
|
let mut children = self.ast.vec();
|
|
while !self.at(Kind::Eof) {
|
|
if let Some(child) = self.parse_jsx_child()? {
|
|
children.push(child);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
Ok(children)
|
|
}
|
|
|
|
/// `JSXChild` :
|
|
/// `JSXText`
|
|
/// `JSXElement`
|
|
/// `JSXFragment`
|
|
/// { `JSXChildExpression_opt` }
|
|
fn parse_jsx_child(&mut self) -> Result<Option<JSXChild<'a>>> {
|
|
match self.cur_kind() {
|
|
// </ close fragment
|
|
Kind::LAngle if self.peek_at(Kind::Slash) => Ok(None),
|
|
// <> open fragment
|
|
Kind::LAngle if self.peek_at(Kind::RAngle) => {
|
|
self.parse_jsx_fragment(true).map(JSXChild::Fragment).map(Some)
|
|
}
|
|
// <ident open element
|
|
Kind::LAngle if self.peek_at(Kind::Ident) || self.peek_kind().is_all_keyword() => {
|
|
self.parse_jsx_element(true).map(JSXChild::Element).map(Some)
|
|
}
|
|
// {...expr}
|
|
Kind::LCurly if self.peek_at(Kind::Dot3) => {
|
|
self.parse_jsx_spread_child().map(JSXChild::Spread).map(Some)
|
|
}
|
|
// {expr}
|
|
Kind::LCurly => {
|
|
self.parse_jsx_expression_container(/* is_jsx_child */ true)
|
|
.map(JSXChild::ExpressionContainer)
|
|
.map(Some)
|
|
}
|
|
// text
|
|
Kind::JSXText => Ok(Some(JSXChild::Text(self.parse_jsx_text()))),
|
|
_ => Err(self.unexpected()),
|
|
}
|
|
}
|
|
|
|
/// { `JSXChildExpression_opt` }
|
|
fn parse_jsx_expression_container(
|
|
&mut self,
|
|
in_jsx_child: bool,
|
|
) -> Result<Box<'a, JSXExpressionContainer<'a>>> {
|
|
let span = self.start_span();
|
|
self.bump_any(); // bump `{`
|
|
|
|
let expr = if self.at(Kind::RCurly) {
|
|
if in_jsx_child {
|
|
self.expect_jsx_child(Kind::RCurly)
|
|
} else {
|
|
self.expect(Kind::RCurly)
|
|
}?;
|
|
let span = self.end_span(span);
|
|
// Handle comment between curly braces (ex. `{/* comment */}`)
|
|
// ^^^^^^^^^^^^^ span
|
|
let expr = self.ast.jsx_empty_expression(Span::new(span.start + 1, span.end - 1));
|
|
JSXExpression::EmptyExpression(expr)
|
|
} else {
|
|
let expr = self.parse_jsx_assignment_expression().map(JSXExpression::from)?;
|
|
if in_jsx_child {
|
|
self.expect_jsx_child(Kind::RCurly)
|
|
} else {
|
|
self.expect(Kind::RCurly)
|
|
}?;
|
|
expr
|
|
};
|
|
|
|
Ok(self.ast.alloc_jsx_expression_container(self.end_span(span), expr))
|
|
}
|
|
|
|
fn parse_jsx_assignment_expression(&mut self) -> Result<Expression<'a>> {
|
|
self.context(Context::default().and_await(self.ctx.has_await()), self.ctx, |p| {
|
|
let expr = p.parse_expr();
|
|
if let Ok(Expression::SequenceExpression(seq)) = &expr {
|
|
return Err(diagnostics::jsx_expressions_may_not_use_the_comma_operator(seq.span));
|
|
}
|
|
expr
|
|
})
|
|
}
|
|
|
|
/// `JSXChildExpression` :
|
|
/// { ... `AssignmentExpression` }
|
|
fn parse_jsx_spread_child(&mut self) -> Result<Box<'a, JSXSpreadChild<'a>>> {
|
|
let span = self.start_span();
|
|
self.bump_any(); // bump `{`
|
|
self.expect(Kind::Dot3)?;
|
|
let expr = self.parse_jsx_assignment_expression()?;
|
|
self.expect_jsx_child(Kind::RCurly)?;
|
|
Ok(self.ast.alloc_jsx_spread_child(self.end_span(span), expr))
|
|
}
|
|
|
|
/// `JSXAttributes` :
|
|
/// `JSXSpreadAttribute` `JSXAttributes_opt`
|
|
/// `JSXAttribute` `JSXAttributes_opt`
|
|
fn parse_jsx_attributes(&mut self) -> Result<Vec<'a, JSXAttributeItem<'a>>> {
|
|
let mut attributes = self.ast.vec();
|
|
while !matches!(self.cur_kind(), Kind::Eof | Kind::LAngle | Kind::RAngle | Kind::Slash) {
|
|
let attribute = match self.cur_kind() {
|
|
Kind::LCurly => {
|
|
self.parse_jsx_spread_attribute().map(JSXAttributeItem::SpreadAttribute)
|
|
}
|
|
_ => self.parse_jsx_attribute().map(JSXAttributeItem::Attribute),
|
|
}?;
|
|
attributes.push(attribute);
|
|
}
|
|
Ok(attributes)
|
|
}
|
|
|
|
/// `JSXAttribute` :
|
|
/// `JSXAttributeName` `JSXAttributeInitializer_opt`
|
|
fn parse_jsx_attribute(&mut self) -> Result<Box<'a, JSXAttribute<'a>>> {
|
|
let span = self.start_span();
|
|
let name = self.parse_jsx_attribute_name()?;
|
|
let value = if self.at(Kind::Eq) {
|
|
self.expect_jsx_attribute_value(Kind::Eq)?;
|
|
Some(self.parse_jsx_attribute_value()?)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(self.ast.alloc_jsx_attribute(self.end_span(span), name, value))
|
|
}
|
|
|
|
/// `JSXSpreadAttribute` :
|
|
/// { ... `AssignmentExpression` }
|
|
fn parse_jsx_spread_attribute(&mut self) -> Result<Box<'a, JSXSpreadAttribute<'a>>> {
|
|
let span = self.start_span();
|
|
self.bump_any(); // bump `{`
|
|
self.expect(Kind::Dot3)?;
|
|
let argument = self.parse_jsx_assignment_expression()?;
|
|
self.expect(Kind::RCurly)?;
|
|
Ok(self.ast.alloc_jsx_spread_attribute(self.end_span(span), argument))
|
|
}
|
|
|
|
/// `JSXAttributeName` :
|
|
/// `JSXIdentifier`
|
|
/// `JSXNamespacedName`
|
|
fn parse_jsx_attribute_name(&mut self) -> Result<JSXAttributeName<'a>> {
|
|
let span = self.start_span();
|
|
let identifier = self.parse_jsx_identifier()?;
|
|
|
|
if self.eat(Kind::Colon) {
|
|
let property = self.parse_jsx_identifier()?;
|
|
return Ok(self.ast.jsx_attribute_name_jsx_namespaced_name(
|
|
self.end_span(span),
|
|
identifier,
|
|
property,
|
|
));
|
|
}
|
|
|
|
Ok(JSXAttributeName::Identifier(self.ast.alloc(identifier)))
|
|
}
|
|
|
|
fn parse_jsx_attribute_value(&mut self) -> Result<JSXAttributeValue<'a>> {
|
|
match self.cur_kind() {
|
|
Kind::Str => self
|
|
.parse_literal_string()
|
|
.map(|str_lit| JSXAttributeValue::StringLiteral(self.ast.alloc(str_lit))),
|
|
Kind::LCurly => {
|
|
let expr = self.parse_jsx_expression_container(/* is_jsx_child */ false)?;
|
|
Ok(JSXAttributeValue::ExpressionContainer(expr))
|
|
}
|
|
Kind::LAngle => {
|
|
if self.peek_at(Kind::RAngle) {
|
|
self.parse_jsx_fragment(false).map(JSXAttributeValue::Fragment)
|
|
} else {
|
|
self.parse_jsx_element(false).map(JSXAttributeValue::Element)
|
|
}
|
|
}
|
|
_ => Err(self.unexpected()),
|
|
}
|
|
}
|
|
|
|
/// `JSXIdentifier` :
|
|
/// `IdentifierStart`
|
|
/// `JSXIdentifier` `IdentifierPart`
|
|
/// `JSXIdentifier` [no `WhiteSpace` or Comment here] -
|
|
fn parse_jsx_identifier(&mut self) -> Result<JSXIdentifier<'a>> {
|
|
let span = self.start_span();
|
|
if !self.at(Kind::Ident) && !self.cur_kind().is_all_keyword() {
|
|
return Err(self.unexpected());
|
|
}
|
|
// Currently at a valid normal Ident or Keyword, keep on lexing for `-` in `<component-name />`
|
|
self.continue_lex_jsx_identifier();
|
|
self.bump_any();
|
|
let span = self.end_span(span);
|
|
let name = span.source_text(self.source_text);
|
|
Ok(self.ast.jsx_identifier(span, name))
|
|
}
|
|
|
|
fn parse_jsx_text(&mut self) -> Box<'a, JSXText<'a>> {
|
|
let span = self.start_span();
|
|
let value = Atom::from(self.cur_string());
|
|
self.bump_any();
|
|
self.ast.alloc_jsx_text(self.end_span(span), value)
|
|
}
|
|
|
|
fn jsx_element_name_eq(lhs: &JSXElementName<'a>, rhs: &JSXElementName<'a>) -> bool {
|
|
match (lhs, rhs) {
|
|
(JSXElementName::Identifier(lhs), JSXElementName::Identifier(rhs)) => {
|
|
lhs.name == rhs.name
|
|
}
|
|
(
|
|
JSXElementName::IdentifierReference(lhs),
|
|
JSXElementName::IdentifierReference(rhs),
|
|
) => lhs.name == rhs.name,
|
|
(JSXElementName::NamespacedName(lhs), JSXElementName::NamespacedName(rhs)) => {
|
|
lhs.namespace.name == rhs.namespace.name && lhs.property.name == rhs.property.name
|
|
}
|
|
(JSXElementName::MemberExpression(lhs), JSXElementName::MemberExpression(rhs)) => {
|
|
Self::jsx_member_expression_eq(lhs, rhs)
|
|
}
|
|
(JSXElementName::ThisExpression(_), JSXElementName::ThisExpression(_)) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn jsx_member_expression_eq(
|
|
lhs: &JSXMemberExpression<'a>,
|
|
rhs: &JSXMemberExpression<'a>,
|
|
) -> bool {
|
|
if lhs.property.name != rhs.property.name {
|
|
return false;
|
|
}
|
|
match (&lhs.object, &rhs.object) {
|
|
(
|
|
JSXMemberExpressionObject::IdentifierReference(lhs),
|
|
JSXMemberExpressionObject::IdentifierReference(rhs),
|
|
) => lhs.name == rhs.name,
|
|
(
|
|
JSXMemberExpressionObject::MemberExpression(lhs),
|
|
JSXMemberExpressionObject::MemberExpression(rhs),
|
|
) => Self::jsx_member_expression_eq(lhs, rhs),
|
|
(
|
|
JSXMemberExpressionObject::ThisExpression(_),
|
|
JSXMemberExpressionObject::ThisExpression(_),
|
|
) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|