-
-
Notifications
You must be signed in to change notification settings - Fork 421
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(biome_css_analyzer): noUnknownUnit (#2535)
Co-authored-by: Emanuele Stoppa <[email protected]>
- Loading branch information
Showing
11 changed files
with
1,108 additions
and
4 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
crates/biome_css_analyze/src/lint/nursery/no_unknown_unit.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
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}; | ||
|
||
const RESOLUTION_MEDIA_FEATURE_NAMES: [&str; 3] = | ||
["resolution", "min-resolution", "max-resolution"]; | ||
|
||
declare_rule! { | ||
/// Disallow unknown CSS units. | ||
/// | ||
/// For details on known CSS units, see the [MDN web docs](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#lengths). | ||
/// | ||
/// | ||
/// ## 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: true, | ||
sources: &[RuleSource::Stylelint("unit-no-unknown")], | ||
} | ||
} | ||
|
||
pub struct NoUnknownUnitState { | ||
unit: String, | ||
span: TextRange, | ||
} | ||
|
||
impl Rule for NoUnknownUnit { | ||
type Query = Ast<AnyCssDimension>; | ||
type State = NoUnknownUnitState; | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> { | ||
let node = ctx.query(); | ||
|
||
match node { | ||
AnyCssDimension::CssUnknownDimension(dimension) => { | ||
let unit_token = dimension.unit_token().ok()?; | ||
let unit = unit_token.text_trimmed().to_string(); | ||
|
||
Some(NoUnknownUnitState { | ||
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(); | ||
|
||
// The `x` unit is parsed as `CssRegularDimension`, but it is used for describing resolutions. | ||
// This check is to disallow the use of the `x` unit outside this specific context. | ||
if unit == "x" { | ||
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 function_name.ends_with("image-set") { | ||
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; | ||
} | ||
} | ||
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; | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
|
||
if !allow_x { | ||
return Some(NoUnknownUnitState { | ||
unit, | ||
span: unit_token.text_trimmed_range(), | ||
}); | ||
} | ||
} | ||
|
||
None | ||
} | ||
_ => None, | ||
} | ||
} | ||
|
||
fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
Some( | ||
RuleDiagnostic::new( | ||
rule_category!(), | ||
state.span, | ||
markup! { | ||
"Unexpected unknown unit: "<Emphasis>{ state.unit }</Emphasis> | ||
}, | ||
) | ||
.note(markup! { | ||
"See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#lengths">"MDN web docs"</Hyperlink>" for more details." | ||
}) | ||
.footer_list( | ||
markup! { | ||
"Use a known unit instead, such as:" | ||
}, | ||
&["px", "em", "rem", "etc."], | ||
), | ||
|
||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
crates/biome_css_analyze/tests/specs/nursery/noUnknownUnit/invalid.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } |
Oops, something went wrong.