Skip to content

Commit da85e3c

Browse files
feat(css): add support for the typed attr CSS function (#8255)
Co-authored-by: Emanuele Stoppa <[email protected]>
1 parent a248e88 commit da85e3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+10773
-204
lines changed

.changeset/proud-llamas-jog.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": minor
3+
---
4+
5+
Added support for the typed `attr` function. Addresses issue #6183.
6+
7+
**Example**
8+
9+
``` css
10+
.btn {
11+
width: attr(data-size type(<length> | <percentage>), 0px);
12+
}
13+
```

crates/biome_css_analyze/src/lint/correctness/no_unknown_unit.rs

Lines changed: 102 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use biome_analyze::{
33
};
44
use biome_console::markup;
55
use biome_css_syntax::{
6-
AnyCssDimension, CssFunction, CssGenericProperty, CssQueryFeaturePlain, CssSyntaxKind,
6+
AnyCssAttrUnit, AnyCssDimension, CssFunction, CssGenericProperty, CssQueryFeaturePlain,
7+
CssSyntaxKind,
78
};
89
use biome_diagnostics::Severity;
9-
use biome_rowan::{SyntaxNodeCast, TextRange};
10+
use biome_rowan::{SyntaxNodeCast, TextRange, declare_node_union};
1011
use biome_rule_options::no_unknown_unit::NoUnknownUnitOptions;
1112
use biome_string_case::StrLikeExtension;
1213

@@ -71,13 +72,17 @@ declare_lint_rule! {
7172
}
7273
}
7374

75+
declare_node_union! {
76+
pub AnyCssUnit = AnyCssDimension | AnyCssAttrUnit
77+
}
78+
7479
pub struct NoUnknownUnitState {
7580
unit: String,
7681
span: TextRange,
7782
}
7883

7984
impl Rule for NoUnknownUnit {
80-
type Query = Ast<AnyCssDimension>;
85+
type Query = Ast<AnyCssUnit>;
8186
type State = NoUnknownUnitState;
8287
type Signals = Option<Self::State>;
8388
type Options = NoUnknownUnitOptions;
@@ -86,87 +91,108 @@ impl Rule for NoUnknownUnit {
8691
let node = ctx.query();
8792

8893
match node {
89-
AnyCssDimension::CssUnknownDimension(dimension) => {
90-
let unit_token = dimension.unit_token().ok()?;
91-
let unit = unit_token.text_trimmed().to_string();
92-
93-
Some(NoUnknownUnitState {
94-
unit,
95-
span: unit_token.text_trimmed_range(),
96-
})
97-
}
98-
AnyCssDimension::CssRegularDimension(dimension) => {
99-
let unit_token = dimension.unit_token().ok()?;
100-
let unit = unit_token.text_trimmed().to_string();
101-
102-
// The `x` unit is parsed as `CssRegularDimension`, but it is used for describing resolutions.
103-
// This check is to disallow the use of the `x` unit outside this specific context.
104-
if unit == "x" {
105-
let mut allow_x = false;
106-
107-
for ancestor in dimension.unit_token().ok()?.ancestors() {
108-
match ancestor.kind() {
109-
CssSyntaxKind::CSS_FUNCTION => {
110-
let function_name_token = ancestor
111-
.cast::<CssFunction>()?
112-
.name()
113-
.ok()?
114-
.value_token()
115-
.ok()?;
116-
let function_name =
117-
function_name_token.text_trimmed().to_ascii_lowercase_cow();
118-
119-
if function_name.ends_with("image-set") {
120-
allow_x = true;
121-
break;
122-
}
123-
}
124-
CssSyntaxKind::CSS_GENERIC_PROPERTY => {
125-
let property_name_token = ancestor
126-
.cast::<CssGenericProperty>()?
127-
.name()
128-
.ok()?
129-
.as_css_identifier()?
130-
.value_token()
131-
.ok()?;
132-
let property_name =
133-
property_name_token.text_trimmed().to_ascii_lowercase_cow();
134-
135-
if property_name == "image-resolution" {
136-
allow_x = true;
137-
break;
94+
AnyCssUnit::AnyCssDimension(dimension) => {
95+
match dimension {
96+
AnyCssDimension::CssUnknownDimension(dimension) => {
97+
let unit_token = dimension.unit_token().ok()?;
98+
let unit = unit_token.text_trimmed().to_string();
99+
100+
Some(NoUnknownUnitState {
101+
unit,
102+
span: unit_token.text_trimmed_range(),
103+
})
104+
}
105+
AnyCssDimension::CssRegularDimension(dimension) => {
106+
let unit_token = dimension.unit_token().ok()?;
107+
let unit = unit_token.text_trimmed().to_string();
108+
109+
// The `x` unit is parsed as `CssRegularDimension`, but it is used for describing resolutions.
110+
// This check is to disallow the use of the `x` unit outside this specific context.
111+
if unit == "x" {
112+
let mut allow_x = false;
113+
114+
for ancestor in dimension.unit_token().ok()?.ancestors() {
115+
match ancestor.kind() {
116+
CssSyntaxKind::CSS_FUNCTION => {
117+
let function_name_token = ancestor
118+
.cast::<CssFunction>()?
119+
.name()
120+
.ok()?
121+
.value_token()
122+
.ok()?;
123+
let function_name = function_name_token
124+
.text_trimmed()
125+
.to_ascii_lowercase_cow();
126+
127+
if function_name.ends_with("image-set") {
128+
allow_x = true;
129+
break;
130+
}
131+
}
132+
CssSyntaxKind::CSS_GENERIC_PROPERTY => {
133+
let property_name_token = ancestor
134+
.cast::<CssGenericProperty>()?
135+
.name()
136+
.ok()?
137+
.as_css_identifier()?
138+
.value_token()
139+
.ok()?;
140+
let property_name = property_name_token
141+
.text_trimmed()
142+
.to_ascii_lowercase_cow();
143+
144+
if property_name == "image-resolution" {
145+
allow_x = true;
146+
break;
147+
}
148+
}
149+
CssSyntaxKind::CSS_QUERY_FEATURE_PLAIN => {
150+
let feature_name_token = ancestor
151+
.cast::<CssQueryFeaturePlain>()?
152+
.name()
153+
.ok()?
154+
.value_token()
155+
.ok()?;
156+
let feature_name = feature_name_token
157+
.text_trimmed()
158+
.to_ascii_lowercase_cow();
159+
160+
if RESOLUTION_MEDIA_FEATURE_NAMES
161+
.contains(&feature_name.as_ref())
162+
{
163+
allow_x = true;
164+
break;
165+
}
166+
}
167+
_ => {}
138168
}
139169
}
140-
CssSyntaxKind::CSS_QUERY_FEATURE_PLAIN => {
141-
let feature_name_token = ancestor
142-
.cast::<CssQueryFeaturePlain>()?
143-
.name()
144-
.ok()?
145-
.value_token()
146-
.ok()?;
147-
let feature_name =
148-
feature_name_token.text_trimmed().to_ascii_lowercase_cow();
149-
150-
if RESOLUTION_MEDIA_FEATURE_NAMES.contains(&feature_name.as_ref()) {
151-
allow_x = true;
152-
break;
153-
}
170+
171+
if !allow_x {
172+
return Some(NoUnknownUnitState {
173+
unit,
174+
span: unit_token.text_trimmed_range(),
175+
});
154176
}
155-
_ => {}
156177
}
157-
}
158178

159-
if !allow_x {
160-
return Some(NoUnknownUnitState {
161-
unit,
162-
span: unit_token.text_trimmed_range(),
163-
});
179+
None
164180
}
181+
_ => None,
165182
}
166-
167-
None
168183
}
169-
_ => None,
184+
AnyCssUnit::AnyCssAttrUnit(unit) => match unit {
185+
AnyCssAttrUnit::CssUnknownAttrUnit(unit) => {
186+
let unit_token = unit.unit_token().ok()?;
187+
let unit = unit_token.text_trimmed().to_string();
188+
189+
Some(NoUnknownUnitState {
190+
unit,
191+
span: unit_token.text_trimmed_range(),
192+
})
193+
}
194+
_ => None,
195+
},
170196
}
171197
}
172198

crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ a { background: image-set('img1x.png' 1x, 'img2x.png' 2x) left 20x / 15% 60% rep
2525
a { background: /* comment */ image-set('img1x.png' 1x, 'img2x.png' 2x) left 20x / 15% 60% repeat-x; }
2626
a { background-image: image-set('img1x.png' 1pix, 'img2x.png' 2x); }
2727
@font-face { color: U+0100-024F; }
28-
a { unicode-range: U+0100-024F; }
28+
a { unicode-range: U+0100-024F; }
29+
a { --test: attr(data-test unknown); }

crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/invalid.css.snap

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
source: crates/biome_css_analyze/tests/spec_tests.rs
33
expression: invalid.css
4-
snapshot_kind: text
54
---
65
# Input
76
```css
@@ -33,6 +32,8 @@ a { background: /* comment */ image-set('img1x.png' 1x, 'img2x.png' 2x) left 20x
3332
a { background-image: image-set('img1x.png' 1pix, 'img2x.png' 2x); }
3433
@font-face { color: U+0100-024F; }
3534
a { unicode-range: U+0100-024F; }
35+
a { --test: attr(data-test unknown); }
36+
3637
```
3738

3839
# Diagnostics
@@ -656,3 +657,26 @@ invalid.css:26:46 lint/correctness/noUnknownUnit ━━━━━━━━━━
656657
657658
658659
```
660+
661+
```
662+
invalid.css:29:28 lint/correctness/noUnknownUnit ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
663+
664+
× Unexpected unknown unit: unknown
665+
666+
27 │ @font-face { color: U+0100-024F; }
667+
28 │ a { unicode-range: U+0100-024F; }
668+
> 29 │ a { --test: attr(data-test unknown); }
669+
│ ^^^^^^^
670+
30 │
671+
672+
i See MDN web docs for more details.
673+
674+
i Use a known unit instead, such as:
675+
676+
- px
677+
- em
678+
- rem
679+
- etc.
680+
681+
682+
```

crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/valid.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,9 @@ a { background-image: image-set(url('first.png') calc(1x * 1), url('second.png')
6969
@media (resolution: 2x) {}
7070
@media ( resOLution: 2x) {}
7171
a { image-resolution: 1x; }
72-
a { width: 8ic; }
72+
a { width: 8ic; }
73+
a: { --test: attr(data-test rem); }
74+
a: { --test: attr(data-test %); }
75+
a: { --test: attr(data-test type(*)); }
76+
a: { --test: attr(data-test type(length)); }
77+
a: { --test: attr(data-test type(<color>)); }

crates/biome_css_analyze/tests/specs/correctness/noUnknownUnit/valid.css.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,10 @@ a { background-image: image-set(url('first.png') calc(1x * 1), url('second.png')
7676
@media ( resOLution: 2x) {}
7777
a { image-resolution: 1x; }
7878
a { width: 8ic; }
79+
a: { --test: attr(data-test rem); }
80+
a: { --test: attr(data-test %); }
81+
a: { --test: attr(data-test type(*)); }
82+
a: { --test: attr(data-test type(length)); }
83+
a: { --test: attr(data-test type(<color>)); }
84+
7985
```

0 commit comments

Comments
 (0)