From 0a105b9903d8b74d51c7f84738a29a01ea0f09b7 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 8 Feb 2025 14:48:14 -0500 Subject: [PATCH] fix(parse/html): handle unclosed elements more gracefully --- .../src/generated/node_factory.rs | 38 ++++++++++++++----- .../src/html/auxiliary/element.rs | 31 +++++++++------ .../element/missing-close-tag-2.html.snap | 2 +- .../error/element/missing-close-tag.html.snap | 2 +- .../error/element/solo-no-tag-name.html.snap | 2 +- .../biome_html_syntax/src/generated/nodes.rs | 8 ++-- .../src/generated/nodes_mut.rs | 10 ++--- xtask/codegen/html.ungram | 2 +- 8 files changed, 60 insertions(+), 35 deletions(-) diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index 01e871947e37..578134082a04 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -159,16 +159,34 @@ impl HtmlDirectiveBuilder { pub fn html_element( opening_element: HtmlOpeningElement, children: HtmlElementList, - closing_element: HtmlClosingElement, -) -> HtmlElement { - HtmlElement::unwrap_cast(SyntaxNode::new_detached( - HtmlSyntaxKind::HTML_ELEMENT, - [ - Some(SyntaxElement::Node(opening_element.into_syntax())), - Some(SyntaxElement::Node(children.into_syntax())), - Some(SyntaxElement::Node(closing_element.into_syntax())), - ], - )) +) -> HtmlElementBuilder { + HtmlElementBuilder { + opening_element, + children, + closing_element: None, + } +} +pub struct HtmlElementBuilder { + opening_element: HtmlOpeningElement, + children: HtmlElementList, + closing_element: Option, +} +impl HtmlElementBuilder { + pub fn with_closing_element(mut self, closing_element: HtmlClosingElement) -> Self { + self.closing_element = Some(closing_element); + self + } + pub fn build(self) -> HtmlElement { + HtmlElement::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::HTML_ELEMENT, + [ + Some(SyntaxElement::Node(self.opening_element.into_syntax())), + Some(SyntaxElement::Node(self.children.into_syntax())), + self.closing_element + .map(|token| SyntaxElement::Node(token.into_syntax())), + ], + )) + } } pub fn html_name(value_token: SyntaxToken) -> HtmlName { HtmlName::unwrap_cast(SyntaxNode::new_detached( diff --git a/crates/biome_html_formatter/src/html/auxiliary/element.rs b/crates/biome_html_formatter/src/html/auxiliary/element.rs index ab15141c423d..5b47099a7d73 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/element.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/element.rs @@ -28,7 +28,6 @@ impl FormatNodeRule for FormatHtmlElement { closing_element, } = node.as_fields(); - let closing_element = closing_element?; let opening_element = opening_element?; let tag_name = opening_element.name()?; let tag_name = tag_name @@ -62,9 +61,13 @@ impl FormatNodeRule for FormatHtmlElement { .last_token() .is_some_and(|tok| tok.has_trailing_whitespace()) || closing_element - .l_angle_token() - .ok() - .is_some_and(|tok| tok.has_leading_whitespace_or_newline()); + .as_ref() + .map(|e| { + e.l_angle_token() + .ok() + .is_some_and(|tok| tok.has_leading_whitespace_or_newline()) + }) + .unwrap_or_default(); // "Borrowing" in this context refers to tokens in nodes that would normally be // formatted by that node's formatter, but are instead formatted by a sibling @@ -98,7 +101,7 @@ impl FormatNodeRule for FormatHtmlElement { None }; let borrowed_closing_tag = if should_borrow_closing_tag { - Some(closing_element.clone()) + closing_element.clone() } else { None }; @@ -147,13 +150,17 @@ impl FormatNodeRule for FormatHtmlElement { } } } - FormatNodeRule::fmt( - &FormatHtmlClosingElement::default().with_options(FormatHtmlClosingElementOptions { - tag_borrowed: should_borrow_closing_tag, - }), - &closing_element, - f, - )?; + if let Some(closing_element) = closing_element { + FormatNodeRule::fmt( + &FormatHtmlClosingElement::default().with_options( + FormatHtmlClosingElementOptions { + tag_borrowed: should_borrow_closing_tag, + }, + ), + &closing_element, + f, + )?; + } Ok(()) } diff --git a/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag-2.html.snap b/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag-2.html.snap index 1eecf30e88bd..4f40b897f617 100644 --- a/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag-2.html.snap +++ b/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag-2.html.snap @@ -27,7 +27,7 @@ HtmlRoot { r_angle_token: R_ANGLE@4..5 ">" [] [], }, children: HtmlElementList [], - closing_element: missing (required), + closing_element: missing (optional), }, ], eof_token: EOF@5..6 "" [Newline("\n")] [], diff --git a/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag.html.snap b/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag.html.snap index 59f2b4812b3e..c35621a3b8ed 100644 --- a/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag.html.snap +++ b/crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag.html.snap @@ -31,7 +31,7 @@ HtmlRoot { value_token: HTML_LITERAL@5..8 "foo" [] [], }, ], - closing_element: missing (required), + closing_element: missing (optional), }, ], eof_token: EOF@8..9 "" [Newline("\n")] [], diff --git a/crates/biome_html_parser/tests/html_specs/error/element/solo-no-tag-name.html.snap b/crates/biome_html_parser/tests/html_specs/error/element/solo-no-tag-name.html.snap index bec8b50fb679..9677a4381404 100644 --- a/crates/biome_html_parser/tests/html_specs/error/element/solo-no-tag-name.html.snap +++ b/crates/biome_html_parser/tests/html_specs/error/element/solo-no-tag-name.html.snap @@ -25,7 +25,7 @@ HtmlRoot { r_angle_token: missing (required), }, children: HtmlElementList [], - closing_element: missing (required), + closing_element: missing (optional), }, ], eof_token: EOF@1..2 "" [Newline("\n")] [], diff --git a/crates/biome_html_syntax/src/generated/nodes.rs b/crates/biome_html_syntax/src/generated/nodes.rs index b394db8b0259..e7250b11b3e9 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -371,8 +371,8 @@ impl HtmlElement { pub fn children(&self) -> HtmlElementList { support::list(&self.syntax, 1usize) } - pub fn closing_element(&self) -> SyntaxResult { - support::required_node(&self.syntax, 2usize) + pub fn closing_element(&self) -> Option { + support::node(&self.syntax, 2usize) } } impl Serialize for HtmlElement { @@ -387,7 +387,7 @@ impl Serialize for HtmlElement { pub struct HtmlElementFields { pub opening_element: SyntaxResult, pub children: HtmlElementList, - pub closing_element: SyntaxResult, + pub closing_element: Option, } #[derive(Clone, PartialEq, Eq, Hash)] pub struct HtmlName { @@ -1114,7 +1114,7 @@ impl std::fmt::Debug for HtmlElement { .field("children", &self.children()) .field( "closing_element", - &support::DebugSyntaxResult(self.closing_element()), + &support::DebugOptionalElement(self.closing_element()), ) .finish() } else { diff --git a/crates/biome_html_syntax/src/generated/nodes_mut.rs b/crates/biome_html_syntax/src/generated/nodes_mut.rs index 5b6db033d550..066e1a5362fc 100644 --- a/crates/biome_html_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_html_syntax/src/generated/nodes_mut.rs @@ -168,11 +168,11 @@ impl HtmlElement { .splice_slots(1usize..=1usize, once(Some(element.into_syntax().into()))), ) } - pub fn with_closing_element(self, element: HtmlClosingElement) -> Self { - Self::unwrap_cast( - self.syntax - .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), - ) + pub fn with_closing_element(self, element: Option) -> Self { + Self::unwrap_cast(self.syntax.splice_slots( + 2usize..=2usize, + once(element.map(|element| element.into_syntax().into())), + )) } } impl HtmlName { diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 4dc985b1e173..035abe4e7cfc 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -84,7 +84,7 @@ HtmlSelfClosingElement = HtmlElement = opening_element: HtmlOpeningElement children: HtmlElementList - closing_element: HtmlClosingElement + closing_element: HtmlClosingElement? //