-
-
Notifications
You must be signed in to change notification settings - Fork 506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(biome_css_analyzer): noUnknownUnit #2535
Changes from 8 commits
1683c6a
b6a5279
7eebe23
4b420a9
5b437d8
bfee475
306e4a4
9cb08e4
f8af9e9
ba1aef2
0953ba7
1adcd00
4990cb9
c52e807
652ca86
a47ad86
073f1b7
18cb89b
1e89e19
a801b5d
ac189f9
c21d091
4ed73c5
8685fdf
da1338a
1f077ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; | ||
use biome_console::markup; | ||
use biome_css_syntax::{ | ||
AnyCssDimension, CssFunction, CssGenericProperty, CssQueryFeaturePlain, CssSyntaxKind, | ||
}; | ||
use biome_rowan::{SyntaxNodeCast, TextRange}; | ||
|
||
use crate::utils::strip_vendor_prefix; | ||
|
||
const RESOLUTION_MEDIA_FEATURE_NAMES: [&str; 3] = | ||
["resolution", "min-resolution", "max-resolution"]; | ||
|
||
fn is_css_hack_unit(value: &str) -> bool { | ||
value == "\\0" | ||
} | ||
|
||
declare_rule! { | ||
/// Disallow unknown units. | ||
/// | ||
/// This rule considers units defined in the CSS Specifications, up to and including Editor's Drafts, to be known. | ||
ematipico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```css,expect_diagnostic | ||
/// a { | ||
/// width: 10pixels; | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```css,expect_diagnostic | ||
/// a { | ||
/// width: calc(10px + 10pixels); | ||
/// } | ||
/// ``` | ||
/// | ||
/// ### Valid | ||
/// | ||
/// ```css | ||
/// a { | ||
/// width: 10px; | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```css | ||
/// a { | ||
/// width: 10Px; | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```css | ||
/// a { | ||
/// width: 10pX; | ||
/// } | ||
/// ``` | ||
/// | ||
/// ```css | ||
/// a { | ||
/// width: calc(10px + 10px); | ||
/// } | ||
/// ``` | ||
/// | ||
pub NoUnknownUnit { | ||
version: "next", | ||
name: "noUnknownUnit", | ||
recommended: false, | ||
sources: &[RuleSource::Stylelint("unit-no-unknown")], | ||
} | ||
} | ||
|
||
pub struct RuleState { | ||
value: String, | ||
span: TextRange, | ||
} | ||
|
||
impl Rule for NoUnknownUnit { | ||
type Query = Ast<AnyCssDimension>; | ||
type State = RuleState; | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> { | ||
let node = ctx.query(); | ||
|
||
// dbg!(ctx.root()); | ||
|
||
match node { | ||
AnyCssDimension::CssUnknownDimension(dimension) => { | ||
let unit_token = dimension.unit_token().ok()?; | ||
let unit = unit_token.text_trimmed().to_string(); | ||
|
||
if is_css_hack_unit(&unit) { | ||
return None; | ||
} | ||
|
||
Some(RuleState { | ||
value: unit, | ||
span: unit_token.text_trimmed_range(), | ||
}) | ||
} | ||
AnyCssDimension::CssRegularDimension(dimension) => { | ||
let unit_token = dimension.unit_token().ok()?; | ||
let unit = unit_token.text_trimmed().to_string(); | ||
|
||
if unit == "x" { | ||
ematipico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let mut allow_x = false; | ||
|
||
for ancestor in dimension.unit_token().ok()?.ancestors() { | ||
match ancestor.kind() { | ||
CssSyntaxKind::CSS_FUNCTION => { | ||
let function_name = ancestor | ||
.cast::<CssFunction>()? | ||
.name() | ||
.ok()? | ||
.value_token() | ||
.ok()? | ||
.text_trimmed() | ||
.to_lowercase(); | ||
|
||
if strip_vendor_prefix(function_name.as_str()) == "image-set" { | ||
allow_x = true; | ||
break; | ||
} | ||
} | ||
CssSyntaxKind::CSS_QUERY_FEATURE_PLAIN => { | ||
let feature_name = ancestor | ||
.cast::<CssQueryFeaturePlain>()? | ||
.name() | ||
.ok()? | ||
.value_token() | ||
.ok()? | ||
.text_trimmed() | ||
.to_lowercase(); | ||
|
||
if RESOLUTION_MEDIA_FEATURE_NAMES.contains(&feature_name.as_str()) { | ||
allow_x = true; | ||
break; | ||
} | ||
} | ||
CssSyntaxKind::CSS_GENERIC_PROPERTY => { | ||
let property_name = ancestor | ||
.cast::<CssGenericProperty>()? | ||
.name() | ||
.ok()? | ||
.as_css_identifier()? | ||
.value_token() | ||
.ok()? | ||
.text_trimmed() | ||
.to_lowercase(); | ||
|
||
if property_name == "image-resolution" { | ||
allow_x = true; | ||
break; | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
|
||
if !allow_x { | ||
return Some(RuleState { | ||
value: unit, | ||
span: unit_token.text_trimmed_range(), | ||
}); | ||
} | ||
} | ||
|
||
None | ||
} | ||
_ => None, | ||
} | ||
} | ||
|
||
fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
let span = state.span; | ||
Some( | ||
RuleDiagnostic::new( | ||
rule_category!(), | ||
span, | ||
markup! { | ||
"Unexpected unknown unit: "<Emphasis>{ state.value }</Emphasis> | ||
}, | ||
) | ||
.note(markup! { | ||
"Fix to a known unit." | ||
ematipico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}), | ||
) | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have brought |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
a { font-size: 13pp; } | ||
a { margin: 13xpx; } | ||
a { font-size: .5remm; } | ||
a { font-size: 0.5remm; } | ||
a { color: rgb(255pix, 0, 51); } | ||
a { color: hsl(255pix, 0, 51); } | ||
a { color: rgba(255pix, 0, 51, 1); } | ||
a { color: hsla(255pix, 0, 51, 1); } | ||
a { margin: calc(13pix + 10px); } | ||
a { margin: calc(10pix*2); } | ||
a { margin: calc(2*10pix); } | ||
a { -webkit-transition-delay: 10pix; } | ||
a { margin: -webkit-calc(13pix + 10px); } | ||
a { margin: some-function(13pix + 10px); } | ||
root { --margin: 10pix; } | ||
@media (min-width: 13pix) {} | ||
@media (min-width: 10px)\n and (max-width: 20PIX) {} | ||
@media (width < 10.01REMS) {} | ||
a { width: 1e4pz; } | ||
a { flex: 0 9r9 auto; } | ||
a { width: 400x; } | ||
@media (resolution: 2x) and (min-width: 200x) {} | ||
@media ( resolution: /* comment */ 2x ) and (min-width: 200x) {} | ||
a { background: image-set('img1x.png' 1x, 'img2x.png' 2x) left 20x / 15% 60% repeat-x; } | ||
a { background: /* comment */ image-set('img1x.png' 1x, 'img2x.png' 2x) left 20x / 15% 60% repeat-x; } | ||
a { background-image: image-set('img1x.png' 1pix, 'img2x.png' 2x); } | ||
@font-face { color: U+0100-024F; } | ||
a { unicode-range: U+0100-024F; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please document what this hack is for and why it exists?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a description of the function and the reason for using this function where it's called.