From 8df660d7e970572a0686bc96d44b95aa224c37ed Mon Sep 17 00:00:00 2001 From: neoki Date: Fri, 24 May 2024 00:40:18 +0900 Subject: [PATCH] feat(biome_css_analyze): add `noShorthandPropertyOverrides` rule --- .../biome_configuration/src/linter/rules.rs | 137 +++--- crates/biome_css_analyze/src/keywords.rs | 434 +++++++++++++++++- crates/biome_css_analyze/src/lint/nursery.rs | 2 + .../no_shorthand_property_overrides.rs | 133 ++++++ crates/biome_css_analyze/src/options.rs | 1 + crates/biome_css_analyze/src/utils.rs | 22 +- .../noShorthandPropertyOverrides/invalid.css | 43 ++ .../invalid.css.snap | 300 ++++++++++++ .../noShorthandPropertyOverrides/valid.css | 24 + .../valid.css.snap | 32 ++ .../src/categories.rs | 1 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 13 files changed, 1080 insertions(+), 61 deletions(-) create mode 100644 crates/biome_css_analyze/src/lint/nursery/no_shorthand_property_overrides.rs create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css.snap create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css.snap diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b993571558a8..c8f2211fff0f 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2785,6 +2785,9 @@ pub struct Nursery { #[doc = "Disallow specified modules when loaded by import or require."] #[serde(skip_serializing_if = "Option::is_none")] pub no_restricted_imports: Option>, + #[doc = "Disallow shorthand properties that override related longhand properties."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_shorthand_property_overrides: Option>, #[doc = "Disallow the use of dependencies that aren't specified in the package.json."] #[serde(skip_serializing_if = "Option::is_none")] pub no_undeclared_dependencies: Option>, @@ -2905,6 +2908,7 @@ impl Nursery { "noNodejsModules", "noReactSpecificProps", "noRestrictedImports", + "noShorthandPropertyOverrides", "noUndeclaredDependencies", "noUnknownFunction", "noUnknownMediaFeatureName", @@ -2945,6 +2949,7 @@ impl Nursery { "noFlatMapIdentity", "noImportantInKeyframe", "noInvalidPositionAtImportRule", + "noShorthandPropertyOverrides", "noUnknownFunction", "noUnknownSelectorPseudoElement", "noUnknownUnit", @@ -2965,13 +2970,14 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), 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[24]), 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]), @@ -3018,6 +3024,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 { @@ -3119,141 +3126,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_shorthand_property_overrides.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_unknown_function.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { + if let Some(rule) = self.no_unknown_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_property.as_ref() { + if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_property.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { + if let Some(rule) = self.no_unknown_unit.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { + if let Some(rule) = self.no_useless_string_concat.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.no_yoda_expression.as_ref() { + if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_yoda_expression.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_array_literals.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_array_literals.as_ref() { if rule.is_enabled() { 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_builtin_instantiation.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> { @@ -3343,141 +3355,146 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_shorthand_property_overrides.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_unknown_function.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { + if let Some(rule) = self.no_unknown_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_unknown_property.as_ref() { + if let Some(rule) = self.no_unknown_media_feature_name.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_property.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.no_unknown_unit.as_ref() { + if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { + if let Some(rule) = self.no_unknown_unit.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unmatchable_anb_selector.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { + if let Some(rule) = self.no_useless_string_concat.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.no_yoda_expression.as_ref() { + if let Some(rule) = self.no_useless_undefined_initialization.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_yoda_expression.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_array_literals.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_array_literals.as_ref() { if rule.is_disabled() { 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_builtin_instantiation.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"] @@ -3582,6 +3599,10 @@ impl Nursery { .no_restricted_imports .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noShorthandPropertyOverrides" => self + .no_shorthand_property_overrides + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUndeclaredDependencies" => self .no_undeclared_dependencies .as_ref() diff --git a/crates/biome_css_analyze/src/keywords.rs b/crates/biome_css_analyze/src/keywords.rs index bccaa3dde10b..21421d8fcfbb 100644 --- a/crates/biome_css_analyze/src/keywords.rs +++ b/crates/biome_css_analyze/src/keywords.rs @@ -5021,6 +5021,315 @@ pub const MEDIA_FEATURE_NAMES: [&str; 60] = [ "width", ]; +pub const SHORTHAND_PROPERTIES: [&str; 57] = [ + "animation", + "background", + "border", + "border-block", + "border-block-end", + "border-block-start", + "border-bottom", + "border-color", + "border-image", + "border-inline", + "border-inline-end", + "border-inline-start", + "border-left", + "border-radius", + "border-right", + "border-style", + "border-top", + "border-width", + "column-rule", + "columns", + "flex", + "flex-flow", + "font", + "font-synthesis", + "gap", + "grid", + "grid-area", + "grid-column", + "grid-gap", + "grid-row", + "grid-template", + "inset", + "inset-block", + "inset-inline", + "list-style", + "margin", + "margin-block", + "margin-inline", + "mask", + "outline", + "overflow", + "overscroll-behavior", + "padding", + "padding-block", + "padding-inline", + "place-content", + "place-items", + "place-self", + "scroll-margin", + "scroll-margin-block", + "scroll-margin-inline", + "scroll-padding", + "scroll-padding-block", + "scroll-padding-inline", + "text-decoration", + "text-emphasis", + "transition", +]; + +pub const LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES: [&[&str]; 57] = [ + &[ + "animation-delay", + "animation-direction", + "animation-duration", + "animation-fill-mode", + "animation-iteration-count", + "animation-name", + "animation-play-state", + "animation-timing-function", + ], + &[ + "background-attachment", + "background-clip", + "background-color", + "background-image", + "background-origin", + "background-position", + "background-repeat", + "background-size", + ], + &[ + "border-bottom-color", + "border-bottom-style", + "border-bottom-width", + "border-color", + "border-left-color", + "border-left-style", + "border-left-width", + "border-right-color", + "border-right-style", + "border-right-width", + "border-style", + "border-top-color", + "border-top-style", + "border-top-width", + "border-width", + ], + &[ + "border-block-color", + "border-block-style", + "border-block-width", + ], + &[ + "border-block-end-color", + "border-block-end-style", + "border-block-end-width", + ], + &[ + "border-block-start-color", + "border-block-start-style", + "border-block-start-width", + ], + &[ + "border-bottom-color", + "border-bottom-style", + "border-bottom-width", + ], + &[ + "border-bottom-color", + "border-left-color", + "border-right-color", + "border-top-color", + ], + &[ + "border-image-outset", + "border-image-repeat", + "border-image-slice", + "border-image-source", + "border-image-width", + ], + &[ + "border-inline-color", + "border-inline-style", + "border-inline-width", + ], + &[ + "border-inline-end-color", + "border-inline-end-style", + "border-inline-end-width", + ], + &[ + "border-inline-start-color", + "border-inline-start-style", + "border-inline-start-width", + ], + &[ + "border-left-color", + "border-left-style", + "border-left-width", + ], + &[ + "border-bottom-left-radius", + "border-bottom-right-radius", + "border-top-left-radius", + "border-top-right-radius", + ], + &[ + "border-right-color", + "border-right-style", + "border-right-width", + ], + &[ + "border-bottom-style", + "border-left-style", + "border-right-style", + "border-top-style", + ], + &["border-top-color", "border-top-style", "border-top-width"], + &[ + "border-bottom-width", + "border-left-width", + "border-right-width", + "border-top-width", + ], + &[ + "column-rule-color", + "column-rule-style", + "column-rule-width", + ], + &["column-count", "column-width"], + &["flex-basis", "flex-grow", "flex-shrink"], + &["flex-direction", "flex-wrap"], + &[ + "font-family", + "font-size", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "line-height", + ], + &[ + "font-synthesis-small-caps", + "font-synthesis-style", + "font-synthesis-weight", + ], + &["column-gap", "row-gap"], + &[ + "grid-auto-columns", + "grid-auto-flow", + "grid-auto-rows", + "grid-column-gap", + "grid-row-gap", + "grid-template-areas", + "grid-template-columns", + "grid-template-rows", + ], + &[ + "grid-column-end", + "grid-column-start", + "grid-row-end", + "grid-row-start", + ], + &["grid-column-end", "grid-column-start"], + &["grid-column-gap", "grid-row-gap"], + &["grid-row-end", "grid-row-start"], + &[ + "grid-template-areas", + "grid-template-columns", + "grid-template-rows", + ], + &["bottom", "left", "right", "top"], + &["inset-block-end", "inset-block-start"], + &["inset-inline-end", "inset-inline-start"], + &["list-style-image", "list-style-position", "list-style-type"], + &["margin-bottom", "margin-left", "margin-right", "margin-top"], + &["margin-block-end", "margin-block-start"], + &["margin-inline-end", "margin-inline-start"], + &[ + "mask-clip", + "mask-composite", + "mask-image", + "mask-mode", + "mask-origin", + "mask-position", + "mask-repeat", + "mask-size", + ], + &["outline-color", "outline-style", "outline-width"], + &["overflow-x", "overflow-y"], + &["overscroll-behavior-x", "overscroll-behavior-y"], + &[ + "padding-bottom", + "padding-left", + "padding-right", + "padding-top", + ], + &["padding-block-end", "padding-block-start"], + &["padding-inline-end", "padding-inline-start"], + &["align-content", "justify-content"], + &["align-items", "justify-items"], + &["align-self", "justify-self"], + &[ + "scroll-margin-bottom", + "scroll-margin-left", + "scroll-margin-right", + "scroll-margin-top", + ], + &["scroll-margin-block-end", "scroll-margin-block-start"], + &["scroll-margin-inline-end", "scroll-margin-inline-start"], + &[ + "scroll-padding-bottom", + "scroll-padding-left", + "scroll-padding-right", + "scroll-padding-top", + ], + &["scroll-padding-block-end", "scroll-padding-block-start"], + &["scroll-padding-inline-end", "scroll-padding-inline-start"], + &[ + "text-decoration-color", + "text-decoration-line", + "text-decoration-style", + "text-decoration-thickness", + ], + &["text-emphasis-color", "text-emphasis-style"], + &[ + "transition-delay", + "transition-duration", + "transition-property", + "transition-timing-function", + ], +]; + +pub const RESET_TO_INITIAL_PROPERTIES_BY_BORDER: [&str; 6] = [ + "border-image", + "border-image-outset", + "border-image-repeat", + "border-image-slice", + "border-image-source", + "border-image-width", +]; + +pub const RESET_TO_INITIAL_PROPERTIES_BY_FONT: [&str; 13] = [ + "font-feature-settings", + "font-kerning", + "font-language-override", + "font-optical-sizing", + "font-size-adjust", + "font-variant-alternates", + "font-variant-caps", + "font-variant-east-asian", + "font-variant-emoji", + "font-variant-ligatures", + "font-variant-numeric", + "font-variant-position", + "font-variation-settings", +]; + #[cfg(test)] mod tests { use std::collections::HashSet; @@ -5028,7 +5337,10 @@ mod tests { use super::{ FUNCTION_KEYWORDS, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES, KNOWN_SAFARI_PROPERTIES, - KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, MEDIA_FEATURE_NAMES, + KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, + LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, MEDIA_FEATURE_NAMES, + RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT, + SHORTHAND_PROPERTIES, }; #[test] @@ -5100,4 +5412,124 @@ mod tests { assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } } + + #[test] + fn test_shorthand_properties_sorted() { + let mut sorted = SHORTHAND_PROPERTIES.to_vec(); + sorted.sort_unstable(); + assert_eq!(SHORTHAND_PROPERTIES, sorted.as_slice()); + } + + #[test] + fn test_shorthand_properties_unique() { + let mut set = HashSet::new(); + let has_duplicates = SHORTHAND_PROPERTIES.iter().any(|&x| !set.insert(x)); + assert!(!has_duplicates); + } + + #[test] + fn test_longhand_sub_properties_of_shorthand_properties_sorted() { + for longhand_sub_properties in LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES.iter() { + let mut sorted = longhand_sub_properties.to_vec(); + sorted.sort_unstable(); + assert_eq!(*longhand_sub_properties, sorted.as_slice()); + } + } + + #[test] + fn test_longhand_sub_properties_of_shorthand_properties_unique() { + for longhand_sub_properties in LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES.iter() { + let mut set = HashSet::new(); + let has_duplicates = longhand_sub_properties.iter().any(|&x| !set.insert(x)); + assert!(!has_duplicates); + } + } + + #[test] + fn test_shorthand_properties_and_longhand_sub_properties_correspond_correctly() { + for (shorthand_property, longhand_sub_properties) in SHORTHAND_PROPERTIES + .iter() + .zip(LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES.iter()) + { + for longhand_sub_property in longhand_sub_properties.iter() { + if [ + "border-color", + "border-radius", + "border-style", + "border-width", + ] + .contains(shorthand_property) + { + let (start, end) = shorthand_property.split_at(6); + assert!(longhand_sub_property.starts_with(start)); + assert!(longhand_sub_property.ends_with(end)); + } else if *shorthand_property == "columns" { + assert!(longhand_sub_property.starts_with("column")); + } else if *shorthand_property == "flex-flow" { + assert!(["flex-direction", "flex-wrap",].contains(longhand_sub_property)); + } else if *shorthand_property == "font" { + if *longhand_sub_property != "line-height" { + assert!(longhand_sub_property.starts_with(shorthand_property)); + } + } else if *shorthand_property == "gap" { + assert!(longhand_sub_property.ends_with(shorthand_property)); + } else if *shorthand_property == "grid-area" { + assert!( + longhand_sub_property.starts_with("grid-row") + || longhand_sub_property.starts_with("grid-column") + ); + } else if *shorthand_property == "grid-gap" { + let (start, end) = shorthand_property.split_at(4); + assert!(longhand_sub_property.starts_with(start)); + assert!(longhand_sub_property.ends_with(end)); + } else if *shorthand_property == "inset" { + assert!(["bottom", "left", "right", "top"].contains(longhand_sub_property)); + } else if ["place-content", "place-items", "place-self"] + .contains(shorthand_property) + { + assert!( + longhand_sub_property.starts_with("align") + || longhand_sub_property.starts_with("justify") + ); + + let (_, end) = shorthand_property.split_at(5); + assert!(longhand_sub_property.ends_with(end)); + } else { + assert!(longhand_sub_property.starts_with(shorthand_property)); + } + } + } + } + + #[test] + fn test_reset_to_initial_properties_by_border_sorted() { + let mut sorted = RESET_TO_INITIAL_PROPERTIES_BY_BORDER.to_vec(); + sorted.sort_unstable(); + assert_eq!(RESET_TO_INITIAL_PROPERTIES_BY_BORDER, sorted.as_slice()); + } + + #[test] + fn test_reset_to_initial_properties_by_border_unique() { + let mut set = HashSet::new(); + let has_duplicates = RESET_TO_INITIAL_PROPERTIES_BY_BORDER + .iter() + .any(|&x| !set.insert(x)); + assert!(!has_duplicates); + } + + #[test] + fn test_reset_to_initial_properties_by_font_sorted() { + let mut sorted = RESET_TO_INITIAL_PROPERTIES_BY_FONT.to_vec(); + sorted.sort_unstable(); + assert_eq!(RESET_TO_INITIAL_PROPERTIES_BY_FONT, sorted.as_slice()); + } + + #[test] + fn test_reset_to_initial_properties_by_font_unique() { + let mut set = HashSet::new(); + let has_duplicates = RESET_TO_INITIAL_PROPERTIES_BY_FONT + .iter() + .any(|&x| !set.insert(x)); + assert!(!has_duplicates); + } } diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 177f67240b5e..6581b97b8753 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -8,6 +8,7 @@ pub mod no_duplicate_selectors_keyframe_block; pub mod no_empty_block; pub mod no_important_in_keyframe; pub mod no_invalid_position_at_import_rule; +pub mod no_shorthand_property_overrides; pub mod no_unknown_function; pub mod no_unknown_media_feature_name; pub mod no_unknown_property; @@ -26,6 +27,7 @@ declare_group! { self :: no_empty_block :: NoEmptyBlock , self :: no_important_in_keyframe :: NoImportantInKeyframe , self :: no_invalid_position_at_import_rule :: NoInvalidPositionAtImportRule , + self :: no_shorthand_property_overrides :: NoShorthandPropertyOverrides , self :: no_unknown_function :: NoUnknownFunction , self :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName , self :: no_unknown_property :: NoUnknownProperty , diff --git a/crates/biome_css_analyze/src/lint/nursery/no_shorthand_property_overrides.rs b/crates/biome_css_analyze/src/lint/nursery/no_shorthand_property_overrides.rs new file mode 100644 index 000000000000..5c815b1e1125 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_shorthand_property_overrides.rs @@ -0,0 +1,133 @@ +use std::collections::HashSet; + +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; +use biome_console::markup; +use biome_css_syntax::{CssDeclarationOrRuleList, CssGenericProperty, CssSyntaxKind}; +use biome_rowan::{AstNode, SyntaxNodeCast, TextRange}; + +use crate::utils::{get_longhand_sub_properties, get_reset_to_initial_properties}; + +declare_rule! { + /// Disallow shorthand properties that override related longhand properties. + /// + /// For details on shorthand properties, see the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties). + /// + /// + /// This rule ignores: + /// + /// - vendor-prefixed properties (e.g., `-webkit-transition`) + /// + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// a { padding-left: 10px; padding: 20px; } + /// ``` + /// + /// ### Valid + /// + /// ```css + /// a { padding: 10px; padding-left: 20px; } + /// ``` + /// + /// ```css + /// a { transition-property: opacity; } a { transition: opacity 1s linear; } + /// ``` + /// + pub NoShorthandPropertyOverrides { + version: "next", + name: "noShorthandPropertyOverrides", + language: "css", + recommended: true, + } +} + +fn get_css_declaration_list(property: &CssGenericProperty) -> Option { + for ancestor in property.syntax().ancestors() { + if matches!(ancestor.kind(), CssSyntaxKind::CSS_DECLARATION_OR_RULE_LIST) { + return Some(ancestor.cast::()?); + } + } + + return None; +} + +fn get_prior_property_names_in_block( + target_property: &CssGenericProperty, +) -> Option> { + let declaration_list = get_css_declaration_list(target_property)?; + + let mut prior_declarations = HashSet::new(); + for declaration in declaration_list { + if let Some(declaration) = declaration.as_css_declaration_with_semicolon() { + let current_property = declaration.declaration().ok()?.property().ok()?; + + let current_property = + if let Some(current_property) = current_property.as_css_generic_property() { + current_property + } else { + continue; + }; + + if current_property == target_property { + break; + } + + let current_property_name = current_property.name().ok()?.text().to_lowercase(); + prior_declarations.insert(current_property_name); + } + } + + Some(prior_declarations) +} + +pub struct NoDeclarationBlockShorthandPropertyOverridesState { + target_property_name: String, + override_property_name: String, + span: TextRange, +} + +impl Rule for NoShorthandPropertyOverrides { + type Query = Ast; + type State = NoDeclarationBlockShorthandPropertyOverridesState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node = ctx.query(); + dbg!(node); + + let target_property_name_node = node.name().ok()?; + let target_property_name = target_property_name_node.text().to_lowercase(); + + let prior_property_names = get_prior_property_names_in_block(node)?; + let longhand_sub_properties = get_longhand_sub_properties(&target_property_name); + let reset_to_initial_properties = get_reset_to_initial_properties(&target_property_name); + + for prior_property_name in prior_property_names { + if longhand_sub_properties.contains(&prior_property_name.as_str()) + || reset_to_initial_properties.contains(&prior_property_name.as_str()) + { + return Some(NoDeclarationBlockShorthandPropertyOverridesState { + target_property_name, + override_property_name: prior_property_name, + span: target_property_name_node.range(), + }); + } + } + + None + } + + fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + rule_category!(), + state.span, + markup! { + "Unexpected shorthand property "{state.target_property_name}" after "{state.override_property_name} + }, + )) + } +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 544a93840219..30bd78f9fb21 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -10,6 +10,7 @@ pub type NoEmptyBlock = ::Options; pub type NoImportantInKeyframe = < lint :: nursery :: no_important_in_keyframe :: NoImportantInKeyframe as biome_analyze :: Rule > :: Options ; pub type NoInvalidPositionAtImportRule = < lint :: nursery :: no_invalid_position_at_import_rule :: NoInvalidPositionAtImportRule as biome_analyze :: Rule > :: Options ; +pub type NoShorthandPropertyOverrides = < lint :: nursery :: no_shorthand_property_overrides :: NoShorthandPropertyOverrides as biome_analyze :: Rule > :: Options ; pub type NoUnknownFunction = ::Options; pub type NoUnknownMediaFeatureName = < lint :: nursery :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_css_analyze/src/utils.rs b/crates/biome_css_analyze/src/utils.rs index 77c26358b956..a728c88a0129 100644 --- a/crates/biome_css_analyze/src/utils.rs +++ b/crates/biome_css_analyze/src/utils.rs @@ -4,8 +4,10 @@ use crate::keywords::{ FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, KNOWN_CHROME_PROPERTIES, KNOWN_EDGE_PROPERTIES, KNOWN_EXPLORER_PROPERTIES, KNOWN_FIREFOX_PROPERTIES, KNOWN_PROPERTIES, KNOWN_SAFARI_PROPERTIES, KNOWN_SAMSUNG_INTERNET_PROPERTIES, KNOWN_US_BROWSER_PROPERTIES, - LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS, MEDIA_FEATURE_NAMES, - OTHER_PSEUDO_ELEMENTS, SHADOW_TREE_PSEUDO_ELEMENTS, SYSTEM_FAMILY_NAME_KEYWORDS, + LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, LINE_HEIGHT_KEYWORDS, + LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, MEDIA_FEATURE_NAMES, OTHER_PSEUDO_ELEMENTS, + RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT, + SHADOW_TREE_PSEUDO_ELEMENTS, SHORTHAND_PROPERTIES, SYSTEM_FAMILY_NAME_KEYWORDS, VENDOR_PREFIXES, VENDOR_SPECIFIC_PSEUDO_ELEMENTS, }; use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericComponentValueList}; @@ -174,3 +176,19 @@ pub fn is_media_feature_name(prop: &str) -> bool { } false } + +pub fn get_longhand_sub_properties(shorthand_property: &str) -> &'static [&'static str] { + if let Ok(index) = SHORTHAND_PROPERTIES.binary_search(&shorthand_property) { + return LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES[index]; + } + + &[] +} + +pub fn get_reset_to_initial_properties(shorthand_property: &str) -> &'static [&'static str] { + match shorthand_property { + "border" => &RESET_TO_INITIAL_PROPERTIES_BY_BORDER, + "font" => &RESET_TO_INITIAL_PROPERTIES_BY_FONT, + _ => &[], + } +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css new file mode 100644 index 000000000000..5b797c5cd393 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css @@ -0,0 +1,43 @@ +a { padding-left: 10px; padding: 20px; } + +a { border-width: 20px; border: 1px solid black; } + +a { border-color: red; border: 1px solid black; } + +a { border-style: dotted; border: 1px solid black; } + +a { border-image: url("foo.png"); border: 1px solid black; } + +a { border-image-source: url("foo.png"); border: 1px solid black; } + +a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; } + +a { PADDING-LEFT: 10PX; PADDING: 20PX; } + +/* Overriding within nested rule */ +a { padding-left: 10px; { b { padding-top: 10px; padding: 20px; }}} + +a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + +a { transition-property: opacity; transition: opacity 1s linear; } + +a { background-repeat: no-repeat; background: url(lion.png); } + +/* @media (color) { background-repeat: no-repeat; background: url(lion.png); } */ + +/* a { @media (color) { background-repeat: no-repeat; background: url(lion.png); }} */ + +/* a { -webkit-transition-property: opacity; -webkit-transition: opacity 1s linear; } */ + +/* a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } */ + +a { font-variant: small-caps; font: sans-serif; } + +a { font-variant: all-small-caps; font: sans-serif; } + +a { font-size-adjust: 0.545; font: Verdana; } + +/* a { font-variant-caps: small-caps; font-variant: normal; } */ + +/* Multiple shorthand property overrides */ +a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } diff --git a/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css.snap new file mode 100644 index 000000000000..a9e1f3763404 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css.snap @@ -0,0 +1,300 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +a { padding-left: 10px; padding: 20px; } + +a { border-width: 20px; border: 1px solid black; } + +a { border-color: red; border: 1px solid black; } + +a { border-style: dotted; border: 1px solid black; } + +a { border-image: url("foo.png"); border: 1px solid black; } + +a { border-image-source: url("foo.png"); border: 1px solid black; } + +a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; } + +a { PADDING-LEFT: 10PX; PADDING: 20PX; } + +/* Overriding within nested rule */ +a { padding-left: 10px; { b { padding-top: 10px; padding: 20px; }}} + +a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + +a { transition-property: opacity; transition: opacity 1s linear; } + +a { background-repeat: no-repeat; background: url(lion.png); } + +/* @media (color) { background-repeat: no-repeat; background: url(lion.png); } */ + +/* a { @media (color) { background-repeat: no-repeat; background: url(lion.png); }} */ + +/* a { -webkit-transition-property: opacity; -webkit-transition: opacity 1s linear; } */ + +/* a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } */ + +a { font-variant: small-caps; font: sans-serif; } + +a { font-variant: all-small-caps; font: sans-serif; } + +a { font-size-adjust: 0.545; font: Verdana; } + +/* a { font-variant-caps: small-caps; font-variant: normal; } */ + +/* Multiple shorthand property overrides */ +a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + +``` + +# Diagnostics +``` +invalid.css:1:25 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-left + + > 1 │ a { padding-left: 10px; padding: 20px; } + │ ^^^^^^^ + 2 │ + 3 │ a { border-width: 20px; border: 1px solid black; } + + +``` + +``` +invalid.css:3:25 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-width + + 1 │ a { padding-left: 10px; padding: 20px; } + 2 │ + > 3 │ a { border-width: 20px; border: 1px solid black; } + │ ^^^^^^ + 4 │ + 5 │ a { border-color: red; border: 1px solid black; } + + +``` + +``` +invalid.css:5:24 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-color + + 3 │ a { border-width: 20px; border: 1px solid black; } + 4 │ + > 5 │ a { border-color: red; border: 1px solid black; } + │ ^^^^^^ + 6 │ + 7 │ a { border-style: dotted; border: 1px solid black; } + + +``` + +``` +invalid.css:7:27 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-style + + 5 │ a { border-color: red; border: 1px solid black; } + 6 │ + > 7 │ a { border-style: dotted; border: 1px solid black; } + │ ^^^^^^ + 8 │ + 9 │ a { border-image: url("foo.png"); border: 1px solid black; } + + +``` + +``` +invalid.css:9:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-image + + 7 │ a { border-style: dotted; border: 1px solid black; } + 8 │ + > 9 │ a { border-image: url("foo.png"); border: 1px solid black; } + │ ^^^^^^ + 10 │ + 11 │ a { border-image-source: url("foo.png"); border: 1px solid black; } + + +``` + +``` +invalid.css:11:42 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-image-source + + 9 │ a { border-image: url("foo.png"); border: 1px solid black; } + 10 │ + > 11 │ a { border-image-source: url("foo.png"); border: 1px solid black; } + │ ^^^^^^ + 12 │ + 13 │ a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; } + + +``` + +``` +invalid.css:13:25 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-left + + 11 │ a { border-image-source: url("foo.png"); border: 1px solid black; } + 12 │ + > 13 │ a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; } + │ ^^^^^^^ + 14 │ + 15 │ a { PADDING-LEFT: 10PX; PADDING: 20PX; } + + +``` + +``` +invalid.css:15:25 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-left + + 13 │ a { pAdDiNg-lEfT: 10Px; pAdDiNg: 20Px; } + 14 │ + > 15 │ a { PADDING-LEFT: 10PX; PADDING: 20PX; } + │ ^^^^^^^ + 16 │ + 17 │ /* Overriding within nested rule */ + + +``` + +``` +invalid.css:18:50 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-top + + 17 │ /* Overriding within nested rule */ + > 18 │ a { padding-left: 10px; { b { padding-top: 10px; padding: 20px; }}} + │ ^^^^^^^ + 19 │ + 20 │ a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + + +``` + +``` +invalid.css:20:49 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-top-width + + 18 │ a { padding-left: 10px; { b { padding-top: 10px; padding: 20px; }}} + 19 │ + > 20 │ a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + │ ^^^^^^ + 21 │ + 22 │ a { transition-property: opacity; transition: opacity 1s linear; } + + +``` + +``` +invalid.css:22:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property transition after transition-property + + 20 │ a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + 21 │ + > 22 │ a { transition-property: opacity; transition: opacity 1s linear; } + │ ^^^^^^^^^^ + 23 │ + 24 │ a { background-repeat: no-repeat; background: url(lion.png); } + + +``` + +``` +invalid.css:24:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property background after background-repeat + + 22 │ a { transition-property: opacity; transition: opacity 1s linear; } + 23 │ + > 24 │ a { background-repeat: no-repeat; background: url(lion.png); } + │ ^^^^^^^^^^ + 25 │ + 26 │ /* @media (color) { background-repeat: no-repeat; background: url(lion.png); } */ + + +``` + +``` +invalid.css:34:31 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property font after font-variant + + 32 │ /* a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } */ + 33 │ + > 34 │ a { font-variant: small-caps; font: sans-serif; } + │ ^^^^ + 35 │ + 36 │ a { font-variant: all-small-caps; font: sans-serif; } + + +``` + +``` +invalid.css:36:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property font after font-variant + + 34 │ a { font-variant: small-caps; font: sans-serif; } + 35 │ + > 36 │ a { font-variant: all-small-caps; font: sans-serif; } + │ ^^^^ + 37 │ + 38 │ a { font-size-adjust: 0.545; font: Verdana; } + + +``` + +``` +invalid.css:38:30 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property font after font-size-adjust + + 36 │ a { font-variant: all-small-caps; font: sans-serif; } + 37 │ + > 38 │ a { font-size-adjust: 0.545; font: Verdana; } + │ ^^^^ + 39 │ + 40 │ /* a { font-variant-caps: small-caps; font-variant: normal; } */ + + +``` + +``` +invalid.css:43:25 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-left + + 42 │ /* Multiple shorthand property overrides */ + > 43 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + │ ^^^^^^^ + 44 │ + + +``` + +``` +invalid.css:43:60 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-width + + 42 │ /* Multiple shorthand property overrides */ + > 43 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + │ ^^^^^^ + 44 │ + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css new file mode 100644 index 000000000000..21e9488ab56a --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css @@ -0,0 +1,24 @@ +a { padding: 10px; } + +a { padding: 10px; padding-left: 20px; } + +/* @media (color) { padding: 10px; padding-left: 20px; } + +a { @media (color) { padding: 10px; padding-left: 20px; }} */ + +a { padding-left: 10px; { b { padding: 20px; }}} + +/* Nested related properties */ +a { border-top-width: 1px; top: 0; bottom: 3px; border-bottom: 2px solid blue; } + +a { transition-property: opacity; } a { transition: opacity 1s linear; } + +a { -webkit-transition-property: opacity; transition: opacity 1s linear; } + +a { transition-property: opacity; -webkit-transition: opacity 1s linear; } + +a { border-block: 1px solid; border: 20px dashed black; } + +a { border-block-end: 1px solid; border: 20px dashed black; } + +a { border-block-start: 1px solid; border: 20px dashed black; } diff --git a/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css.snap new file mode 100644 index 000000000000..97967f757b06 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css.snap @@ -0,0 +1,32 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +a { padding: 10px; } + +a { padding: 10px; padding-left: 20px; } + +/* @media (color) { padding: 10px; padding-left: 20px; } + +a { @media (color) { padding: 10px; padding-left: 20px; }} */ + +a { padding-left: 10px; { b { padding: 20px; }}} + +/* Nested related properties */ +a { border-top-width: 1px; top: 0; bottom: 3px; border-bottom: 2px solid blue; } + +a { transition-property: opacity; } a { transition: opacity 1s linear; } + +a { -webkit-transition-property: opacity; transition: opacity 1s linear; } + +a { transition-property: opacity; -webkit-transition: opacity 1s linear; } + +a { border-block: 1px solid; border: 20px dashed black; } + +a { border-block-end: 1px solid; border: 20px dashed black; } + +a { border-block-start: 1px solid; border: 20px dashed black; } + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 9aca2a8f794e..c22fc7014bba 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -129,6 +129,7 @@ define_categories! { "lint/nursery/noNodejsModules": "https://biomejs.dev/linter/rules/no-nodejs-modules", "lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props", "lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports", + "lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides", "lint/nursery/noTypeOnlyImportAttributes": "https://biomejs.dev/linter/rules/no-type-only-import-attributes", "lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies", "lint/nursery/noUnknownFunction": "https://biomejs.dev/linter/rules/no-unknown-function", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index f7eac08f17f9..9a1bf482a7e5 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1025,6 +1025,10 @@ export interface Nursery { * Disallow specified modules when loaded by import or require. */ noRestrictedImports?: RuleConfiguration_for_RestrictedImportsOptions; + /** + * Disallow shorthand properties that override related longhand properties. + */ + noShorthandPropertyOverrides?: RuleConfiguration_for_Null; /** * Disallow the use of dependencies that aren't specified in the package.json. */ @@ -2302,6 +2306,7 @@ export type Category = | "lint/nursery/noNodejsModules" | "lint/nursery/noReactSpecificProps" | "lint/nursery/noRestrictedImports" + | "lint/nursery/noShorthandPropertyOverrides" | "lint/nursery/noTypeOnlyImportAttributes" | "lint/nursery/noUndeclaredDependencies" | "lint/nursery/noUnknownFunction" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index e8a6cb31b31b..41d38d804c0c 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1730,6 +1730,13 @@ { "type": "null" } ] }, + "noShorthandPropertyOverrides": { + "description": "Disallow shorthand properties that override related longhand properties.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUndeclaredDependencies": { "description": "Disallow the use of dependencies that aren't specified in the package.json.", "anyOf": [