Skip to content

Commit 2a190e0

Browse files
authored
fix(parse/css): improve parsing of @utility names (#8850)
1 parent 85f81f9 commit 2a190e0

6 files changed

Lines changed: 178 additions & 6 deletions

File tree

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 [#8708](https://github.com/biomejs/biome/issues/8708): Tailwind `@utility` directives now parse functional utility names like `px-*` when Tailwind directives are enabled.

crates/biome_css_formatter/tests/specs/css/tailwind/utility.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@
1515

1616
@utility tab-4{tab-size:4;}
1717

18-
@utility tab-*{tab-size:--value(--tab-size-*);}
18+
@utility tab-*{tab-size:--value(--tab-size-*);}
19+
20+
@utility px-*{padding-inline:--value(--padding-inline-*);}

crates/biome_css_formatter/tests/specs/css/tailwind/utility.css.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ info: css/tailwind/utility.css
2323
@utility tab-4{tab-size:4;}
2424
2525
@utility tab-*{tab-size:--value(--tab-size-*);}
26+
27+
@utility px-*{padding-inline:--value(--padding-inline-*);}
28+
2629
```
2730

2831

@@ -66,6 +69,10 @@ Quote style: Double Quotes
6669
@utility tab-* {
6770
tab-size: --value(--tab-size-*);
6871
}
72+
73+
@utility px-* {
74+
padding-inline: --value(--padding-inline-*);
75+
}
6976
```
7077

7178
## Output 1
@@ -104,4 +111,8 @@ Quote style: Double Quotes
104111
@utility tab-* {
105112
tab-size: --value(--tab-size-*);
106113
}
114+
115+
@utility px-* {
116+
padding-inline: --value(--padding-inline-*);
117+
}
107118
```

crates/biome_css_parser/src/syntax/at_rule/tailwind.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,18 @@ pub(crate) fn parse_utility_at_rule(p: &mut CssParser) -> ParsedSyntax {
4141
p.bump(T![utility]);
4242

4343
// Parse utility name - can be simple or functional
44-
if !is_at_identifier(p) {
44+
if !is_at_utility_identifier(p) {
4545
p.error(expected_identifier(p, p.cur_range()));
4646
return Present(m.complete(p, CSS_BOGUS_AT_RULE));
4747
}
4848

49-
parse_utility_name(p).ok();
49+
parse_utility_name(p)
50+
.or_recover_with_token_set(
51+
p,
52+
&ParseRecoveryTokenSet::new(CSS_BOGUS_CUSTOM_IDENTIFIER, token_set![T!['{']]),
53+
expected_identifier,
54+
)
55+
.ok();
5056

5157
parse_declaration_or_rule_list_block(p);
5258

@@ -55,21 +61,41 @@ pub(crate) fn parse_utility_at_rule(p: &mut CssParser) -> ParsedSyntax {
5561

5662
fn parse_utility_name(p: &mut CssParser) -> ParsedSyntax {
5763
// Check if this is a functional utility (ends with -*)
58-
if p.at(T![ident]) && p.nth_at(1, T![-]) && p.nth_at(2, T![*]) {
64+
if is_at_functional_utility_name(p) {
5965
// Functional utility: tab-*
6066
let m = p.start();
6167

62-
parse_regular_identifier(p).ok();
68+
parse_utility_identifier(p).ok();
6369
p.expect(T![-]);
6470
p.expect(T![*]);
6571

6672
Present(m.complete(p, TW_FUNCTIONAL_UTILITY_NAME))
6773
} else {
6874
// Simple utility: center-flex
69-
parse_regular_identifier(p)
75+
parse_utility_identifier(p)
7076
}
7177
}
7278

79+
fn parse_utility_identifier(p: &mut CssParser) -> ParsedSyntax {
80+
if is_at_identifier(p) {
81+
parse_identifier(p, CssLexContext::Regular)
82+
} else if p.cur().is_known_dimension_unit() {
83+
let m = p.start();
84+
p.bump_remap(T![ident]);
85+
Present(m.complete(p, CSS_IDENTIFIER))
86+
} else {
87+
Absent
88+
}
89+
}
90+
91+
fn is_at_utility_identifier(p: &mut CssParser) -> bool {
92+
is_at_identifier(p) || p.cur().is_known_dimension_unit()
93+
}
94+
95+
fn is_at_functional_utility_name(p: &mut CssParser) -> bool {
96+
is_at_utility_identifier(p) && p.nth_at(1, T![-]) && p.nth_at(2, T![*])
97+
}
98+
7399
// @variant dark { background: black; }
74100
pub(crate) fn parse_variant_at_rule(p: &mut CssParser) -> ParsedSyntax {
75101
if !p.at(T![variant]) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@utility px-* {
2+
padding-inline: --value(--padding-inline-*);
3+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
source: crates/biome_css_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
## Input
6+
7+
```css
8+
@utility px-* {
9+
padding-inline: --value(--padding-inline-*);
10+
}
11+
12+
```
13+
14+
15+
## AST
16+
17+
```
18+
CssRoot {
19+
bom_token: missing (optional),
20+
rules: CssRuleList [
21+
CssAtRule {
22+
at_token: AT@0..1 "@" [] [],
23+
rule: TwUtilityAtRule {
24+
utility_token: UTILITY_KW@1..9 "utility" [] [Whitespace(" ")],
25+
name: TwFunctionalUtilityName {
26+
identifier: CssIdentifier {
27+
value_token: IDENT@9..11 "px" [] [],
28+
},
29+
minus_token: MINUS@11..12 "-" [] [],
30+
star_token: STAR@12..14 "*" [] [Whitespace(" ")],
31+
},
32+
block: CssDeclarationOrRuleBlock {
33+
l_curly_token: L_CURLY@14..15 "{" [] [],
34+
items: CssDeclarationOrRuleList [
35+
CssDeclarationWithSemicolon {
36+
declaration: CssDeclaration {
37+
property: CssGenericProperty {
38+
name: CssIdentifier {
39+
value_token: IDENT@15..31 "padding-inline" [Newline("\n"), Whitespace("\t")] [],
40+
},
41+
colon_token: COLON@31..33 ":" [] [Whitespace(" ")],
42+
value: CssGenericComponentValueList [
43+
CssFunction {
44+
name: CssIdentifier {
45+
value_token: IDENT@33..40 "--value" [] [],
46+
},
47+
l_paren_token: L_PAREN@40..41 "(" [] [],
48+
items: CssParameterList [
49+
CssParameter {
50+
any_css_expression: CssListOfComponentValuesExpression {
51+
css_component_value_list: CssComponentValueList [
52+
TwValueThemeReference {
53+
reference: CssDashedIdentifier {
54+
value_token: IDENT@41..57 "--padding-inline" [] [],
55+
},
56+
minus_token: MINUS@57..58 "-" [] [],
57+
star_token: STAR@58..59 "*" [] [],
58+
},
59+
],
60+
},
61+
},
62+
],
63+
r_paren_token: R_PAREN@59..60 ")" [] [],
64+
},
65+
],
66+
},
67+
important: missing (optional),
68+
},
69+
semicolon_token: SEMICOLON@60..61 ";" [] [],
70+
},
71+
],
72+
r_curly_token: R_CURLY@61..63 "}" [Newline("\n")] [],
73+
},
74+
},
75+
},
76+
],
77+
eof_token: EOF@63..64 "" [Newline("\n")] [],
78+
}
79+
```
80+
81+
## CST
82+
83+
```
84+
85+
0: (empty)
86+
87+
88+
0: [email protected] "@" [] []
89+
90+
0: [email protected] "utility" [] [Whitespace(" ")]
91+
92+
93+
0: [email protected] "px" [] []
94+
1: [email protected] "-" [] []
95+
2: [email protected] "*" [] [Whitespace(" ")]
96+
97+
0: [email protected] "{" [] []
98+
1: CSS_DECLARATION_OR_RULE_LIST@15..61
99+
0: CSS_DECLARATION_WITH_SEMICOLON@15..61
100+
0: CSS_DECLARATION@15..60
101+
0: CSS_GENERIC_PROPERTY@15..60
102+
0: CSS_IDENTIFIER@15..31
103+
0: IDENT@15..31 "padding-inline" [Newline("\n"), Whitespace("\t")] []
104+
1: COLON@31..33 ":" [] [Whitespace(" ")]
105+
2: CSS_GENERIC_COMPONENT_VALUE_LIST@33..60
106+
0: CSS_FUNCTION@33..60
107+
0: CSS_IDENTIFIER@33..40
108+
0: IDENT@33..40 "--value" [] []
109+
1: L_PAREN@40..41 "(" [] []
110+
2: CSS_PARAMETER_LIST@41..59
111+
0: CSS_PARAMETER@41..59
112+
0: CSS_LIST_OF_COMPONENT_VALUES_EXPRESSION@41..59
113+
0: CSS_COMPONENT_VALUE_LIST@41..59
114+
0: TW_VALUE_THEME_REFERENCE@41..59
115+
0: CSS_DASHED_IDENTIFIER@41..57
116+
0: IDENT@41..57 "--padding-inline" [] []
117+
1: MINUS@57..58 "-" [] []
118+
2: STAR@58..59 "*" [] []
119+
3: R_PAREN@59..60 ")" [] []
120+
1: (empty)
121+
1: SEMICOLON@60..61 ";" [] []
122+
2: R_CURLY@61..63 "}" [Newline("\n")] []
123+
2: EOF@63..64 "" [Newline("\n")] []
124+
125+
```

0 commit comments

Comments
 (0)