Skip to content

Commit

Permalink
feat(biome_css_analyze): implement noDuplicateFontNames (#2308)
Browse files Browse the repository at this point in the history
  • Loading branch information
togami2864 authored Apr 11, 2024
1 parent 43f17a3 commit ce223aa
Show file tree
Hide file tree
Showing 19 changed files with 652 additions and 49 deletions.
14 changes: 12 additions & 2 deletions crates/biome_analyze/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pub enum RuleSource {
EslintMysticatea(&'static str),
/// Rules from [Eslint Plugin Barrel Files](https://github.com/thepassle/eslint-plugin-barrel-files)
EslintBarrelFiles(&'static str),
/// Rules from [Stylelint](https://github.com/stylelint/stylelint)
Stylelint(&'static str),
}

impl PartialEq for RuleSource {
Expand All @@ -112,6 +114,7 @@ impl std::fmt::Display for RuleSource {
RuleSource::EslintUnicorn(_) => write!(f, "eslint-plugin-unicorn"),
RuleSource::EslintMysticatea(_) => write!(f, "eslint-plugin-mysticates"),
RuleSource::EslintBarrelFiles(_) => write!(f, "eslint-plugin-barrel-files"),
RuleSource::Stylelint(_) => write!(f, "Stylelint"),
}
}
}
Expand Down Expand Up @@ -154,7 +157,8 @@ impl RuleSource {
| Self::EslintStylistic(rule_name)
| Self::EslintUnicorn(rule_name)
| Self::EslintMysticatea(rule_name)
| Self::EslintBarrelFiles(rule_name) => rule_name,
| Self::EslintBarrelFiles(rule_name)
| Self::Stylelint(rule_name) => rule_name,
}
}

Expand All @@ -173,6 +177,7 @@ impl RuleSource {
Self::EslintUnicorn(rule_name) => format!("unicorn/{rule_name}"),
Self::EslintMysticatea(rule_name) => format!("@mysticatea/{rule_name}"),
Self::EslintBarrelFiles(rule_name) => format!("barrel-files/{rule_name}"),
Self::Stylelint(rule_name) => format!("stylelint/{rule_name}"),
}
}

Expand All @@ -191,7 +196,8 @@ impl RuleSource {
Self::EslintStylistic(rule_name) => format!("https://eslint.style/rules/default/{rule_name}"),
Self::EslintUnicorn(rule_name) => format!("https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/{rule_name}.md"),
Self::EslintMysticatea(rule_name) => format!("https://github.com/mysticatea/eslint-plugin/blob/master/docs/rules/{rule_name}.md"),
Self::EslintBarrelFiles(rule_name) => format!("https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/{rule_name}.md")
Self::EslintBarrelFiles(rule_name) => format!("https://github.com/thepassle/eslint-plugin-barrel-files/blob/main/docs/rules/{rule_name}.md"),
Self::Stylelint(rule_name) => format!("https://github.com/stylelint/stylelint/blob/main/lib/rules/{rule_name}/README.md"),
}
}

Expand All @@ -208,6 +214,10 @@ impl RuleSource {
pub const fn is_eslint_plugin(&self) -> bool {
!matches!(self, Self::Clippy(_) | Self::Eslint(_))
}

pub const fn is_stylelint(&self) -> bool {
matches!(self, Self::Stylelint(_))
}
}

#[derive(Debug, Default, Clone, Copy)]
Expand Down
113 changes: 67 additions & 46 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions crates/biome_css_analyze/src/keywords.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
pub const BASIC_KEYWORDS: [&str; 5] = ["initial", "inherit", "revert", "revert-layer", "unset"];

pub const _SYSTEM_FONT_KEYWORDS: [&str; 6] = [
"caption",
"icon",
"menu",
"message-box",
"small-caption",
"status-bar",
];

pub const FONT_FAMILY_KEYWORDS: [&str; 10] = [
"serif",
"sans-serif",
"cursive",
"fantasy",
"monospace",
"system-ui",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
];

pub const FONT_WEIGHT_ABSOLUTE_KEYWORDS: [&str; 2] = ["normal", "bold"];
pub const FONT_WEIGHT_NUMERIC_KEYWORDS: [&str; 9] = [
"100", "200", "300", "400", "500", "600", "700", "800", "900",
];
pub const FONT_STYLE_KEYWORDS: [&str; 3] = ["normal", "italic", "oblique"];
pub const FONT_VARIANTS_KEYWORDS: [&str; 35] = [
"normal",
"none",
"historical-forms",
"none",
"common-ligatures",
"no-common-ligatures",
"discretionary-ligatures",
"no-discretionary-ligatures",
"historical-ligatures",
"no-historical-ligatures",
"contextual",
"no-contextual",
"small-caps",
"all-small-caps",
"petite-caps",
"all-petite-caps",
"unicase",
"titling-caps",
"lining-nums",
"oldstyle-nums",
"proportional-nums",
"tabular-nums",
"diagonal-fractions",
"stacked-fractions",
"ordinal",
"slashed-zero",
"jis78",
"jis83",
"jis90",
"jis04",
"simplified",
"traditional",
"full-width",
"proportional-width",
"ruby",
];

pub const FONT_STRETCH_KEYWORDS: [&str; 8] = [
"semi-condensed",
"condensed",
"extra-condensed",
"ultra-condensed",
"semi-expanded",
"expanded",
"extra-expanded",
"ultra-expanded",
];

pub const FONT_SIZE_KEYWORDS: [&str; 9] = [
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "larger", "smaller",
];

pub const LINE_HEIGHT_KEYWORDS: [&str; 1] = ["normal"];
2 changes: 2 additions & 0 deletions crates/biome_css_analyze/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod keywords;
mod lint;
pub mod options;
mod registry;
mod utils;

pub use crate::registry::visit_registry;
use biome_analyze::{
Expand Down
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 @@ -3,12 +3,14 @@
use biome_analyze::declare_group;

pub mod no_color_invalid_hex;
pub mod no_duplicate_font_names;

declare_group! {
pub Nursery {
name : "nursery" ,
rules : [
self :: no_color_invalid_hex :: NoColorInvalidHex ,
self :: no_duplicate_font_names :: NoDuplicateFontNames ,
]
}
}
152 changes: 152 additions & 0 deletions crates/biome_css_analyze/src/lint/nursery/no_duplicate_font_names.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::collections::HashSet;

use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource};
use biome_console::markup;
use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericProperty};
use biome_rowan::{AstNode, TextRange};

use crate::utils::{find_font_family, is_font_family_keyword};

declare_rule! {
/// Disallow duplicate names within font families.
///
/// This rule checks the `font` and `font-family` properties for duplicate font names.
///
/// This rule ignores var(--custom-property) variable syntaxes now.
///
///
/// ## Examples
///
/// ### Invalid
///
/// ```css,expect_diagnostic
/// a { font-family: "Lucida Grande", 'Arial', sans-serif, sans-serif; }
/// ```
///
/// ```css,expect_diagnostic
/// a { font-family: 'Arial', "Lucida Grande", Arial, sans-serif; }
/// ```
///
/// ```css,expect_diagnostic
/// a { FONT: italic 300 16px/30px Arial, " Arial", serif; }
/// ```
///
/// ### Valid
///
/// ```css
/// a { font-family: "Lucida Grande", "Arial", sans-serif; }
/// ```
///
/// ```css
/// b { font: normal 14px/32px -apple-system, BlinkMacSystemFont, sans-serif; }
/// ```
pub NoDuplicateFontNames {
version: "next",
name: "noDuplicateFontNames",
recommended: true,
sources: &[RuleSource::Stylelint("font-family-no-duplicate-names")],
}
}

pub struct RuleState {
value: String,
span: TextRange,
}

impl Rule for NoDuplicateFontNames {
type Query = Ast<CssGenericProperty>;
type State = RuleState;
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
let node = ctx.query();
let property_name = node.name().ok()?.text().to_lowercase();

let is_font_family = property_name == "font-family";
let is_font = property_name == "font";

if !is_font_family && !is_font {
return None;
}

let mut unquoted_family_names: HashSet<String> = HashSet::new();
let mut family_names: HashSet<String> = HashSet::new();
let value_list = node.value();
let font_families = if is_font {
find_font_family(value_list)
} else {
value_list
.into_iter()
.filter_map(|v| match v {
AnyCssGenericComponentValue::AnyCssValue(value) => Some(value),
_ => None,
})
.collect()
};

for css_value in font_families {
match css_value {
// A generic family name like `sans-serif` or unquoted font name.
AnyCssValue::CssIdentifier(val) => {
let font_name = val.text();

// check the case: "Arial", Arial
// we ignore the case of the font name is a keyword(context: https://github.com/stylelint/stylelint/issues/1284)
// e.g "sans-serif", sans-serif
if family_names.contains(&font_name) && !is_font_family_keyword(&font_name) {
return Some(RuleState {
value: font_name,
span: val.range(),
});
}

// check the case: sans-self, sans-self
if unquoted_family_names.contains(&font_name) {
return Some(RuleState {
value: font_name,
span: val.range(),
});
}
unquoted_family_names.insert(font_name);
}
// A font family name. e.g "Lucida Grande", "Arial".
AnyCssValue::CssString(val) => {
let normalized_font_name: String = val
.text()
.chars()
.filter(|&c| c != '\'' && c != '\"' && !c.is_whitespace())
.collect();

if family_names.contains(&normalized_font_name)
|| unquoted_family_names.contains(&normalized_font_name)
{
return Some(RuleState {
value: normalized_font_name,
span: val.range(),
});
}
family_names.insert(normalized_font_name);
}
_ => continue,
}
}
None
}

fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let span = state.span;
Some(
RuleDiagnostic::new(
rule_category!(),
span,
markup! {
"Unexpected duplicate font name: "<Emphasis>{ state.value }</Emphasis>
},
)
.note(markup! {
"Remove duplicate font names within the property"
}),
)
}
}
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 @@ -4,3 +4,5 @@ use crate::lint;

pub type NoColorInvalidHex =
<lint::nursery::no_color_invalid_hex::NoColorInvalidHex as biome_analyze::Rule>::Options;
pub type NoDuplicateFontNames =
<lint::nursery::no_duplicate_font_names::NoDuplicateFontNames as biome_analyze::Rule>::Options;
Loading

0 comments on commit ce223aa

Please sign in to comment.