diff --git a/crates/oxc_parser/src/diagnostics.rs b/crates/oxc_parser/src/diagnostics.rs index d25e8ef74ef10..48260e5fe4992 100644 --- a/crates/oxc_parser/src/diagnostics.rs +++ b/crates/oxc_parser/src/diagnostics.rs @@ -402,3 +402,9 @@ pub fn static_constructor(span0: Span) -> OxcDiagnostic { OxcDiagnostic::error("TS1089: `static` modifier cannot appear on a constructor declaration.") .with_labels([span0.into()]) } + +#[cold] +pub fn jsx_element_no_match(span0: Span, span1: Span, name: &str) -> OxcDiagnostic { + OxcDiagnostic::error(format!("Expected corresponding JSX closing tag for '{name}'.")) + .with_labels([span0.into(), span1.into()]) +} diff --git a/crates/oxc_parser/src/jsx/mod.rs b/crates/oxc_parser/src/jsx/mod.rs index 2c281afaa1f61..e9535bb0c3d50 100644 --- a/crates/oxc_parser/src/jsx/mod.rs +++ b/crates/oxc_parser/src/jsx/mod.rs @@ -5,7 +5,7 @@ use oxc_allocator::{Box, Vec}; use oxc_ast::ast::*; use oxc_diagnostics::Result; -use oxc_span::{Atom, Span}; +use oxc_span::{Atom, GetSpan, Span}; use crate::{diagnostics, lexer::Kind, Context, ParserImpl}; @@ -62,9 +62,19 @@ impl<'a> ParserImpl<'a> { } else { self.parse_jsx_children()? }; - let closing_element = (!opening_element.self_closing) - .then(|| self.parse_jsx_closing_element(in_jsx_child)) - .transpose()?; + 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.jsx_element(self.end_span(span), opening_element, closing_element, children)) } @@ -378,4 +388,39 @@ impl<'a> ParserImpl<'a> { self.bump_any(); self.ast.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::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) + } + _ => 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::Identifier(lhs), + JSXMemberExpressionObject::Identifier(rhs), + ) => lhs.name == rhs.name, + ( + JSXMemberExpressionObject::MemberExpression(lhs), + JSXMemberExpressionObject::MemberExpression(rhs), + ) => Self::jsx_member_expression_eq(lhs, rhs), + _ => false, + } + } } diff --git a/tasks/coverage/misc/fail/oxc-3528.jsx b/tasks/coverage/misc/fail/oxc-3528.jsx new file mode 100644 index 0000000000000..4d87a37fc4a29 --- /dev/null +++ b/tasks/coverage/misc/fail/oxc-3528.jsx @@ -0,0 +1,5 @@ +let a = ; + +let b = ; + +let c = ; diff --git a/tasks/coverage/parser_misc.snap b/tasks/coverage/parser_misc.snap index 089eedeec8470..01dbdf0611a4b 100644 --- a/tasks/coverage/parser_misc.snap +++ b/tasks/coverage/parser_misc.snap @@ -1,7 +1,7 @@ parser_misc Summary: AST Parsed : 18/18 (100.00%) Positive Passed: 18/18 (100.00%) -Negative Passed: 9/9 (100.00%) +Negative Passed: 10/10 (100.00%) × Unexpected token ╭─[fail/oxc-169.js:2:1] @@ -105,6 +105,28 @@ Negative Passed: 9/9 (100.00%) · ─ ╰──── + × Expected corresponding JSX closing tag for 'Apple'. + ╭─[fail/oxc-3528.jsx:1:10] + 1 │ let a = ; + · ───── ────── + 2 │ + ╰──── + + × Expected corresponding JSX closing tag for 'Apple:Orange'. + ╭─[fail/oxc-3528.jsx:3:10] + 2 │ + 3 │ let b = ; + · ──────────── ────── + 4 │ + ╰──── + + × Expected corresponding JSX closing tag for 'Apple.Orange'. + ╭─[fail/oxc-3528.jsx:5:10] + 4 │ + 5 │ let c = ; + · ──────────── ────── + ╰──── + × The keyword 'let' is reserved ╭─[fail/oxc.js:1:1] 1 │ let.a = 1; diff --git a/tasks/coverage/parser_typescript.snap b/tasks/coverage/parser_typescript.snap index b12f546d290bb..c50b0a0e8a836 100644 --- a/tasks/coverage/parser_typescript.snap +++ b/tasks/coverage/parser_typescript.snap @@ -3,7 +3,7 @@ commit: d8086f14 parser_typescript Summary: AST Parsed : 5280/5283 (99.94%) Positive Passed: 5273/5283 (99.81%) -Negative Passed: 1052/4875 (21.58%) +Negative Passed: 1053/4875 (21.60%) Expect Syntax Error: "compiler/ClassDeclaration10.ts" Expect Syntax Error: "compiler/ClassDeclaration11.ts" Expect Syntax Error: "compiler/ClassDeclaration13.ts" @@ -3026,7 +3026,6 @@ Expect Syntax Error: "conformance/jsx/tsxUnionElementType3.tsx" Expect Syntax Error: "conformance/jsx/tsxUnionElementType4.tsx" Expect Syntax Error: "conformance/jsx/tsxUnionElementType6.tsx" Expect Syntax Error: "conformance/jsx/tsxUnionTypeComponent2.tsx" -Expect Syntax Error: "conformance/jsx/unicodeEscapesInJsxtags.tsx" Expect Syntax Error: "conformance/override/override1.ts" Expect Syntax Error: "conformance/override/override11.ts" Expect Syntax Error: "conformance/override/override13.ts" @@ -16206,6 +16205,54 @@ Expect to Parse: "conformance/salsa/plainJSRedeclare3.ts" 42 │ } ╰──── + × Expected corresponding JSX closing tag for '\u0061'. + ╭─[conformance/jsx/unicodeEscapesInJsxtags.tsx:15:4] + 14 │ // tag name: + 15 │ ; <\u0061> + · ────── ─ + 16 │ ; <\u0061-b> + ╰──── + + × Expected corresponding JSX closing tag for '\u0061-b'. + ╭─[conformance/jsx/unicodeEscapesInJsxtags.tsx:16:4] + 15 │ ; <\u0061> + 16 │ ; <\u0061-b> + · ──────── ─── + 17 │ ; + ╰──── + + × Expected corresponding JSX closing tag for 'a-'. + ╭─[conformance/jsx/unicodeEscapesInJsxtags.tsx:17:4] + 16 │ ; <\u0061-b> + 17 │ ; + · ── ─── + 18 │ ; + ╰──── + + × Expected corresponding JSX closing tag for '\u{0061}'. + ╭─[conformance/jsx/unicodeEscapesInJsxtags.tsx:20:4] + 19 │ ; + 20 │ ; <\u{0061}> + · ──────── ─ + 21 │ ; <\u{0061}-b> + ╰──── + + × Expected corresponding JSX closing tag for '\u{0061}-b'. + ╭─[conformance/jsx/unicodeEscapesInJsxtags.tsx:21:4] + 20 │ ; <\u{0061}> + 21 │ ; <\u{0061}-b> + · ────────── ─── + 22 │ ; + ╰──── + + × Expected corresponding JSX closing tag for 'a-'. + ╭─[conformance/jsx/unicodeEscapesInJsxtags.tsx:22:4] + 21 │ ; <\u{0061}-b> + 22 │ ; + · ── ─── + 23 │ ; + ╰──── + × Expected `(` but found `Identifier` ╭─[conformance/override/overrideKeywordOrder.ts:12:18] 11 │ override async m1() {}