diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b91195962af6..844c4c982d94 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2827,6 +2827,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 = "Require the default clause in switch statements."] #[serde(skip_serializing_if = "Option::is_none")] pub use_default_switch_clause: Option>, @@ -2915,6 +2918,7 @@ impl Nursery { "useAdjacentOverloadSignatures", "useArrayLiterals", "useConsistentBuiltinInstantiation", + "useConsistentGridAreas", "useDefaultSwitchClause", "useErrorMessage", "useExplicitLengthCheck", @@ -2965,9 +2969,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[33]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3013,6 +3017,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), 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]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3179,71 +3184,76 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_default_switch_clause.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_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[31])); } } - 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[32])); } } - 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[33])); } } - 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[34])); } } - 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[35])); } } - 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[36])); } } - 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[37])); } } - 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[38])); } } - 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[39])); } } - 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[40])); } } - 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[41])); } } - 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[42])); } } + 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[43])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3398,71 +3408,76 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_default_switch_clause.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_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[31])); } } - 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[32])); } } - 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[33])); } } - 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[34])); } } - 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[35])); } } - 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[36])); } } - 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[37])); } } - 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[38])); } } - 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[39])); } } - 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[40])); } } - 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[41])); } } - 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[42])); } } + 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[43])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3619,6 +3634,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())), "useDefaultSwitchClause" => self .use_default_switch_clause .as_ref() diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 177f67240b5e..b9d9b46c4104 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -14,6 +14,7 @@ pub mod no_unknown_property; 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! { @@ -32,6 +33,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..42c5dd1a2378 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/use_consistent_grid_areas.rs @@ -0,0 +1,234 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; +use biome_console::markup; +use biome_css_syntax::{CssDeclarationOrRuleList, CssLanguage}; +use biome_rowan::{SyntaxToken, TextRange}; + +declare_rule! { + /// Disallowing 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, + } +} + +type GridAreaProperty = SyntaxToken; +type GridAreaProps = Vec; + +#[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 grid_areas_props: GridAreaProps = 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()) + }) + .collect(); + + is_consistent_grids(grid_areas_props) + } + + 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: GridAreaProps) -> Option { + let plain_grid_areas_props = grid_areas_props + .iter() + .map(|x| { + // Need to remove `"` with escaping slash from the grid area + // Ex: "\"a a a\"" + let trimmed_text = x.token_text_trimmed().replace('"', ""); + let text_range = x.text_range(); + (trimmed_text, text_range) + }) + .collect::>(); + + // Check if the grid areas are empty + if let Some(prop) = plain_grid_areas_props.iter().find(|prop| prop.0.is_empty()) { + return Some(UseConsistentGridAreasState { + text: None, + span: prop.1, + reason: GridAreaValidationError::EmptyGridArea, + }); + } + + // Check if all elements have the same length + if let Some(prop) = plain_grid_areas_props + .iter() + .find(|prop| prop.0.len() != plain_grid_areas_props[0].0.len()) + { + if prop.0.len() < plain_grid_areas_props[0].0.len() { + return Some(UseConsistentGridAreasState { + text: None, + span: prop.1, + reason: GridAreaValidationError::InconsistentCellCount, + }); + } else { + return Some(UseConsistentGridAreasState { + text: None, + span: plain_grid_areas_props[0].1, + reason: GridAreaValidationError::InconsistentCellCount, + }); + } + } + // Check if each property of grid areas are the same + if plain_grid_areas_props + .iter() + .all(|prop| is_all_same(prop.0.as_str())) + { + return None; + } + // 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. + // 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(&plain_grid_areas_props) { + return Some(UseConsistentGridAreasState { + text: Some(result.0), + span: result.1, + reason: GridAreaValidationError::DuplicateGridToken, + }); + } + + None +} + +fn is_all_same(prop: &str) -> bool { + let arr: Vec = prop.chars().filter(|&c| !c.is_whitespace()).collect(); + let result = arr.windows(2).all(|w| w[0] == w[1]); + result +} + +fn has_partial_match(vec: &[(String, TextRange)]) -> Option<(String, TextRange)> { + for (i, s1) in vec.iter().enumerate() { + let parts: Vec<&str> = s1.0.split_whitespace().collect(); + for (j, s2) in vec.iter().enumerate() { + if i != j { + let parts2: Vec<&str> = s2.0.split_whitespace().collect(); + for part in &parts { + if parts2.contains(part) { + return Some(((*part).to_string(), s2.1)); + } + } + } + } + } + None +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 544a93840219..e167aebba08a 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -19,5 +19,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..8c9125c24c99 --- /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 a"; } +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.new b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css.snap.new new file mode 100644 index 000000000000..3625634f1bb8 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/invalid.css.snap.new @@ -0,0 +1,239 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +assertion_line: 83 +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 a"; } +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 a"; } + + i Consider removing the duplicated filled-in rectangle: a + + +``` + +``` +invalid.css:13:33 lint/nursery/useConsistentGridAreas ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Duplicate filled in rectangle are not allowed + + 11 │ "g g g" + 12 │ "z y a"; } + > 13 │ a { grid-template-areas: "a a a" + │ + > 14 │ "b b a"; } + │ ^^^^^^^ + 15 │ a { grid-template-areas: "a a a" + 16 │ "a . a"; } + + 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 a"; } + > 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..18dc43d94490 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css @@ -0,0 +1,11 @@ +a { grid-template-areas: "a a a" + "b b b"; } +a { grid-template-areas: "a a a" + "a a a"; } +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"; } diff --git a/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css.snap.new b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css.snap.new new file mode 100644 index 000000000000..5d197137f1dd --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/useConsistentGridAreas/valid.css.snap.new @@ -0,0 +1,20 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +assertion_line: 83 +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"; } +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"; } + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index d2b65a69d4a7..c1f30981fed9 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -144,6 +144,7 @@ define_categories! { "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", "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/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause", "lint/nursery/useErrorMessage": "https://biomejs.dev/linter/rules/use-error-message", "lint/nursery/useExplicitLengthCheck": "https://biomejs.dev/linter/rules/use-explicit-length-check", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 1f045cc55454..fb163ad55b8b 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1077,6 +1077,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; /** * Require the default clause in switch statements. */ @@ -2309,6 +2313,7 @@ export type Category = | "lint/nursery/useArrayLiterals" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useConsistentBuiltinInstantiation" + | "lint/nursery/useConsistentGridAreas" | "lint/nursery/useDefaultSwitchClause" | "lint/nursery/useErrorMessage" | "lint/nursery/useExplicitLengthCheck" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 9134d738d172..c69ce159fcc6 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1821,6 +1821,13 @@ { "type": "null" } ] }, + "useConsistentGridAreas": { + "description": "Disallowing invalid named grid areas in CSS Grid Layouts.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useDefaultSwitchClause": { "description": "Require the default clause in switch statements.", "anyOf": [