use crate::prelude::*;
pub mod jsx_parse_errors;
use biome_js_syntax::JsSyntaxKind::*;
use biome_parser::diagnostic::expected_token;
use biome_parser::parse_lists::ParseNodeList;
use biome_rowan::TextRange;
use crate::lexer::{JsLexContext, JsReLexContext, JsSyntaxKind, T};
use crate::syntax::expr::{
is_nth_at_identifier_or_keyword, parse_expression, parse_name, ExpressionContext,
};
use crate::syntax::js_parse_error::{expected_expression, expected_identifier};
use crate::syntax::jsx::jsx_parse_errors::{
jsx_expected_attribute, jsx_expected_attribute_value, jsx_expected_children,
jsx_expected_closing_tag,
};
use crate::syntax::typescript::TypeContext;
use crate::JsSyntaxFeature::TypeScript;
use crate::{parser::RecoveryResult, JsParser, ParseRecoveryTokenSet, ParsedSyntax};
use crate::{Absent, Present};
use super::typescript::parse_ts_type_arguments;
pub(crate) fn parse_jsx_tag_expression(p: &mut JsParser) -> ParsedSyntax {
if !p.at(T![<]) {
return Absent;
}
if !p.nth_at(1, T![>]) && !is_nth_at_identifier_or_keyword(p, 1) {
return Absent;
}
let m = p.start();
parse_any_jsx_tag(p, true).unwrap();
Present(m.complete(p, JSX_TAG_EXPRESSION))
}
fn parse_any_jsx_tag(p: &mut JsParser, in_expression: bool) -> ParsedSyntax {
match parse_any_jsx_opening_tag(p, in_expression) {
Some(OpeningElement::SelfClosing(marker)) => Present(marker),
Some(OpeningElement::Fragment(fragment_opening)) => {
let opening_range = fragment_opening.range(p);
let fragment = fragment_opening.precede(p);
parse_jsx_children(p);
expect_closing_fragment(p, in_expression, opening_range);
Present(fragment.complete(p, JSX_FRAGMENT))
}
Some(OpeningElement::Element { name, opening }) => {
let opening_range = opening.range(p);
let element = opening.precede(p);
parse_jsx_children(p);
expect_closing_element(p, in_expression, name, opening_range);
Present(element.complete(p, JSX_ELEMENT))
}
None => Absent,
}
}
enum OpeningElement {
Fragment(CompletedMarker),
Element {
name: Option<CompletedMarker>,
opening: CompletedMarker,
},
SelfClosing(CompletedMarker),
}
fn parse_any_jsx_opening_tag(p: &mut JsParser, in_expression: bool) -> Option<OpeningElement> {
if !p.at(T![<]) {
return None;
}
let m = p.start();
p.bump(T![<]);
if p.at(T![>]) {
p.bump_with_context(T![>], JsLexContext::JsxChild);
return Some(OpeningElement::Fragment(
m.complete(p, JSX_OPENING_FRAGMENT),
));
}
let name = parse_jsx_any_element_name(p).or_add_diagnostic(p, expected_identifier);
if TypeScript.is_supported(p) {
let _ = parse_ts_type_arguments(p, TypeContext::default());
}
JsxAttributeList.parse_list(p);
if p.eat(T![/]) {
expect_jsx_token(p, T![>], !in_expression);
Some(OpeningElement::SelfClosing(
m.complete(p, JSX_SELF_CLOSING_ELEMENT),
))
} else {
expect_jsx_token(p, T![>], true);
Some(OpeningElement::Element {
opening: m.complete(p, JSX_OPENING_ELEMENT),
name,
})
}
}
fn expect_closing_fragment(
p: &mut JsParser,
in_expression: bool,
opening_range: TextRange,
) -> CompletedMarker {
let m = p.start();
p.expect(T![<]);
p.expect(T![/]);
if let Present(name) = parse_jsx_any_element_name(p) {
p.error(
p.err_builder(
"JSX fragment has no corresponding closing tag.",
opening_range,
)
.with_detail(opening_range, "Opening fragment")
.with_detail(name.range(p), "Closing tag"),
);
}
expect_jsx_token(p, T![>], !in_expression);
m.complete(p, JSX_CLOSING_FRAGMENT)
}
fn expect_closing_element(
p: &mut JsParser,
in_expression: bool,
opening_name_marker: Option<CompletedMarker>,
opening_range: TextRange,
) -> CompletedMarker {
let m = p.start();
p.expect(T![<]);
p.expect(T![/]);
let name_marker = parse_jsx_any_element_name(p);
if let Some(opening_name_marker) = opening_name_marker {
let opening_name = opening_name_marker.text(p);
let error = match name_marker {
Present(name) if name.text(p) != opening_name => {
let closing_end = if p.at(T![>]) {
p.cur_range().end()
} else {
name.range(p).end()
};
let closing_range = TextRange::new(m.start(), closing_end);
Some(jsx_expected_closing_tag(
p,
opening_name,
opening_range,
closing_range,
))
}
Present(_) => None,
Absent => {
if p.at(T![>]) {
let closing_range = TextRange::new(m.start(), p.cur_range().end());
Some(jsx_expected_closing_tag(
p,
opening_name,
opening_range,
closing_range,
))
} else {
Some(expected_identifier(p, p.cur_range()))
}
}
};
if let Some(error) = error {
p.error(error);
}
}
expect_jsx_token(p, T![>], !in_expression);
m.complete(p, JSX_CLOSING_ELEMENT)
}
fn expect_jsx_token(p: &mut JsParser, token: JsSyntaxKind, before_child_content: bool) {
if !before_child_content {
p.expect(token);
} else if p.at(token) {
p.bump_with_context(token, JsLexContext::JsxChild);
} else {
p.error(expected_token(token));
p.re_lex(JsReLexContext::JsxChild);
}
}
struct JsxChildrenList;
impl ParseNodeList for JsxChildrenList {
type Kind = JsSyntaxKind;
type Parser<'source> = JsParser<'source>;
const LIST_KIND: Self::Kind = JsSyntaxKind::JSX_CHILD_LIST;
fn parse_element(&mut self, p: &mut JsParser) -> ParsedSyntax {
match p.cur() {
T![<] => parse_any_jsx_tag(p, false),
T!['{'] => parse_jsx_expression_child(p),
JsSyntaxKind::JSX_TEXT_LITERAL => {
let m = p.start();
p.bump(JSX_TEXT_LITERAL);
ParsedSyntax::Present(m.complete(p, JSX_TEXT))
}
_ => ParsedSyntax::Absent,
}
}
fn is_at_list_end(&self, p: &mut JsParser) -> bool {
let at_l_angle0 = p.at(T![<]);
let at_slash1 = p.nth_at(1, T![/]);
at_l_angle0 && at_slash1
}
fn recover(&mut self, p: &mut JsParser, parsed_element: ParsedSyntax) -> RecoveryResult {
parsed_element.or_recover_with_token_set(
p,
&ParseRecoveryTokenSet::new(
JsSyntaxKind::JS_BOGUS,
token_set![T![<], T![>], T!['{'], T!['}']],
),
jsx_expected_children,
)
}
}
#[inline]
fn parse_jsx_children(p: &mut JsParser) {
JsxChildrenList.parse_list(p);
}
fn parse_jsx_expression_child(p: &mut JsParser) -> ParsedSyntax {
if !p.at(T!['{']) {
return ParsedSyntax::Absent;
}
let m = p.start();
p.bump(T!['{']);
let is_spread = p.eat(T![...]);
let expr = parse_jsx_assignment_expression(p, is_spread);
if is_spread {
expr.or_add_diagnostic(p, expected_expression);
}
expect_jsx_token(p, T!['}'], true);
let kind = if is_spread {
JsSyntaxKind::JSX_SPREAD_CHILD
} else {
JsSyntaxKind::JSX_EXPRESSION_CHILD
};
ParsedSyntax::Present(m.complete(p, kind))
}
fn parse_jsx_any_element_name(p: &mut JsParser) -> ParsedSyntax {
let name = parse_jsx_name_or_namespace(p);
name.map(|mut name| {
if name.kind(p) == JSX_NAME && (p.at(T![.]) || !is_intrinsic_element(name.text(p))) {
name.change_kind(p, JSX_REFERENCE_IDENTIFIER)
} else if name.kind(p) == JSX_NAMESPACE_NAME && p.at(T![.]) {
let error = p.err_builder(
"JSX property access expressions cannot include JSX namespace names.",
name.range(p),
);
p.error(error);
name.change_to_bogus(p);
}
while p.at(T![.]) {
let m = name.precede(p);
p.bump(T![.]);
parse_name(p).or_add_diagnostic(p, expected_identifier);
name = m.complete(p, JSX_MEMBER_NAME)
}
name
})
}
fn is_intrinsic_element(element_name: &str) -> bool {
if let Some(first) = element_name.chars().next() {
first.is_lowercase()
} else {
false
}
}
fn parse_jsx_name_or_namespace(p: &mut JsParser) -> ParsedSyntax {
parse_jsx_name(p).map(|identifier| {
if p.at(T![:]) {
let m = identifier.precede(p);
p.bump(T![:]);
parse_jsx_name(p).or_add_diagnostic(p, expected_identifier);
m.complete(p, JSX_NAMESPACE_NAME)
} else {
identifier
}
})
}
fn parse_jsx_name(p: &mut JsParser) -> ParsedSyntax {
p.re_lex(JsReLexContext::JsxIdentifier);
if p.at(JSX_IDENT) {
let name = p.start();
p.bump(JSX_IDENT);
Present(name.complete(p, JSX_NAME))
} else {
Absent
}
}
struct JsxAttributeList;
impl ParseNodeList for JsxAttributeList {
type Kind = JsSyntaxKind;
type Parser<'source> = JsParser<'source>;
const LIST_KIND: Self::Kind = JsSyntaxKind::JSX_ATTRIBUTE_LIST;
fn parse_element(&mut self, p: &mut JsParser) -> ParsedSyntax {
if matches!(p.cur(), T!['{'] | T![...]) {
parse_jsx_spread_attribute(p)
} else {
parse_jsx_attribute(p)
}
}
fn is_at_list_end(&self, p: &mut JsParser) -> bool {
matches!(p.cur(), T![>] | T![/] | T![<])
}
fn recover(&mut self, p: &mut JsParser, parsed_element: ParsedSyntax) -> RecoveryResult {
parsed_element.or_recover_with_token_set(
p,
&ParseRecoveryTokenSet::new(
JsSyntaxKind::JS_BOGUS,
token_set![T![/], T![>], T![<], T!['{'], T!['}'], T![...], T![ident]],
),
jsx_expected_attribute,
)
}
}
fn parse_jsx_attribute(p: &mut JsParser) -> ParsedSyntax {
if !is_nth_at_identifier_or_keyword(p, 0) {
return Absent;
}
let m = p.start();
parse_jsx_name_or_namespace(p).unwrap();
let _ = parse_jsx_attribute_initializer_clause(p);
Present(m.complete(p, JsSyntaxKind::JSX_ATTRIBUTE))
}
fn parse_jsx_spread_attribute(p: &mut JsParser) -> ParsedSyntax {
if !matches!(p.cur(), T![...] | T!['{']) {
return Absent;
}
let m = p.start();
p.expect(T!['{']);
p.expect(T![...]);
let argument = parse_expression(p, ExpressionContext::default()).map(|mut expr| {
if expr.kind(p) == JS_SEQUENCE_EXPRESSION {
p.error(p.err_builder(
"Comma operator isn't a valid value for a JSX spread argument.",
expr.range(p),
));
expr.change_to_bogus(p);
}
expr
});
argument.or_add_diagnostic(p, expected_expression);
p.expect(T!['}']);
Present(m.complete(p, JSX_SPREAD_ATTRIBUTE))
}
fn parse_jsx_attribute_initializer_clause(p: &mut JsParser) -> ParsedSyntax {
if !p.at(T![=]) {
return Absent;
}
let m = p.start();
p.bump_with_context(T![=], JsLexContext::JsxAttributeValue);
parse_jsx_attribute_value(p).or_add_diagnostic(p, jsx_expected_attribute_value);
ParsedSyntax::Present(m.complete(p, JsSyntaxKind::JSX_ATTRIBUTE_INITIALIZER_CLAUSE))
}
fn parse_jsx_attribute_value(p: &mut JsParser) -> ParsedSyntax {
match p.cur() {
T!['{'] => parse_jsx_expression_attribute_value(p),
T![<] => parse_any_jsx_tag(p, true),
JsSyntaxKind::JSX_STRING_LITERAL => {
let m = p.start();
p.bump(JSX_STRING_LITERAL);
ParsedSyntax::Present(m.complete(p, JSX_STRING))
}
_ => ParsedSyntax::Absent,
}
}
fn parse_jsx_expression_attribute_value(p: &mut JsParser) -> ParsedSyntax {
if !p.at(T!['{']) {
return ParsedSyntax::Absent;
}
let m = p.start();
p.bump(T!['{']);
parse_jsx_assignment_expression(p, false).or_add_diagnostic(p, expected_expression);
if !p.expect(T!['}']) && p.nth_at(1, T!['}']) {
p.parse_as_skipped_trivia_tokens(|p| {
p.bump_any();
});
p.expect(T!['}']);
}
ParsedSyntax::Present(m.complete(p, JSX_EXPRESSION_ATTRIBUTE_VALUE))
}
fn parse_jsx_assignment_expression(p: &mut JsParser, is_spread: bool) -> ParsedSyntax {
let expr = parse_expression(p, ExpressionContext::default());
expr.map(|mut expr| {
let msg = if is_spread {
"This expression is not valid as a JSX spread expression"
} else {
"This expression is not valid as a JSX expression."
};
let err = match expr.kind(p) {
JsSyntaxKind::JS_IMPORT_META_EXPRESSION
| JsSyntaxKind::JS_NEW_TARGET_EXPRESSION
| JsSyntaxKind::JS_CLASS_EXPRESSION => Some(p.err_builder(msg, expr.range(p))),
JsSyntaxKind::JS_SEQUENCE_EXPRESSION if is_spread => {
Some(p.err_builder(msg, expr.range(p)))
}
_ => None,
};
if let Some(err) = err {
p.error(err);
expr.change_to_bogus(p);
}
expr
})
}