Skip to content

Commit

Permalink
feat(biome_css_analyzer): noUnknownUnit (#2535)
Browse files Browse the repository at this point in the history
Co-authored-by: Emanuele Stoppa <[email protected]>
  • Loading branch information
neoki07 and ematipico committed Apr 23, 2024
1 parent 81c7a85 commit 7ea5dff
Show file tree
Hide file tree
Showing 11 changed files with 1,108 additions and 4 deletions.
29 changes: 25 additions & 4 deletions crates/biome_configuration/src/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod no_css_empty_block;
pub mod no_duplicate_font_names;
pub mod no_duplicate_selectors_keyframe_block;
pub mod no_important_in_keyframe;
pub mod no_unknown_unit;

declare_group! {
pub Nursery {
Expand All @@ -17,6 +18,7 @@ declare_group! {
self :: no_duplicate_font_names :: NoDuplicateFontNames ,
self :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock ,
self :: no_important_in_keyframe :: NoImportantInKeyframe ,
self :: no_unknown_unit :: NoUnknownUnit ,
]
}
}
187 changes: 187 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery/no_unknown_unit.rs
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."],
),

)
}
}
2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ pub type NoDuplicateFontNames =
<lint::nursery::no_duplicate_font_names::NoDuplicateFontNames as biome_analyze::Rule>::Options;
pub type NoDuplicateSelectorsKeyframeBlock = < lint :: nursery :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock as biome_analyze :: Rule > :: Options ;
pub type NoImportantInKeyframe = < lint :: nursery :: no_important_in_keyframe :: NoImportantInKeyframe as biome_analyze :: Rule > :: Options ;
pub type NoUnknownUnit =
<lint::nursery::no_unknown_unit::NoUnknownUnit as biome_analyze::Rule>::Options;
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; }
Loading

0 comments on commit 7ea5dff

Please sign in to comment.