diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index 8d498c88eb59..6dd240c3d2b1 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2857,6 +2857,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>, @@ -2978,6 +2981,7 @@ impl Nursery { "noMisplacedAssertion", "noReactSpecificProps", "noRestrictedImports", + "noShorthandPropertyOverrides", "noUndeclaredDependencies", "noUnknownFunction", "noUnknownMediaFeatureName", @@ -3018,6 +3022,7 @@ impl Nursery { "noImportantInKeyframe", "noInvalidPositionAtImportRule", "noLabelWithoutControl", + "noShorthandPropertyOverrides", "noUnknownFunction", "noUnknownPseudoClassSelector", "noUnknownSelectorPseudoElement", @@ -3038,14 +3043,15 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), 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[16]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), 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[33]), + 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[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]), @@ -3091,6 +3097,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 { @@ -3182,146 +3189,151 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - 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[15])); } } - 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[16])); } } - 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[17])); } } - 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[18])); } } - if let Some(rule) = self.no_unknown_pseudo_class_selector.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[19])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class_selector.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_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[21])); } } - 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[22])); } } - if let Some(rule) = self.no_unused_function_parameters.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[23])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unused_function_parameters.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_consistent_builtin_instantiation.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_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[29])); } } - 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[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> { @@ -3401,146 +3413,151 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - 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[15])); } } - 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[16])); } } - 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[17])); } } - 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[18])); } } - if let Some(rule) = self.no_unknown_pseudo_class_selector.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[19])); } } - if let Some(rule) = self.no_unknown_selector_pseudo_element.as_ref() { + if let Some(rule) = self.no_unknown_pseudo_class_selector.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_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[21])); } } - 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[22])); } } - if let Some(rule) = self.no_unused_function_parameters.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[23])); } } - if let Some(rule) = self.no_useless_string_concat.as_ref() { + if let Some(rule) = self.no_unused_function_parameters.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_consistent_builtin_instantiation.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_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[29])); } } - 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[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"] @@ -3637,6 +3654,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 fae6ddae7b6f..4b41b8dafdf6 100644 --- a/crates/biome_css_analyze/src/keywords.rs +++ b/crates/biome_css_analyze/src/keywords.rs @@ -5124,6 +5124,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; @@ -5131,7 +5440,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] @@ -5203,4 +5515,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 c76e61840661..387c775568d0 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; @@ -27,6 +28,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..7573247ebbcd --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_shorthand_property_overrides.rs @@ -0,0 +1,209 @@ +use crate::utils::{get_longhand_sub_properties, get_reset_to_initial_properties, vender_prefix}; +use biome_analyze::{ + context::RuleContext, declare_rule, AddVisitor, Phases, QueryMatch, Queryable, Rule, + RuleDiagnostic, RuleSource, ServiceBag, Visitor, VisitorContext, +}; +use biome_console::markup; +use biome_css_syntax::{AnyCssDeclarationName, CssGenericProperty, CssLanguage, CssSyntaxKind}; +use biome_rowan::{AstNode, Language, SyntaxNode, TextRange, WalkEvent}; + +fn remove_vendor_prefix(prop: &str, prefix: &str) -> String { + if let Some(prop) = prop.strip_prefix(prefix) { + return prop.to_string(); + } + + prop.to_string() +} + +fn get_override_props(property: &str) -> Vec<&str> { + let longhand_sub_props = get_longhand_sub_properties(property); + let reset_to_initial_props = get_reset_to_initial_properties(property); + + let mut merged = Vec::with_capacity(longhand_sub_props.len() + reset_to_initial_props.len()); + + let (mut i, mut j) = (0, 0); + + while i < longhand_sub_props.len() && j < reset_to_initial_props.len() { + if longhand_sub_props[i] < reset_to_initial_props[j] { + merged.push(longhand_sub_props[i]); + i += 1; + } else { + merged.push(reset_to_initial_props[j]); + j += 1; + } + } + + if i < longhand_sub_props.len() { + merged.extend_from_slice(&longhand_sub_props[i..]); + } + + if j < reset_to_initial_props.len() { + merged.extend_from_slice(&reset_to_initial_props[j..]); + } + + merged +} + +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). + /// + /// ## 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, + sources: &[RuleSource::Stylelint("declaration-block-no-shorthand-property-overrides")], + } +} + +#[derive(Default)] +struct PriorProperty { + original: String, + lowercase: String, +} + +#[derive(Default)] +struct NoDeclarationBlockShorthandPropertyOverridesVisitor { + prior_props_in_block: Vec, +} + +impl Visitor for NoDeclarationBlockShorthandPropertyOverridesVisitor { + type Language = CssLanguage; + + fn visit( + &mut self, + event: &WalkEvent>, + mut ctx: VisitorContext, + ) { + if let WalkEvent::Enter(node) = event { + match node.kind() { + CssSyntaxKind::CSS_DECLARATION_OR_RULE_BLOCK => { + self.prior_props_in_block.clear(); + } + CssSyntaxKind::CSS_GENERIC_PROPERTY => { + if let Some(prop_node) = CssGenericProperty::cast_ref(node) + .and_then(|property_node| property_node.name().ok()) + { + let prop = prop_node.text(); + let prop_lowercase = prop.to_lowercase(); + + let prop_prefix = vender_prefix(&prop_lowercase); + let unprefixed_prop = remove_vendor_prefix(&prop_lowercase, &prop_prefix); + let override_props = get_override_props(&unprefixed_prop); + + self.prior_props_in_block.iter().for_each(|prior_prop| { + let prior_prop_prefix = vender_prefix(&prior_prop.lowercase); + let unprefixed_prior_prop = + remove_vendor_prefix(&prior_prop.lowercase, &prior_prop_prefix); + + if prop_prefix == prior_prop_prefix + && override_props + .binary_search(&unprefixed_prior_prop.as_str()) + .is_ok() + { + ctx.match_query( + NoDeclarationBlockShorthandPropertyOverridesQuery { + property_node: prop_node.clone(), + override_property: prior_prop.original.clone(), + }, + ); + } + }); + + self.prior_props_in_block.push(PriorProperty { + original: prop, + lowercase: prop_lowercase, + }); + } + } + _ => {} + } + } + } +} + +#[derive(Clone)] +pub struct NoDeclarationBlockShorthandPropertyOverridesQuery { + property_node: AnyCssDeclarationName, + override_property: String, +} + +impl QueryMatch for NoDeclarationBlockShorthandPropertyOverridesQuery { + fn text_range(&self) -> TextRange { + self.property_node.range() + } +} + +impl Queryable for NoDeclarationBlockShorthandPropertyOverridesQuery { + type Input = Self; + type Language = CssLanguage; + type Output = NoDeclarationBlockShorthandPropertyOverridesQuery; + type Services = (); + + fn build_visitor( + analyzer: &mut impl AddVisitor, + _: &::Root, + ) { + analyzer.add_visitor( + Phases::Syntax, + NoDeclarationBlockShorthandPropertyOverridesVisitor::default, + ); + } + + fn unwrap_match(_: &ServiceBag, query: &Self::Input) -> Self::Output { + query.clone() + } +} + +pub struct NoDeclarationBlockShorthandPropertyOverridesState { + target_property: String, + override_property: String, + span: TextRange, +} + +impl Rule for NoShorthandPropertyOverrides { + type Query = NoDeclarationBlockShorthandPropertyOverridesQuery; + type State = NoDeclarationBlockShorthandPropertyOverridesState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let query = ctx.query(); + + Some(NoDeclarationBlockShorthandPropertyOverridesState { + target_property: query.property_node.text(), + override_property: query.override_property.clone(), + span: query.text_range(), + }) + } + + fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + rule_category!(), + state.span, + markup! { + "Unexpected shorthand property "{state.target_property}" after "{state.override_property} + }, + )) + } +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index add2b6cc33f3..68e4afd0a9bc 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 28669ff2ed82..df274dc12ba9 100644 --- a/crates/biome_css_analyze/src/utils.rs +++ b/crates/biome_css_analyze/src/utils.rs @@ -7,8 +7,10 @@ use crate::keywords::{ 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, LINGUISTIC_PSEUDO_CLASSES, - LOGICAL_COMBINATIONS_PSEUDO_CLASSES, MEDIA_FEATURE_NAMES, OTHER_PSEUDO_CLASSES, - OTHER_PSEUDO_ELEMENTS, RESOURCE_STATE_PSEUDO_CLASSES, SHADOW_TREE_PSEUDO_ELEMENTS, + LOGICAL_COMBINATIONS_PSEUDO_CLASSES, LONGHAND_SUB_PROPERTIES_OF_SHORTHAND_PROPERTIES, + MEDIA_FEATURE_NAMES, OTHER_PSEUDO_CLASSES, OTHER_PSEUDO_ELEMENTS, + RESET_TO_INITIAL_PROPERTIES_BY_BORDER, RESET_TO_INITIAL_PROPERTIES_BY_FONT, + RESOURCE_STATE_PSEUDO_CLASSES, SHADOW_TREE_PSEUDO_ELEMENTS, SHORTHAND_PROPERTIES, SYSTEM_FAMILY_NAME_KEYWORDS, VENDOR_PREFIXES, VENDOR_SPECIFIC_PSEUDO_ELEMENTS, }; use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericComponentValueList}; @@ -197,3 +199,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..65577b7a4f73 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css @@ -0,0 +1,37 @@ +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; } + +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) { a { 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 { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + +a { padding-left: 10px; padding-right: 10px; padding: 20px; } 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..f94472af5bfe --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/invalid.css.snap @@ -0,0 +1,357 @@ +--- +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; } + +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) { a { 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 { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + +a { padding-left: 10px; padding-right: 10px; padding: 20px; } + +``` + +# 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 │ a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + + +``` + +``` +invalid.css:17:49 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-top-width + + 15 │ a { PADDING-LEFT: 10PX; PADDING: 20PX; } + 16 │ + > 17 │ a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + │ ^^^^^^ + 18 │ + 19 │ a { transition-property: opacity; transition: opacity 1s linear; } + + +``` + +``` +invalid.css:19:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property transition after transition-property + + 17 │ a { border-top-width: 1px; top: 0; bottom: 3px; border: 2px solid blue; } + 18 │ + > 19 │ a { transition-property: opacity; transition: opacity 1s linear; } + │ ^^^^^^^^^^ + 20 │ + 21 │ a { background-repeat: no-repeat; background: url(lion.png); } + + +``` + +``` +invalid.css:21:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property background after background-repeat + + 19 │ a { transition-property: opacity; transition: opacity 1s linear; } + 20 │ + > 21 │ a { background-repeat: no-repeat; background: url(lion.png); } + │ ^^^^^^^^^^ + 22 │ + 23 │ @media (color) { a { background-repeat: no-repeat; background: url(lion.png); }} + + +``` + +``` +invalid.css:23:52 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property background after background-repeat + + 21 │ a { background-repeat: no-repeat; background: url(lion.png); } + 22 │ + > 23 │ @media (color) { a { background-repeat: no-repeat; background: url(lion.png); }} + │ ^^^^^^^^^^ + 24 │ + 25 │ a { -webkit-transition-property: opacity; -webkit-transition: opacity 1s linear; } + + +``` + +``` +invalid.css:25:43 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property -webkit-transition after -webkit-transition-property + + 23 │ @media (color) { a { background-repeat: no-repeat; background: url(lion.png); }} + 24 │ + > 25 │ a { -webkit-transition-property: opacity; -webkit-transition: opacity 1s linear; } + │ ^^^^^^^^^^^^^^^^^^ + 26 │ + 27 │ a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } + + +``` + +``` +invalid.css:27:43 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property -webKIT-transition after -WEBKIT-transition-property + + 25 │ a { -webkit-transition-property: opacity; -webkit-transition: opacity 1s linear; } + 26 │ + > 27 │ a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } + │ ^^^^^^^^^^^^^^^^^^ + 28 │ + 29 │ a { font-variant: small-caps; font: sans-serif; } + + +``` + +``` +invalid.css:29:31 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property font after font-variant + + 27 │ a { -WEBKIT-transition-property: opacity; -webKIT-transition: opacity 1s linear; } + 28 │ + > 29 │ a { font-variant: small-caps; font: sans-serif; } + │ ^^^^ + 30 │ + 31 │ a { font-variant: all-small-caps; font: sans-serif; } + + +``` + +``` +invalid.css:31:35 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property font after font-variant + + 29 │ a { font-variant: small-caps; font: sans-serif; } + 30 │ + > 31 │ a { font-variant: all-small-caps; font: sans-serif; } + │ ^^^^ + 32 │ + 33 │ a { font-size-adjust: 0.545; font: Verdana; } + + +``` + +``` +invalid.css:33:30 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property font after font-size-adjust + + 31 │ a { font-variant: all-small-caps; font: sans-serif; } + 32 │ + > 33 │ a { font-size-adjust: 0.545; font: Verdana; } + │ ^^^^ + 34 │ + 35 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + + +``` + +``` +invalid.css:35:25 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-left + + 33 │ a { font-size-adjust: 0.545; font: Verdana; } + 34 │ + > 35 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + │ ^^^^^^^ + 36 │ + 37 │ a { padding-left: 10px; padding-right: 10px; padding: 20px; } + + +``` + +``` +invalid.css:35:60 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property border after border-width + + 33 │ a { font-size-adjust: 0.545; font: Verdana; } + 34 │ + > 35 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + │ ^^^^^^ + 36 │ + 37 │ a { padding-left: 10px; padding-right: 10px; padding: 20px; } + + +``` + +``` +invalid.css:37:46 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-left + + 35 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + 36 │ + > 37 │ a { padding-left: 10px; padding-right: 10px; padding: 20px; } + │ ^^^^^^^ + 38 │ + + +``` + +``` +invalid.css:37:46 lint/nursery/noShorthandPropertyOverrides ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected shorthand property padding after padding-right + + 35 │ a { padding-left: 10px; padding: 20px; border-width: 20px; border: 1px solid black; } + 36 │ + > 37 │ a { padding-left: 10px; padding-right: 10px; padding: 20px; } + │ ^^^^^^^ + 38 │ + + +``` 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..2fbd2d0e2bc0 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css @@ -0,0 +1,19 @@ +a { padding: 10px; } + +a { padding: 10px; padding-left: 20px; } + +@media (color) { a { padding: 10px; padding-left: 20px; }} + +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..17a5de242623 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noShorthandPropertyOverrides/valid.css.snap @@ -0,0 +1,27 @@ +--- +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) { a { padding: 10px; padding-left: 20px; }} + +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 6a6ffc747792..591c7eb5f651 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -131,6 +131,7 @@ define_categories! { "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", "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 9abd091d616c..6d2802d0efa2 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1034,6 +1034,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. */ @@ -2344,6 +2348,7 @@ export type Category = | "lint/nursery/noMissingGenericFamilyKeyword" | "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 ee9b103bf4a1..a375e7df175e 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1769,6 +1769,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": [