diff --git a/Cargo.lock b/Cargo.lock index 0424f406f5b8..6cbd89baf7b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,7 @@ dependencies = [ "biome_test_utils", "insta", "lazy_static", + "rustc-hash", "schemars", "serde", "tests_macros", diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index 6dd240c3d2b1..f24ad9f9b521 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2905,6 +2905,9 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub use_consistent_builtin_instantiation: Option>, + #[doc = "Disallowing invalid named grid areas in CSS Grid Layouts."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_consistent_grid_areas: Option>, #[doc = "Use Date.now() to get the number of milliseconds since the Unix Epoch."] #[serde(skip_serializing_if = "Option::is_none")] pub use_date_now: Option>, @@ -2996,6 +2999,7 @@ impl Nursery { "noYodaExpression", "useAdjacentOverloadSignatures", "useConsistentBuiltinInstantiation", + "useConsistentGridAreas", "useDateNow", "useDefaultSwitchClause", "useErrorMessage", @@ -3049,9 +3053,9 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3098,6 +3102,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3264,76 +3269,81 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_date_now.as_ref() { + if let Some(rule) = self.use_consistent_grid_areas.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_date_now.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_throw_new_error.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } + if let Some(rule) = self.use_top_level_regex.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3488,76 +3498,81 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_date_now.as_ref() { + if let Some(rule) = self.use_consistent_grid_areas.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_date_now.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_throw_new_error.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } + if let Some(rule) = self.use_top_level_regex.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3714,6 +3729,10 @@ impl Nursery { .use_consistent_builtin_instantiation .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useConsistentGridAreas" => self + .use_consistent_grid_areas + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useDateNow" => self .use_date_now .as_ref() diff --git a/crates/biome_css_analyze/Cargo.toml b/crates/biome_css_analyze/Cargo.toml index 4b877301ac79..56a480b6035a 100644 --- a/crates/biome_css_analyze/Cargo.toml +++ b/crates/biome_css_analyze/Cargo.toml @@ -22,6 +22,7 @@ biome_diagnostics = { workspace = true } biome_rowan = { workspace = true } biome_suppression = { workspace = true } lazy_static = { workspace = true } +rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 387c775568d0..b5b6861bf249 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -16,6 +16,7 @@ pub mod no_unknown_pseudo_class_selector; pub mod no_unknown_selector_pseudo_element; pub mod no_unknown_unit; pub mod no_unmatchable_anb_selector; +pub mod use_consistent_grid_areas; pub mod use_generic_font_names; declare_group! { @@ -36,6 +37,7 @@ declare_group! { self :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement , self :: no_unknown_unit :: NoUnknownUnit , self :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector , + self :: use_consistent_grid_areas :: UseConsistentGridAreas , self :: use_generic_font_names :: UseGenericFontNames , ] } diff --git a/crates/biome_css_analyze/src/lint/nursery/use_consistent_grid_areas.rs b/crates/biome_css_analyze/src/lint/nursery/use_consistent_grid_areas.rs new file mode 100644 index 000000000000..d836d4280a43 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/use_consistent_grid_areas.rs @@ -0,0 +1,243 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_css_syntax::CssDeclarationOrRuleList; +use biome_rowan::{TextRange, TokenText}; + +use rustc_hash::FxHashSet; + +declare_rule! { + /// Disallows invalid named grid areas in CSS Grid Layouts. + /// + /// For a named grid area to be valid, all strings must define: + /// + /// - the same number of cell tokens + /// - at least one cell token + /// + /// And all named grid areas that spans multiple grid cells must form a single filled-in rectangle. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// a { grid-template-areas: "a a" + /// "b b b"; } + /// ``` + /// + /// ```css,expect_diagnostic + /// a { grid-template-areas: "b b b" + /// ""; } + /// ``` + /// + /// ```css,expect_diagnostic + /// a { grid-template-areas: "a a a" + /// "b b a"; } + /// ``` + /// + /// ### Valid + /// + /// ```css + /// a { grid-template-areas: "a a a" + /// "b b b"; } + /// ``` + /// + /// ```css + /// a { grid-template-areas: "a a a" + /// "a a a"; } + /// ``` + /// + pub UseConsistentGridAreas { + version: "next", + name: "useConsistentGridAreas", + language: "css", + recommended: false, + sources: &[RuleSource::Stylelint("named-grid-areas-no-invalid")], + } +} + +type GridAreasProp = (String, TextRange); +type GridAreasProps = Vec<(TokenText, TextRange)>; + +#[derive(Debug)] +enum GridAreaValidationError { + EmptyGridArea, + InconsistentCellCount, + DuplicateGridToken, +} + +pub struct UseConsistentGridAreasState { + text: Option, + span: TextRange, + reason: GridAreaValidationError, +} + +impl Rule for UseConsistentGridAreas { + type Query = Ast; + type State = UseConsistentGridAreasState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node = ctx.query(); + // Extracting the property values of grid-template-areas + let plain_grid_areas_props = node + .into_iter() + .filter_map(|item| { + let grid_props = item + .as_css_declaration_with_semicolon()? + .declaration() + .ok()? + .property() + .ok()? + .as_css_generic_property()? + .value(); + Some(grid_props) + }) + .flat_map(|grid_props| { + grid_props + .into_iter() + .filter_map(|x| x.as_any_css_value()?.as_css_string()?.value_token().ok()) + }) + // Need to remove `"` with escaping slash from the grid area + // Ex: "\"a a a\"" + .map(|x| { + let trimmed_text = x.token_text(); + let text_range = x.text_range(); + (trimmed_text, text_range) + }) + .collect::(); + + if !plain_grid_areas_props.is_empty() { + is_consistent_grids(plain_grid_areas_props) + } else { + None + } + } + + fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { + match state.reason { + GridAreaValidationError::EmptyGridArea => Some( + RuleDiagnostic::new( + rule_category!(), + state.span, + markup! { + "Empty grid areas are not allowed." + }, + ) + .note(markup! { + "Consider adding the cell token within string." + }), + ), + GridAreaValidationError::InconsistentCellCount => Some( + RuleDiagnostic::new( + rule_category!(), + state.span, + markup! { + "Inconsistent cell count in grid areas are not allowed." + }, + ) + .note(markup! { + "Consider adding the same number of cell tokens in each string." + }), + ), + GridAreaValidationError::DuplicateGridToken => Some( + RuleDiagnostic::new( + rule_category!(), + state.span, + markup! { + "Duplicate filled in rectangle are not allowed." + }, + ) + .note(markup! { + "Consider removing the duplicated filled-in rectangle: " {state.text.as_ref().unwrap()} + }), + ), + } + } +} + +// Check if the grid areas are consistent +fn is_consistent_grids(grid_areas_props: GridAreasProps) -> Option { + let first_prop = clean_text(&grid_areas_props[0].0); + let first_len = first_prop.len(); + let mut shortest = &grid_areas_props[0]; + + for grid_areas_prop in &grid_areas_props { + let cleaned_text = clean_text(&grid_areas_prop.0); + // Check if the grid areas are empty + if cleaned_text.is_empty() { + return Some(UseConsistentGridAreasState { + text: None, + span: grid_areas_prop.1, + reason: GridAreaValidationError::EmptyGridArea, + }); + } + // Check if all elements have the same length + if cleaned_text.len() != first_len { + if cleaned_text.len() < clean_text(&shortest.0).len() { + shortest = grid_areas_prop; + } + return Some(UseConsistentGridAreasState { + text: None, + span: shortest.1, + reason: GridAreaValidationError::InconsistentCellCount, + }); + } + } + + // Check if there are no duplicate grid tokens + // It should be partial match because for example, in the following grid areas: + // {"a a a" + // "b b b"; } + // are the consistent grid properties because it forms a single filled-in rectangle. + if grid_areas_props + .iter() + .all(|prop| is_all_same(prop.0.clone())) + { + return None; + } + // But in the following grid areas: + // {"a a a" + // "b b a"; } + // are not consistent because `a` breaks a single filled-in rectangle. + if let Some(result) = has_partial_match(&grid_areas_props) { + return Some(UseConsistentGridAreasState { + text: Some(result.0), + span: result.1, + reason: GridAreaValidationError::DuplicateGridToken, + }); + } + + None +} + +// Check if all characters in a string are the same +fn is_all_same(token_text: TokenText) -> bool { + let prop = clean_text(&token_text); + let chars: Vec = prop.chars().filter(|c| !c.is_whitespace()).collect(); + let head = chars[0]; + chars.iter().all(|&c| c == head) +} + +fn has_partial_match(grid_areas_props: &GridAreasProps) -> Option { + let mut seen_parts = FxHashSet::default(); + + for (text, range) in grid_areas_props { + let prop = clean_text(text); + let parts: FxHashSet = prop + .split_whitespace() + .map(|part| part.to_string()) + .collect(); + for part in parts { + if !seen_parts.insert(part.clone()) { + return Some((part, *range)); + } + } + } + + None +} + +fn clean_text(text: &TokenText) -> String { + text.replace('"', "").trim().to_string() +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 68e4afd0a9bc..63bda292b251 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -21,5 +21,6 @@ pub type NoUnknownSelectorPseudoElement = < lint :: nursery :: no_unknown_select pub type NoUnknownUnit = ::Options; pub type NoUnmatchableAnbSelector = < lint :: nursery :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector as biome_analyze :: Rule > :: Options ; +pub type UseConsistentGridAreas = < lint :: nursery :: use_consistent_grid_areas :: UseConsistentGridAreas as biome_analyze :: Rule > :: Options ; pub type UseGenericFontNames = ::Options; diff --git a/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css new file mode 100644 index 000000000000..1e0ad7a62c99 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css @@ -0,0 +1,28 @@ +a { grid-template-areas: "" } +a { grid-template-areas: "a a" + "b b b"; } +a { grid-template-areas: "b b b" + ""; } +a { grid-template-areas: "a a a" + "a b a"; } +a { grid-template-areas: "a a a" + "b b b" + "c c c" + "g g g" + "z y a"; } +a { grid-template-areas: "a a a" + "b b b"; } +a { grid-template-areas: "a a a" + "a . a"; } +a { grid-template-areas: "o o o ," + "p , p p" + "q q , q"; } +a { grid-template-areas: "s s t t" + "s s t t" + "u v v" + "u u v v"; } +a { grid-template-areas: "a a a" + "b z a"; } +a { grid-template-areas: "a a a" + "g f f" + "b z a"; } diff --git a/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css.snap new file mode 100644 index 000000000000..16c8e4b54ce5 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css.snap @@ -0,0 +1,219 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +a { grid-template-areas: "" } +a { grid-template-areas: "a a" + "b b b"; } +a { grid-template-areas: "b b b" + ""; } +a { grid-template-areas: "a a a" + "a b a"; } +a { grid-template-areas: "a a a" + "b b b" + "c c c" + "g g g" + "z y a"; } +a { grid-template-areas: "a a a" + "b b b"; } +a { grid-template-areas: "a a a" + "a . a"; } +a { grid-template-areas: "o o o ," + "p , p p" + "q q , q"; } +a { grid-template-areas: "s s t t" + "s s t t" + "u v v" + "u u v v"; } +a { grid-template-areas: "a a a" + "b z a"; } +a { grid-template-areas: "a a a" + "g f f" + "b z a"; } + +``` + +# Diagnostics +``` +invalid.css:1:26 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Empty grid areas are not allowed. + + > 1 │ a { grid-template-areas: "" } + │ ^^^ + 2 │ a { grid-template-areas: "a a" + 3 │ "b b b"; } + + i Consider adding the cell token within string. + + +``` + +``` +invalid.css:2:26 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Inconsistent cell count in grid areas are not allowed. + + 1 │ a { grid-template-areas: "" } + > 2 │ a { grid-template-areas: "a a" + │ ^^^^^ + 3 │ "b b b"; } + 4 │ a { grid-template-areas: "b b b" + + i Consider adding the same number of cell tokens in each string. + + +``` + +``` +invalid.css:4:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Empty grid areas are not allowed. + + 2 │ a { grid-template-areas: "a a" + 3 │ "b b b"; } + > 4 │ a { grid-template-areas: "b b b" + │ + > 5 │ ""; } + │ ^^ + 6 │ a { grid-template-areas: "a a a" + 7 │ "a b a"; } + + i Consider adding the cell token within string. + + +``` + +``` +invalid.css:6:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed. + + 4 │ a { grid-template-areas: "b b b" + 5 │ ""; } + > 6 │ a { grid-template-areas: "a a a" + │ + > 7 │ "a b a"; } + │ ^^^^^^^ + 8 │ a { grid-template-areas: "a a a" + 9 │ "b b b" + + i Consider removing the duplicated filled-in rectangle: a + + +``` + +``` +invalid.css:11:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed. + + 9 │ "b b b" + 10 │ "c c c" + > 11 │ "g g g" + │ + > 12 │ "z y a"; } + │ ^^^^^^^ + 13 │ a { grid-template-areas: "a a a" + 14 │ "b b b"; } + + i Consider removing the duplicated filled-in rectangle: a + + +``` + +``` +invalid.css:15:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed. + + 13 │ a { grid-template-areas: "a a a" + 14 │ "b b b"; } + > 15 │ a { grid-template-areas: "a a a" + │ + > 16 │ "a . a"; } + │ ^^^^^^^ + 17 │ a { grid-template-areas: "o o o ," + 18 │ "p , p p" + + i Consider removing the duplicated filled-in rectangle: a + + +``` + +``` +invalid.css:17:35 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed. + + 15 │ a { grid-template-areas: "a a a" + 16 │ "a . a"; } + > 17 │ a { grid-template-areas: "o o o ," + │ + > 18 │ "p , p p" + │ ^^^^^^^^^ + 19 │ "q q , q"; } + 20 │ a { grid-template-areas: "s s t t" + + i Consider removing the duplicated filled-in rectangle: , + + +``` + +``` +invalid.css:21:35 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Inconsistent cell count in grid areas are not allowed. + + 19 │ "q q , q"; } + 20 │ a { grid-template-areas: "s s t t" + > 21 │ "s s t t" + │ + > 22 │ "u v v" + │ ^^^^^^^ + 23 │ "u u v v"; } + 24 │ a { grid-template-areas: "a a a" + + i Consider adding the same number of cell tokens in each string. + + +``` + +``` +invalid.css:24:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed. + + 22 │ "u v v" + 23 │ "u u v v"; } + > 24 │ a { grid-template-areas: "a a a" + │ + > 25 │ "b z a"; } + │ ^^^^^^^ + 26 │ a { grid-template-areas: "a a a" + 27 │ "g f f" + + i Consider removing the duplicated filled-in rectangle: a + + +``` + +``` +invalid.css:27:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed. + + 25 │ "b z a"; } + 26 │ a { grid-template-areas: "a a a" + > 27 │ "g f f" + │ + > 28 │ "b z a"; } + │ ^^^^^^^ + 29 │ + + i Consider removing the duplicated filled-in rectangle: a + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css new file mode 100644 index 000000000000..c692e80a3c08 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css @@ -0,0 +1,18 @@ +a { grid-template-areas: "a a a" + "b b b"; } +a { grid-template-areas: "a a a" + "a a a" + "b b b" + "b b b"; } +a { grid-template-areas: "o o o o" + "p p p p" + "q q q q"; } +a { grid-template-areas: "s s s" + "s s s" + "v v v" + "u u u"; } +a { grid-template-areas: "s s s" + "a a a" + "v v v" + "u u u" + "a a a"; } diff --git a/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css.snap new file mode 100644 index 000000000000..2ff2cfeb02cf --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css.snap @@ -0,0 +1,26 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +a { grid-template-areas: "a a a" + "b b b"; } +a { grid-template-areas: "a a a" + "a a a" + "b b b" + "b b b"; } +a { grid-template-areas: "o o o o" + "p p p p" + "q q q q"; } +a { grid-template-areas: "s s s" + "s s s" + "v v v" + "u u u"; } +a { grid-template-areas: "s s s" + "a a a" + "v v v" + "u u u" + "a a a"; } + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 591c7eb5f651..2e476e66e35d 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -148,6 +148,7 @@ define_categories! { "lint/nursery/useAdjacentOverloadSignatures": "https://biomejs.dev/linter/rules/use-adjacent-overload-signatures", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", "lint/nursery/useConsistentBuiltinInstantiation": "https://biomejs.dev/linter/rules/use-consistent-new-builtin", + "lint/nursery/useConsistentGridAreas": "https://biomejs.dev/linter/rules/use-consistent-grid-areas", "lint/nursery/useDateNow": "https://biomejs.dev/linter/rules/use-date-now", "lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause", "lint/nursery/useErrorMessage": "https://biomejs.dev/linter/rules/use-error-message", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 6d2802d0efa2..03c433252930 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1098,6 +1098,10 @@ export interface Nursery { * Enforce the use of new for all builtins, except String, Number, Boolean, Symbol and BigInt. */ useConsistentBuiltinInstantiation?: RuleFixConfiguration_for_Null; + /** + * Disallowing invalid named grid areas in CSS Grid Layouts. + */ + useConsistentGridAreas?: RuleConfiguration_for_Null; /** * Use Date.now() to get the number of milliseconds since the Unix Epoch. */ @@ -2365,6 +2369,7 @@ export type Category = | "lint/nursery/useAdjacentOverloadSignatures" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useConsistentBuiltinInstantiation" + | "lint/nursery/useConsistentGridAreas" | "lint/nursery/useDateNow" | "lint/nursery/useDefaultSwitchClause" | "lint/nursery/useErrorMessage" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index a375e7df175e..acb99a340da5 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1878,6 +1878,13 @@ { "type": "null" } ] }, + "useConsistentGridAreas": { + "description": "Disallowing invalid named grid areas in CSS Grid Layouts.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useDateNow": { "description": "Use Date.now() to get the number of milliseconds since the Unix Epoch.", "anyOf": [