Skip to content

Commit 9694e37

Browse files
authored
fix(parse/html/vue): emit diagnostics for invalid vue shorthand syntaxes (#8242)
1 parent 59b2f9a commit 9694e37

6 files changed

Lines changed: 228 additions & 0 deletions

File tree

.changeset/loose-bikes-attend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed bugs in the HTML parser so that it will flag invalid shorthand syntaxes instead of silently accepting them. For example, `<Foo : foo="5" />` is now invalid because there is a space after the `:`.

crates/biome_html_parser/src/syntax/vue.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ pub(crate) fn parse_vue_v_on_shorthand_directive(p: &mut HtmlParser) -> ParsedSy
7070

7171
let m = p.start();
7272

73+
let pos = p.source().position();
7374
p.bump_with_context(T![@], HtmlLexContext::InsideTagVue);
75+
// is there any trivia after the @ and before argument?
76+
if let Some(last_trivia) = p.source().trivia_list.last()
77+
&& pos < last_trivia.text_range().start()
78+
{
79+
// `@ click="foo"` is not valid syntax
80+
// but we want to recover gracefully
81+
p.error(expected_vue_directive_argument(p, last_trivia.text_range()));
82+
return Present(m.complete(p, VUE_BOGUS_DIRECTIVE));
83+
}
7484
parse_vue_dynamic_argument(p)
7585
.or_else(|| parse_vue_static_argument(p))
7686
.ok();
@@ -118,7 +128,17 @@ fn parse_vue_directive_argument(p: &mut HtmlParser) -> ParsedSyntax {
118128

119129
let m = p.start();
120130

131+
let pos = p.source().position();
121132
p.bump_with_context(T![:], HtmlLexContext::InsideTagVue);
133+
// is there any trivia after the colon and before argument?
134+
if let Some(last_trivia) = p.source().trivia_list.last()
135+
&& pos < last_trivia.text_range().start()
136+
{
137+
// `: foo="5"` is not valid syntax
138+
// but we want to recover gracefully
139+
p.error(expected_vue_directive_argument(p, last_trivia.text_range()));
140+
return Present(m.complete(p, VUE_BOGUS_DIRECTIVE));
141+
}
122142
parse_vue_dynamic_argument(p)
123143
.or_else(|| parse_vue_static_argument(p))
124144
.ok();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Foo : foo="5" />
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
source: crates/biome_html_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
## Input
6+
7+
```vue
8+
<Foo : foo="5" />
9+
10+
```
11+
12+
13+
## AST
14+
15+
```
16+
HtmlRoot {
17+
bom_token: missing (optional),
18+
frontmatter: missing (optional),
19+
directive: missing (optional),
20+
html: HtmlElementList [
21+
HtmlSelfClosingElement {
22+
l_angle_token: L_ANGLE@0..1 "<" [] [],
23+
name: HtmlTagName {
24+
value_token: HTML_LITERAL@1..5 "Foo" [] [Whitespace(" ")],
25+
},
26+
attributes: HtmlAttributeList [
27+
HtmlBogusAttribute {
28+
items: [
29+
VueBogusDirective {
30+
items: [
31+
COLON@5..7 ":" [] [Whitespace(" ")],
32+
],
33+
},
34+
VueModifierList [],
35+
],
36+
},
37+
HtmlAttribute {
38+
name: HtmlAttributeName {
39+
value_token: HTML_LITERAL@7..10 "foo" [] [],
40+
},
41+
initializer: HtmlAttributeInitializerClause {
42+
eq_token: EQ@10..11 "=" [] [],
43+
value: HtmlString {
44+
value_token: HTML_STRING_LITERAL@11..15 "\"5\"" [] [Whitespace(" ")],
45+
},
46+
},
47+
},
48+
],
49+
slash_token: SLASH@15..16 "/" [] [],
50+
r_angle_token: R_ANGLE@16..17 ">" [] [],
51+
},
52+
],
53+
eof_token: EOF@17..18 "" [Newline("\n")] [],
54+
}
55+
```
56+
57+
## CST
58+
59+
```
60+
61+
0: (empty)
62+
1: (empty)
63+
2: (empty)
64+
65+
66+
0: [email protected] "<" [] []
67+
68+
0: [email protected] "Foo" [] [Whitespace(" ")]
69+
70+
71+
72+
0: [email protected] ":" [] [Whitespace(" ")]
73+
74+
75+
76+
0: [email protected] "foo" [] []
77+
78+
0: [email protected] "=" [] []
79+
80+
0: [email protected] "\"5\"" [] [Whitespace(" ")]
81+
3: [email protected] "/" [] []
82+
4: [email protected] ">" [] []
83+
4: [email protected] "" [Newline("\n")] []
84+
85+
```
86+
87+
## Diagnostics
88+
89+
```
90+
invalid-v-bind-shorthand.vue:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
91+
92+
× Expected a vue directive argument but instead found ' '.
93+
94+
> 1 │ <Foo : foo="5" />
95+
│ ^
96+
2 │
97+
98+
i Expected a vue directive argument here.
99+
100+
> 1 │ <Foo : foo="5" />
101+
│ ^
102+
2 │
103+
104+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Foo @ click="handleClick" />
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
source: crates/biome_html_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
## Input
6+
7+
```vue
8+
<Foo @ click="handleClick" />
9+
10+
```
11+
12+
13+
## AST
14+
15+
```
16+
HtmlRoot {
17+
bom_token: missing (optional),
18+
frontmatter: missing (optional),
19+
directive: missing (optional),
20+
html: HtmlElementList [
21+
HtmlSelfClosingElement {
22+
l_angle_token: L_ANGLE@0..1 "<" [] [],
23+
name: HtmlTagName {
24+
value_token: HTML_LITERAL@1..5 "Foo" [] [Whitespace(" ")],
25+
},
26+
attributes: HtmlAttributeList [
27+
VueBogusDirective {
28+
items: [
29+
AT@5..7 "@" [] [Whitespace(" ")],
30+
],
31+
},
32+
HtmlAttribute {
33+
name: HtmlAttributeName {
34+
value_token: HTML_LITERAL@7..12 "click" [] [],
35+
},
36+
initializer: HtmlAttributeInitializerClause {
37+
eq_token: EQ@12..13 "=" [] [],
38+
value: HtmlString {
39+
value_token: HTML_STRING_LITERAL@13..27 "\"handleClick\"" [] [Whitespace(" ")],
40+
},
41+
},
42+
},
43+
],
44+
slash_token: SLASH@27..28 "/" [] [],
45+
r_angle_token: R_ANGLE@28..29 ">" [] [],
46+
},
47+
],
48+
eof_token: EOF@29..30 "" [Newline("\n")] [],
49+
}
50+
```
51+
52+
## CST
53+
54+
```
55+
56+
0: (empty)
57+
1: (empty)
58+
2: (empty)
59+
60+
61+
0: [email protected] "<" [] []
62+
63+
0: [email protected] "Foo" [] [Whitespace(" ")]
64+
65+
66+
0: [email protected] "@" [] [Whitespace(" ")]
67+
68+
69+
0: [email protected] "click" [] []
70+
71+
0: [email protected] "=" [] []
72+
73+
0: [email protected] "\"handleClick\"" [] [Whitespace(" ")]
74+
3: [email protected] "/" [] []
75+
4: [email protected] ">" [] []
76+
4: [email protected] "" [Newline("\n")] []
77+
78+
```
79+
80+
## Diagnostics
81+
82+
```
83+
invalid-v-on-shorthand.vue:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84+
85+
× Expected a vue directive argument but instead found ' '.
86+
87+
> 1 │ <Foo @ click="handleClick" />
88+
│ ^
89+
2 │
90+
91+
i Expected a vue directive argument here.
92+
93+
> 1 │ <Foo @ click="handleClick" />
94+
│ ^
95+
2 │
96+
97+
```

0 commit comments

Comments
 (0)