Skip to content

Commit

Permalink
feat(parser/html): emit diagnostics for parsing errors (#3846)
Browse files Browse the repository at this point in the history
  • Loading branch information
dyc3 authored Sep 10, 2024
1 parent d658225 commit 647653a
Show file tree
Hide file tree
Showing 12 changed files with 417 additions and 19 deletions.
33 changes: 14 additions & 19 deletions crates/biome_html_parser/src/syntax/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
mod parse_error;

use crate::parser::HtmlParser;
use crate::syntax::parse_error::{expected_attribute, expected_child};
use crate::syntax::parse_error::*;
use crate::token_source::HtmlLexContext;
use biome_html_syntax::HtmlSyntaxKind::{
HTML_ATTRIBUTE, HTML_ATTRIBUTE_INITIALIZER_CLAUSE, HTML_ATTRIBUTE_LIST, HTML_BOGUS_ELEMENT,
HTML_CLOSING_ELEMENT, HTML_CONTENT, HTML_DIRECTIVE, HTML_ELEMENT, HTML_ELEMENT_LIST,
HTML_LITERAL, HTML_NAME, HTML_OPENING_ELEMENT, HTML_ROOT, HTML_SELF_CLOSING_ELEMENT,
HTML_STRING, HTML_STRING_LITERAL, UNICODE_BOM,
};
use biome_html_syntax::HtmlSyntaxKind::*;
use biome_html_syntax::{HtmlSyntaxKind, T};
use biome_parser::parse_lists::ParseNodeList;
use biome_parser::parse_recovery::{ParseRecoveryTokenSet, RecoveryResult};
Expand Down Expand Up @@ -59,28 +54,29 @@ fn parse_element(p: &mut HtmlParser) -> ParsedSyntax {
let m = p.start();

p.bump(T![<]);
// TODO: handle error
parse_literal(p).ok();
parse_literal(p).or_add_diagnostic(p, expected_element_name);

AttributeList.parse_list(p);

if p.at(T![/]) {
p.bump(T![/]);
p.bump(T![>]);
p.expect(T![>]);
Present(m.complete(p, HTML_SELF_CLOSING_ELEMENT))
} else {
p.bump_with_context(T![>], HtmlLexContext::ElementList);
p.expect_with_context(T![>], HtmlLexContext::ElementList);
let opening = m.complete(p, HTML_OPENING_ELEMENT);
ElementList.parse_list(p);
// TODO: handle error
parse_closing_element(p).ok();
parse_closing_element(p).or_add_diagnostic(p, expected_closing_tag);
let previous = opening.precede(p);

Present(previous.complete(p, HTML_ELEMENT))
}
}

fn parse_closing_element(p: &mut HtmlParser) -> ParsedSyntax {
if !p.at(T![<]) || !p.nth_at(1, T![/]) {
return Absent;
}
let m = p.start();
p.bump(T![<]);
p.bump(T![/]);
Expand Down Expand Up @@ -112,7 +108,8 @@ impl ParseNodeList for ElementList {
fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool {
let at_l_angle0 = p.at(T![<]);
let at_slash1 = p.nth_at(1, T![/]);
at_l_angle0 && at_slash1
let at_eof = p.at(EOF);
at_l_angle0 && at_slash1 || at_eof
}

fn recover(
Expand Down Expand Up @@ -142,7 +139,7 @@ impl ParseNodeList for AttributeList {
}

fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool {
p.at(T![>]) || p.at(T![/])
p.at(T![>]) || p.at(T![/]) || p.at(EOF)
}

fn recover(
Expand All @@ -163,8 +160,7 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax {
return Absent;
}
let m = p.start();
// TODO: handle error
parse_literal(p).ok();
parse_literal(p).or_add_diagnostic(p, expected_attribute);
if p.at(T![=]) {
parse_attribute_initializer(p).ok();
Present(m.complete(p, HTML_ATTRIBUTE))
Expand Down Expand Up @@ -201,7 +197,6 @@ fn parse_attribute_initializer(p: &mut HtmlParser) -> ParsedSyntax {
}
let m = p.start();
p.bump(T![=]);
// TODO: handle error
parse_string_literal(p).ok();
parse_string_literal(p).or_add_diagnostic(p, expected_initializer);
Present(m.complete(p, HTML_ATTRIBUTE_INITIALIZER_CLAUSE))
}
29 changes: 29 additions & 0 deletions crates/biome_html_parser/src/syntax/parse_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,32 @@ pub(crate) fn expected_attribute(p: &HtmlParser, range: TextRange) -> ParseDiagn
pub(crate) fn expected_child(p: &HtmlParser, range: TextRange) -> ParseDiagnostic {
expect_one_of(&["element", "text"], range).into_diagnostic(p)
}

/// The parser was expecting a value for an attribute initializer clause.
///
/// ```html
/// <div id= />
/// ^ expected initializer
/// ```
pub(crate) fn expected_initializer(p: &HtmlParser, range: TextRange) -> ParseDiagnostic {
expected_node("initializer", range, p).into_diagnostic(p)
}

/// The parser encountered a tag that does not have a corresponding closing tag.
///
/// ```html
/// <div>foo
/// ```
pub(crate) fn expected_closing_tag(p: &HtmlParser, range: TextRange) -> ParseDiagnostic {
expected_node("closing tag", range, p).into_diagnostic(p)
}

/// The parser was encountered a tag that does not have a name.
///
/// ```html
/// <>
/// ^ expected element name
/// ```
pub(crate) fn expected_element_name(p: &HtmlParser, range: TextRange) -> ParseDiagnostic {
expected_node("element name", range, p).into_diagnostic(p)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class=></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
source: crates/biome_html_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```html
<div class=></div>
```
## AST
```
HtmlRoot {
bom_token: missing (optional),
directive: missing (optional),
html: HtmlElement {
opening_element: HtmlOpeningElement {
l_angle_token: L_ANGLE@0..1 "<" [] [],
name: HtmlName {
value_token: HTML_LITERAL@1..5 "div" [] [Whitespace(" ")],
},
attributes: HtmlAttributeList [
HtmlAttribute {
name: HtmlName {
value_token: HTML_LITERAL@5..10 "class" [] [],
},
initializer: HtmlAttributeInitializerClause {
eq_token: EQ@10..11 "=" [] [],
value: missing (required),
},
},
],
r_angle_token: R_ANGLE@11..12 ">" [] [],
},
children: HtmlElementList [],
closing_element: HtmlClosingElement {
l_angle_token: L_ANGLE@12..13 "<" [] [],
slash_token: SLASH@13..14 "/" [] [],
name: HtmlName {
value_token: HTML_LITERAL@14..17 "div" [] [],
},
r_angle_token: R_ANGLE@17..18 ">" [] [],
},
},
eof_token: EOF@18..19 "" [Newline("\n")] [],
}
```
## CST
```
0: [email protected]
0: (empty)
1: (empty)
2: [email protected]
0: [email protected]
0: [email protected] "<" [] []
1: [email protected]
0: [email protected] "div" [] [Whitespace(" ")]
2: [email protected]
0: [email protected]
0: [email protected]
0: [email protected] "class" [] []
1: [email protected]
0: [email protected] "=" [] []
1: (empty)
3: [email protected] ">" [] []
1: [email protected]
2: [email protected]
0: [email protected] "<" [] []
1: [email protected] "/" [] []
2: [email protected]
0: [email protected] "div" [] []
3: [email protected] ">" [] []
3: [email protected] "" [Newline("\n")] []
```
## Diagnostics
```
missing-initializer.html:1:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× Expected an initializer but instead found '>'.
> 1 │ <div class=></div>
^
2
i Expected an initializer here.
> 1 │ <div class=></div>
^
2
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div>foo
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
source: crates/biome_html_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```html
<div>foo
```
## AST
```
HtmlRoot {
bom_token: missing (optional),
directive: missing (optional),
html: HtmlElement {
opening_element: HtmlOpeningElement {
l_angle_token: L_ANGLE@0..1 "<" [] [],
name: HtmlName {
value_token: HTML_LITERAL@1..4 "div" [] [],
},
attributes: HtmlAttributeList [],
r_angle_token: R_ANGLE@4..5 ">" [] [],
},
children: HtmlElementList [
HtmlContent {
value_token: HTML_LITERAL@5..9 "foo\n" [] [],
},
],
closing_element: missing (required),
},
eof_token: EOF@9..9 "" [] [],
}
```
## CST
```
0: [email protected]
0: (empty)
1: (empty)
2: [email protected]
0: [email protected]
0: [email protected] "<" [] []
1: [email protected]
0: [email protected] "div" [] []
2: [email protected]
3: [email protected] ">" [] []
1: [email protected]
0: [email protected]
0: [email protected] "foo\n" [] []
2: (empty)
3: [email protected] "" [] []
```
## Diagnostics
```
missing-close-tag.html:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× Expected a closing tag but instead found the end of the file.
1 │ <div>foo
> 2 │
i Expected a closing tag here.
1 │ <div>foo
> 2 │
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<></>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
source: crates/biome_html_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```html
<></>
```


## AST

```
HtmlRoot {
bom_token: missing (optional),
directive: missing (optional),
html: HtmlElement {
opening_element: HtmlOpeningElement {
l_angle_token: L_ANGLE@0..1 "<" [] [],
name: missing (required),
attributes: HtmlAttributeList [],
r_angle_token: R_ANGLE@1..2 ">" [] [],
},
children: HtmlElementList [],
closing_element: HtmlClosingElement {
l_angle_token: L_ANGLE@2..3 "<" [] [],
slash_token: SLASH@3..4 "/" [] [],
name: missing (required),
r_angle_token: R_ANGLE@4..5 ">" [] [],
},
},
eof_token: EOF@5..6 "" [Newline("\n")] [],
}
```

## CST

```
0: [email protected]
0: (empty)
1: (empty)
2: [email protected]
0: [email protected]
0: [email protected] "<" [] []
1: (empty)
2: [email protected]
3: [email protected] ">" [] []
1: [email protected]
2: [email protected]
0: [email protected] "<" [] []
1: [email protected] "/" [] []
2: (empty)
3: [email protected] ">" [] []
3: [email protected] "" [Newline("\n")] []
```

## Diagnostics

```
missing-element-name.html:1:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
× Expected an element name but instead found '>'.
> 1 │ <></>
│ ^
2 │
i Expected an element name here.
> 1 │ <></>
│ ^
2 │
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div
class="foo"
role="button"
>
foo
</div>
Loading

0 comments on commit 647653a

Please sign in to comment.