Skip to content

Commit 6ba4157

Browse files
authored
fix(parse/html/vue): reject whitespace before vue directive arguments (#8178)
1 parent 0c8349e commit 6ba4157

File tree

6 files changed

+209
-0
lines changed

6 files changed

+209
-0
lines changed

.changeset/dry-cooks-start.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#8174](https://github.com/biomejs/biome/issues/8174), where the HTML parser would parse 2 directives as a single directive because it would not reject whitespace in Vue directives. This would cause the formatter to erroneously merge the 2 directives into one, resulting in broken code.
6+
7+
```diff
8+
- <Component v-else:property="123" />
9+
+ <Component v-else :property="123" />
10+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<Component v-if="operation" :property="123" />
2+
<Component v-else :property="123" />
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
source: crates/biome_formatter_test/src/snapshot_builder.rs
3+
info: vue/issue-8174.vue
4+
---
5+
# Input
6+
7+
```vue
8+
<Component v-if="operation" :property="123" />
9+
<Component v-else :property="123" />
10+
11+
```
12+
13+
14+
=============================
15+
16+
# Outputs
17+
18+
## Output 1
19+
20+
-----
21+
Indent style: Tab
22+
Indent width: 2
23+
Line ending: LF
24+
Line width: 80
25+
Attribute Position: Auto
26+
Bracket same line: false
27+
Whitespace sensitivity: css
28+
Indent script and style: false
29+
Self close void elements: never
30+
-----
31+
32+
```vue
33+
<Component v-if="operation" :property="123"/>
34+
<Component v-else :property="123"/>
35+
```

crates/biome_html_parser/src/syntax/vue.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,18 @@ pub(crate) fn parse_vue_directive(p: &mut HtmlParser) -> ParsedSyntax {
1919

2020
let m = p.start();
2121

22+
let pos = p.source().position();
2223
// FIXME: Ideally, the lexer would just lex VUE_IDENT directly
2324
p.bump_remap_with_context(VUE_IDENT, HtmlLexContext::InsideTagVue);
2425
if p.at(T![:]) {
26+
// is there any trivia after the directive name and before the colon?
27+
if let Some(last_trivia) = p.source().trivia_list.last()
28+
&& pos < last_trivia.text_range().start()
29+
{
30+
// `v-else :foo="5"` is 2 directives, not `v-else:foo="5"`
31+
p.start().complete(p, VUE_MODIFIER_LIST);
32+
return Present(m.complete(p, VUE_DIRECTIVE));
33+
}
2534
parse_vue_directive_argument(p).ok();
2635
}
2736
VueModifierList.parse_list(p);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<Component v-if="operation" :property="123" />
2+
<Component v-else :property="123" />
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
source: crates/biome_html_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
## Input
6+
7+
```vue
8+
<Component v-if="operation" :property="123" />
9+
<Component v-else :property="123" />
10+
11+
```
12+
13+
14+
## AST
15+
16+
```
17+
HtmlRoot {
18+
bom_token: missing (optional),
19+
frontmatter: missing (optional),
20+
directive: missing (optional),
21+
html: HtmlElementList [
22+
HtmlSelfClosingElement {
23+
l_angle_token: L_ANGLE@0..1 "<" [] [],
24+
name: HtmlTagName {
25+
value_token: HTML_LITERAL@1..11 "Component" [] [Whitespace(" ")],
26+
},
27+
attributes: HtmlAttributeList [
28+
VueDirective {
29+
name_token: VUE_IDENT@11..15 "v-if" [] [],
30+
arg: missing (optional),
31+
modifiers: VueModifierList [],
32+
initializer: HtmlAttributeInitializerClause {
33+
eq_token: EQ@15..16 "=" [] [],
34+
value: HtmlString {
35+
value_token: HTML_STRING_LITERAL@16..28 "\"operation\"" [] [Whitespace(" ")],
36+
},
37+
},
38+
},
39+
VueVBindShorthandDirective {
40+
arg: VueDirectiveArgument {
41+
colon_token: COLON@28..29 ":" [] [],
42+
arg: VueStaticArgument {
43+
name_token: HTML_LITERAL@29..37 "property" [] [],
44+
},
45+
},
46+
modifiers: VueModifierList [],
47+
initializer: HtmlAttributeInitializerClause {
48+
eq_token: EQ@37..38 "=" [] [],
49+
value: HtmlString {
50+
value_token: HTML_STRING_LITERAL@38..44 "\"123\"" [] [Whitespace(" ")],
51+
},
52+
},
53+
},
54+
],
55+
slash_token: SLASH@44..45 "/" [] [],
56+
r_angle_token: R_ANGLE@45..46 ">" [] [],
57+
},
58+
HtmlSelfClosingElement {
59+
l_angle_token: L_ANGLE@46..48 "<" [Newline("\n")] [],
60+
name: HtmlTagName {
61+
value_token: HTML_LITERAL@48..58 "Component" [] [Whitespace(" ")],
62+
},
63+
attributes: HtmlAttributeList [
64+
VueDirective {
65+
name_token: VUE_IDENT@58..65 "v-else" [] [Whitespace(" ")],
66+
arg: missing (optional),
67+
modifiers: VueModifierList [],
68+
initializer: missing (optional),
69+
},
70+
VueVBindShorthandDirective {
71+
arg: VueDirectiveArgument {
72+
colon_token: COLON@65..66 ":" [] [],
73+
arg: VueStaticArgument {
74+
name_token: HTML_LITERAL@66..74 "property" [] [],
75+
},
76+
},
77+
modifiers: VueModifierList [],
78+
initializer: HtmlAttributeInitializerClause {
79+
eq_token: EQ@74..75 "=" [] [],
80+
value: HtmlString {
81+
value_token: HTML_STRING_LITERAL@75..81 "\"123\"" [] [Whitespace(" ")],
82+
},
83+
},
84+
},
85+
],
86+
slash_token: SLASH@81..82 "/" [] [],
87+
r_angle_token: R_ANGLE@82..83 ">" [] [],
88+
},
89+
],
90+
eof_token: EOF@83..84 "" [Newline("\n")] [],
91+
}
92+
```
93+
94+
## CST
95+
96+
```
97+
98+
0: (empty)
99+
1: (empty)
100+
2: (empty)
101+
102+
103+
0: [email protected] "<" [] []
104+
105+
0: [email protected] "Component" [] [Whitespace(" ")]
106+
107+
108+
0: [email protected] "v-if" [] []
109+
1: (empty)
110+
111+
112+
0: [email protected] "=" [] []
113+
114+
0: [email protected] "\"operation\"" [] [Whitespace(" ")]
115+
116+
117+
0: [email protected] ":" [] []
118+
119+
0: [email protected] "property" [] []
120+
121+
122+
0: [email protected] "=" [] []
123+
124+
0: [email protected] "\"123\"" [] [Whitespace(" ")]
125+
3: [email protected] "/" [] []
126+
4: [email protected] ">" [] []
127+
128+
0: [email protected] "<" [Newline("\n")] []
129+
130+
0: [email protected] "Component" [] [Whitespace(" ")]
131+
132+
133+
0: [email protected] "v-else" [] [Whitespace(" ")]
134+
1: (empty)
135+
136+
3: (empty)
137+
138+
139+
0: [email protected] ":" [] []
140+
141+
0: [email protected] "property" [] []
142+
143+
144+
0: [email protected] "=" [] []
145+
146+
0: [email protected] "\"123\"" [] [Whitespace(" ")]
147+
3: [email protected] "/" [] []
148+
4: [email protected] ">" [] []
149+
4: [email protected] "" [Newline("\n")] []
150+
151+
```

0 commit comments

Comments
 (0)