Skip to content
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): implement function-linear-gradient-no-nonstandard-direction #2911

Merged
1 change: 1 addition & 0 deletions Cargo.lock

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

133 changes: 80 additions & 53 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_css_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ biome_deserialize_macros = { workspace = true }
biome_diagnostics = { workspace = true }
biome_rowan = { workspace = true }
lazy_static = { workspace = true }
regex = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }

Expand Down
2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery.rs

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,148 @@
use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource};
use biome_console::markup;
use biome_css_syntax::{CssFunction, CssParameter};
use biome_rowan::AstNode;
use lazy_static::lazy_static;
use regex::Regex;

declare_rule! {
/// Disallow non-standard direction values for linear gradient functions.
///
/// A valid and standard direction value is one of the following:
/// - an angle
/// - to plus a side-or-corner (to top, to bottom, to left, to right; to top right, to right top, to bottom left, etc.)
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
///
/// A common mistake (matching outdated non-standard syntax) is to use just a side-or-corner without the preceding to.
///
/// ## Examples
///
/// ### Invalid
///
/// ```css,expect_diagnostic
/// .foo { background: linear-gradient(top, #fff, #000); }
/// ```
///
/// ```css,expect_diagnostic
/// .foo { background: linear-gradient(45, #fff, #000); }
/// ```
///
/// ### Valid
///
/// ```css
/// .foo { background: linear-gradient(to top, #fff, #000); }
/// ```
///
/// ```css
/// .foo { background: linear-gradient(45deg, #fff, #000); }
/// ```
///
pub NoInvalidDirectionInLinearGradient {
version: "next",
name: "noInvalidDirectionInLinearGradient",
language: "css",
recommended: true,
sources: &[RuleSource::Stylelint("function-linear-gradient-no-nonstandard-direction")],
}
}

lazy_static! {
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
pub static ref GET_PREFIX_REGEX: Regex = Regex::new(r"^(-\w+-)").unwrap();
pub static ref LINEAR_GRADIENT_FUNCTION_NAME: Regex =
Regex::new(r"^(?i)(-webkit-|-moz-|-o-|-ms-)?linear-gradient").unwrap();
pub static ref IN_KEYWORD: Regex = Regex::new(r"(?i)\bin\b").unwrap();
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
pub static ref ANGLE: Regex = Regex::new(r"^[\d.]+(?:deg|grad|rad|turn)$").unwrap();
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
pub static ref DIRECTION: Regex = Regex::new(r"(?i)top|left|bottom|right").unwrap();
pub static ref DIRECTION_WITH_TO: Regex = Regex::new(&format!(
r"(?i)^to ({})(?: ({}))?$",
DIRECTION.as_str(),
DIRECTION.as_str()
))
.unwrap();
pub static ref DIRECTION_WITHOUT_TO: Regex = Regex::new(&format!(
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
r"(?i)^({})(?: ({}))?$",
DIRECTION.as_str(),
DIRECTION.as_str()
))
.unwrap();
pub static ref DIGIT: Regex = Regex::new(r"[\d.]").unwrap();
}

impl Rule for NoInvalidDirectionInLinearGradient {
type Query = Ast<CssFunction>;
type State = CssParameter;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
let node = ctx.query();
let node_name = node.name().ok()?.text();
let is_linear_gradient = LINEAR_GRADIENT_FUNCTION_NAME.is_match(&node_name);
if !is_linear_gradient {
return None;
}
let css_parameter = node.items();

if let Some(Ok(first_css_parameter)) = css_parameter.into_iter().next() {
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
let first_css_parameter_text = first_css_parameter.text();
if IN_KEYWORD.is_match(&first_css_parameter_text) {
return None;
}
if let Some(first_char) = first_css_parameter_text.chars().next() {
ematipico marked this conversation as resolved.
Show resolved Hide resolved
if first_char.is_ascii_digit() {
if ANGLE.is_match(&first_css_parameter_text) {
return None;
}
return Some(first_css_parameter);
}
}
if !DIRECTION.is_match(&first_css_parameter_text) {
return None;
}
let has_prefix = LINEAR_GRADIENT_FUNCTION_NAME
.captures(&node_name)
.and_then(|caps| caps.get(1))
.is_some();
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
if !is_standdard_direction(&first_css_parameter_text, !has_prefix) {
return Some(first_css_parameter);
}
return None;
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
}
None
}

fn diagnostic(_: &RuleContext<Self>, node: &Self::State) -> Option<RuleDiagnostic> {
let span = node.range();
Some(
RuleDiagnostic::new(
rule_category!(),
span,
markup! {
"Unexpected nonstandard direction"
},
).note(markup! {
"See "<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient">"MDN web docs"</Hyperlink>" for more details."
mdm317 marked this conversation as resolved.
Show resolved Hide resolved
})
)
}
}

fn is_standdard_direction(direction: &str, with_to_prefix: bool) -> bool {
let matches = match with_to_prefix {
true => DIRECTION_WITH_TO.captures(direction),
false => DIRECTION_WITHOUT_TO.captures(direction),
};
if let Some(matches) = matches {
match (matches.get(1), matches.get(2)) {
(Some(_), None) => {
return true;
}
(Some(first_direction), Some(second_direction)) => {
if first_direction.as_str() != second_direction.as_str() {
return true;
}
}
_ => return true,
}
}
false
}
1 change: 1 addition & 0 deletions crates/biome_css_analyze/src/options.rs

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,83 @@
.foo {
background: linear-gradient(bottom, #fff, #000);
}
.foo {
background: lInEaR-gRaDiEnT(bottom, #fff, #000);
}
.foo {
background: LINEAR-GRADIENT(bottom, #fff, #000);
}
.foo {
background: linear-gradient(bOtToM, #fff, #000);
}
.foo {
background: linear-gradient(BOTTOM, #fff, #000);
}
.foo {
background: linear-gradient(top, #fff, #000);
}
.foo {
background: linear-gradient(left, #fff, #000);
}
.foo {
background: linear-gradient(right, #fff, #000);
}
.foo {
background: linear-gradient(to top top, #fff, #000);
}
.foo {
background: linear-gradient(45, #fff, #000);
}
.foo {
background: linear-gradient(0.25, #fff, #000);
}
.foo {
background: linear-gradient(1.577, #fff, #000);
}
.foo {
background: linear-gradient(topin, #fff, #000);
}
.foo {
background: -webkit-linear-gradient(to bottom, #fff, #000);
}
.foo {
background: -moz-linear-gradient(to bottom, #fff, #000);
}
.foo {
background: -o-linear-gradient(to bottom, #fff, #000);
}
.foo {
background: url(foo.png), -webkit-linear-gradient(to bottom, #fff, #000);
}
.foo {
background: url(foo.png), -moz-linear-gradient(to bottom, #fff, #000);
}
.foo {
background: url(foo.png), -o-linear-gradient(to bottom, #fff, #000);
}
.foo {
background: -webkit-linear-gradient(to bottom, #fff, #000), url(foo.png);
}
.foo {
background: url(foo.png), -moz-linear-gradient(to bottom, #fff, #000),
url(foo.png);
}
.foo {
background: -o-linear-gradient(to bottom, #fff, #000), url(foo.png);
}
.foo {
background: url(foo.png), -webkit-linear-gradient(to bottom, #fff, #000),
url(bar.png);
}
.foo {
background: url(foo.png), -moz-linear-gradient(to bottom, #fff, #000),
url(bar.png);
}
.foo {
background: url(foo.png), -o-linear-gradient(to bottom, #fff, #000),
url(bar.png);
}
.foo {
background: url(foo.png), -ms-linear-gradient(to bottom, #fff, #000),
url(bar.png);
}
Loading