From 3229b32515c7791e00117eafaad5fe0a1e0621e3 Mon Sep 17 00:00:00 2001 From: Timon Jurschitsch <103483059+DerTimonius@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:40:21 +0200 Subject: [PATCH] feat(biome_css_analyze): implement noIrregularWhitespaceCss (#3428) --- CHANGELOG.md | 1 + .../biome_configuration/src/linter/rules.rs | 203 +++++---- crates/biome_css_analyze/src/lint/nursery.rs | 2 + .../nursery/no_irregular_whitespace_css.rs | 98 +++++ crates/biome_css_analyze/src/options.rs | 1 + .../noIrregularWhitespaceCss/invalid.css | 97 +++++ .../noIrregularWhitespaceCss/invalid.css.snap | 411 ++++++++++++++++++ .../noIrregularWhitespaceCss/valid.css | 10 + .../noIrregularWhitespaceCss/valid.css.snap | 18 + .../src/categories.rs | 1 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 12 files changed, 762 insertions(+), 92 deletions(-) create mode 100644 crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace_css.rs create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css.snap create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css create mode 100644 crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 86ba6618d143..ec1978e862a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b Contributed by @Conaclos - Add [nursery/noIrregularWhitespace](https://biomejs.dev/linter/rules/no-irregular-whitespace). Contributed by @michellocana +- Implement `noIrreguluarWhitespace` for CSS. Contributed by @DerTimonius - Add [nursery/useTrimStartEnd](https://biomejs.dev/linter/rules/use-trim-start-end/). Contributed by @chansuke #### Enhancements diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index bd94353dd757..83a926d9e9ab 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2863,6 +2863,9 @@ pub struct Nursery { #[doc = "Disallows the use of irregular whitespace characters."] #[serde(skip_serializing_if = "Option::is_none")] pub no_irregular_whitespace: Option>, + #[doc = "Disallows the use of irregular whitespace."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_irregular_whitespace_css: Option>, #[doc = "Enforce that a label element or component has a text label and an associated input."] #[serde(skip_serializing_if = "Option::is_none")] pub no_label_without_control: Option>, @@ -3027,6 +3030,7 @@ impl Nursery { "noInvalidDirectionInLinearGradient", "noInvalidPositionAtImportRule", "noIrregularWhitespace", + "noIrregularWhitespaceCss", "noLabelWithoutControl", "noMisplacedAssertion", "noReactSpecificProps", @@ -3108,19 +3112,19 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3181,6 +3185,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3277,216 +3282,221 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_label_without_control.as_ref() { + if let Some(rule) = self.no_irregular_whitespace_css.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_label_without_control.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_react_specific_props.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_react_specific_props.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_shorthand_property_overrides.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_static_element_interactions.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[21])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - 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[24])); } } - 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[25])); } } - 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[26])); } } - 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[27])); } } - 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[28])); } } - 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[29])); } } - 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[30])); } } - 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[31])); } } - 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[32])); } } - 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[33])); } } - if let Some(rule) = self.no_value_at_rule.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[34])); } } - if let Some(rule) = self.no_yoda_expression.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - 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[36])); } } - 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[37])); } } - if let Some(rule) = self.use_consistent_curly_braces.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[38])); } } - if let Some(rule) = self.use_consistent_grid_areas.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_date_now.as_ref() { + if let Some(rule) = self.use_consistent_grid_areas.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - 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[41])); } } - if let Some(rule) = self.use_deprecated_reason.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[42])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - 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[44])); } } - 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[45])); } } - 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[46])); } } - 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[47])); } } - 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[48])); } } - 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[49])); } } - 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[50])); } } - 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[51])); } } - if let Some(rule) = self.use_strict_mode.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[52])); } } - if let Some(rule) = self.use_throw_new_error.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - 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[54])); } } - 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[55])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + 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[56])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3571,216 +3581,221 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_label_without_control.as_ref() { + if let Some(rule) = self.no_irregular_whitespace_css.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_label_without_control.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_react_specific_props.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_react_specific_props.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_shorthand_property_overrides.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_static_element_interactions.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[21])); } } - if let Some(rule) = self.no_substr.as_ref() { + if let Some(rule) = self.no_static_element_interactions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_substr.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - 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[24])); } } - 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[25])); } } - 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[26])); } } - 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[27])); } } - 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[28])); } } - 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[29])); } } - 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[30])); } } - 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[31])); } } - 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[32])); } } - 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[33])); } } - if let Some(rule) = self.no_value_at_rule.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[34])); } } - if let Some(rule) = self.no_yoda_expression.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - 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[36])); } } - 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[37])); } } - if let Some(rule) = self.use_consistent_curly_braces.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[38])); } } - if let Some(rule) = self.use_consistent_grid_areas.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_date_now.as_ref() { + if let Some(rule) = self.use_consistent_grid_areas.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - 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[41])); } } - if let Some(rule) = self.use_deprecated_reason.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[42])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - 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[44])); } } - 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[45])); } } - 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[46])); } } - 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[47])); } } - 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[48])); } } - 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[49])); } } - 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[50])); } } - 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[51])); } } - if let Some(rule) = self.use_strict_mode.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[52])); } } - if let Some(rule) = self.use_throw_new_error.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - 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[54])); } } - 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[55])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + 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[56])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3881,6 +3896,10 @@ impl Nursery { .no_irregular_whitespace .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noIrregularWhitespaceCss" => self + .no_irregular_whitespace_css + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noLabelWithoutControl" => self .no_label_without_control .as_ref() diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index b75236895c6e..980ddd5b6e4d 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -9,6 +9,7 @@ pub mod no_empty_block; pub mod no_important_in_keyframe; pub mod no_invalid_direction_in_linear_gradient; pub mod no_invalid_position_at_import_rule; +pub mod no_irregular_whitespace_css; pub mod no_shorthand_property_overrides; pub mod no_unknown_function; pub mod no_unknown_media_feature_name; @@ -32,6 +33,7 @@ declare_lint_group! { self :: no_important_in_keyframe :: NoImportantInKeyframe , self :: no_invalid_direction_in_linear_gradient :: NoInvalidDirectionInLinearGradient , self :: no_invalid_position_at_import_rule :: NoInvalidPositionAtImportRule , + self :: no_irregular_whitespace_css :: NoIrregularWhitespaceCss , self :: no_shorthand_property_overrides :: NoShorthandPropertyOverrides , self :: no_unknown_function :: NoUnknownFunction , self :: no_unknown_media_feature_name :: NoUnknownMediaFeatureName , diff --git a/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace_css.rs b/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace_css.rs new file mode 100644 index 000000000000..875d6cedfc7f --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_irregular_whitespace_css.rs @@ -0,0 +1,98 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_css_syntax::{AnyCssRule, CssLanguage}; +use biome_rowan::{AstNode, Direction, SyntaxToken, TextRange}; + +const IRREGULAR_WHITESPACES: &[char; 22] = &[ + '\u{c}', '\u{b}', '\u{85}', '\u{feff}', '\u{a0}', '\u{1680}', '\u{180e}', '\u{2000}', + '\u{2001}', '\u{2002}', '\u{2003}', '\u{2004}', '\u{2005}', '\u{2006}', '\u{2007}', '\u{2008}', + '\u{2009}', '\u{200a}', '\u{200b}', '\u{202f}', '\u{205f}', '\u{3000}', +]; + +declare_lint_rule! { + /// Disallows the use of irregular whitespace. + /// + /// Using irregular whitespace would lead to the failure of selecting the correct target. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// .firstClass .secondClass { + /// color: red; + /// } + /// ``` + /// + /// ```css,expect_diagnostic + /// .firstClass .secondClass { + /// color: red; + /// } + /// ``` + /// ### Valid + /// + /// ```css + /// .firstClass .secondClass { + /// color: red; + /// } + /// ``` + /// + pub NoIrregularWhitespaceCss { + version: "next", + name: "noIrregularWhitespaceCss", + language: "css", + recommended: false, + sources: &[RuleSource::Stylelint("no-irregular-whitespace")], + } +} + +impl Rule for NoIrregularWhitespaceCss { + type Query = Ast; + type State = TextRange; + type Signals = Vec; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + get_irregular_whitespace(node) + } + + fn diagnostic(_: &RuleContext, range: &Self::State) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + range, + markup! { + "Irregular whitespace found." + }, + ) + .note(markup! { + "Replace the irregular whitespace with normal whitespaces." + }), + ) + } +} + +fn get_irregular_whitespace(node: &AnyCssRule) -> Vec { + let syntax = node.syntax(); + let mut all_whitespaces_token: Vec = vec![]; + let matches_irregular_whitespace = |token: &SyntaxToken| { + !token.has_leading_comments() + && !token.has_trailing_comments() + && token.text().chars().any(|char| { + IRREGULAR_WHITESPACES + .iter() + .any(|irregular_whitespace| &char == irregular_whitespace) + }) + }; + + for token in syntax.descendants_tokens(Direction::Next) { + if matches_irregular_whitespace(&token) { + all_whitespaces_token.push(token.text_range()); + } + } + + all_whitespaces_token +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 1b96851c45c4..8bc2f5e2ae94 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -11,6 +11,7 @@ pub type NoEmptyBlock = pub type NoImportantInKeyframe = < lint :: nursery :: no_important_in_keyframe :: NoImportantInKeyframe as biome_analyze :: Rule > :: Options ; pub type NoInvalidDirectionInLinearGradient = < lint :: nursery :: no_invalid_direction_in_linear_gradient :: NoInvalidDirectionInLinearGradient as biome_analyze :: Rule > :: Options ; pub type NoInvalidPositionAtImportRule = < lint :: nursery :: no_invalid_position_at_import_rule :: NoInvalidPositionAtImportRule as biome_analyze :: Rule > :: Options ; +pub type NoIrregularWhitespaceCss = < lint :: nursery :: no_irregular_whitespace_css :: NoIrregularWhitespaceCss as biome_analyze :: Rule > :: Options ; pub type NoShorthandPropertyOverrides = < lint :: nursery :: no_shorthand_property_overrides :: NoShorthandPropertyOverrides as biome_analyze :: Rule > :: Options ; pub type NoUnknownFunction = ::Options; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css new file mode 100644 index 000000000000..7118e2f380e3 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css @@ -0,0 +1,97 @@ +/* \u{b} */ +@import 'a.css'; +/* \u{c} */ +@layer module, state; +/* \u{feff} */ +.firstClass.secondClass { + padding: 10px; +} +/* \u{a0} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{1680} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2000} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2001} */ +.firstClass .secondClass { + flex: 1 1 100px; +} +/* \u{2002} */ +.firstClass.secondClass { + padding: 10px; +} +/* \u{2003} */ +.firstClass .secondClass  { + padding: 10px; +} +/* \u{2004} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2005} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2006} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2007} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2008} */ +@view-transition { + navigation: auto; +} +/* \u{2009} */ +@layer state { + body { + padding: 10px; + } +} +/* \u{200a} */ +@layer state { + body { + padding: 10px; + } +} +/* \u{200b} */ +@keyframes slidein { + from​{ + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } +} +/* \u{202f} */ +@font-face { + font-family: "Trickster"; + src: + local("Trickster"), + url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), + url("trickster-outline.otf") format("opentype"), + url("trickster-outline.woff") format("woff"); +} +/* \u{205f} */ +@keyframes slidein { + from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } +} +/* \u{3000} */ +@container (width < 15rem) { + color: blue; +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css.snap new file mode 100644 index 000000000000..f5a5806420f6 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/invalid.css.snap @@ -0,0 +1,411 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +/* \u{b} */ +@import 'a.css'; +/* \u{c} */ +@layer module, state; +/* \u{feff} */ +.firstClass.secondClass { + padding: 10px; +} +/* \u{a0} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{1680} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2000} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2001} */ +.firstClass .secondClass { + flex: 1 1 100px; +} +/* \u{2002} */ +.firstClass.secondClass { + padding: 10px; +} +/* \u{2003} */ +.firstClass .secondClass  { + padding: 10px; +} +/* \u{2004} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2005} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2006} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2007} */ +.firstClass .secondClass { + padding: 10px; +} +/* \u{2008} */ +@view-transition { + navigation: auto; +} +/* \u{2009} */ +@layer state { + body { + padding: 10px; + } +} +/* \u{200a} */ +@layer state { + body { + padding: 10px; + } +} +/* \u{200b} */ +@keyframes slidein { + from​{ + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } +} +/* \u{202f} */ +@font-face { + font-family: "Trickster"; + src: + local("Trickster"), + url("trickster-COLRv1.otf") format("opentype") tech(color-COLRv1), + url("trickster-outline.otf") format("opentype"), + url("trickster-outline.woff") format("woff"); +} +/* \u{205f} */ +@keyframes slidein { + from { + transform: translateX(0%); + } + + to { + transform: translateX(100%); + } +} +/* \u{3000} */ +@container (width < 15rem) { + color: blue; +} + +``` + +# Diagnostics +``` +invalid.css:2:2 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 1 │ /* \u{b} */ + > 2 │ @import␋ 'a.css'; + │ ^^^^^^^^ + 3 │ /* \u{c} */ + 4 │ @layer↡module, state; + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:4:2 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 2 │ @import␋ 'a.css'; + 3 │ /* \u{c} */ + > 4 │ @layer↡module, state; + │ ^^^^^^ + 5 │ /* \u{feff} */ + 6 │ .firstClass�.secondClass { + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:6:12 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 4 │ @layer↡module, state; + 5 │ /* \u{feff} */ + > 6 │ .firstClass�.secondClass { + │ ^ + 7 │ padding: 10px; + 8 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:18:12 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 16 │ } + 17 │ /* \u{2000} */ + > 18 │ .firstClass␠.secondClass { + │ ^ + 19 │ padding: 10px; + 20 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:23:9 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 21 │ /* \u{2001} */ + 22 │ .firstClass .secondClass { + > 23 │ flex: 1␠1 100px; + │ ^ + 24 │ } + 25 │ /* \u{2002} */ + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:30:25 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 28 │ } + 29 │ /* \u{2003} */ + > 30 │ .firstClass .secondClass␠ { + │ ^^ + 31 │ padding: 10px; + 32 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:34:12 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 32 │ } + 33 │ /* \u{2004} */ + > 34 │ .firstClass␠.secondClass { + │ ^ + 35 │ padding: 10px; + 36 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:38:12 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 36 │ } + 37 │ /* \u{2005} */ + > 38 │ .firstClass␠.secondClass { + │ ^ + 39 │ padding: 10px; + 40 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:42:12 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 40 │ } + 41 │ /* \u{2006} */ + > 42 │ .firstClass␠.secondClass { + │ ^ + 43 │ padding: 10px; + 44 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:46:12 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 44 │ } + 45 │ /* \u{2007} */ + > 46 │ .firstClass␠.secondClass { + │ ^ + 47 │ padding: 10px; + 48 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:51:14 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 49 │ /* \u{2008} */ + 50 │ @view-transition { + > 51 │ navigation:␠auto; + │ ^ + 52 │ } + 53 │ /* \u{2009} */ + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:56:11 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 54 │ @layer state { + 55 │ body { + > 56 │ padding:␠10px; + │ ^ + 57 │ } + 58 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:56:11 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 54 │ @layer state { + 55 │ body { + > 56 │ padding:␠10px; + │ ^ + 57 │ } + 58 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:60:7 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 58 │ } + 59 │ /* \u{200a} */ + > 60 │ @layer␠state { + │ ^ + 61 │ body { + 62 │ padding: 10px; + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:67:6 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 65 │ /* \u{200b} */ + 66 │ @keyframes slidein { + > 67 │ from�{ + │ ^ + 68 │ transform: translateX(0%); + 69 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:76:11 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 74 │ } + 75 │ /* \u{202f} */ + > 76 │ @font-face␠{ + │ ^ + 77 │ font-family: "Trickster"; + 78 │ src: + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:85:11 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 83 │ } + 84 │ /* \u{205f} */ + > 85 │ @keyframes␠slidein { + │ ^ + 86 │ from { + 87 │ transform: translateX(0%); + + i Replace the irregular whitespace with normal whitespaces. + + +``` + +``` +invalid.css:95:11 lint/nursery/noIrregularWhitespaceCss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Irregular whitespace found. + + 93 │ } + 94 │ /* \u{3000} */ + > 95 │ @container␠(width < 15rem) { + │ ^ + 96 │ color: blue; + 97 │ } + + i Replace the irregular whitespace with normal whitespaces. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css new file mode 100644 index 000000000000..37a96887d532 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css @@ -0,0 +1,10 @@ +/* should not generate diagnostics */ +.firstClass .secondClass { + padding: 10px; +} + +.firstClass .secondClass { + /* whitespace can be in comments */ + padding: 10px; +} + diff --git a/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css.snap new file mode 100644 index 000000000000..6623366ddc9d --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noIrregularWhitespaceCss/valid.css.snap @@ -0,0 +1,18 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +/* should not generate diagnostics */ +.firstClass .secondClass { + padding: 10px; +} + +.firstClass .secondClass { + /* whitespace can be in comments */ + padding: 10px; +} + + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 750c37f53ff3..21c00ee04b8d 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/noInvalidDirectionInLinearGradient": "https://biomejs.dev/linter/rules/no-invalid-direction-in-linear-gradient", "lint/nursery/noInvalidPositionAtImportRule": "https://biomejs.dev/linter/rules/no-invalid-position-at-import-rule", "lint/nursery/noIrregularWhitespace": "https://biomejs.dev/linter/rules/no-irregular-whitespace", + "lint/nursery/noIrregularWhitespaceCss": "https://biomejs.dev/linter/rules/no-irregular-whitespace-css", "lint/nursery/noLabelWithoutControl": "https://biomejs.dev/linter/rules/no-label-without-control", "lint/nursery/noMisplacedAssertion": "https://biomejs.dev/linter/rules/no-misplaced-assertion", "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index a6838a751b8c..e346c2a8000e 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1113,6 +1113,10 @@ export interface Nursery { * Disallows the use of irregular whitespace characters. */ noIrregularWhitespace?: RuleConfiguration_for_Null; + /** + * Disallows the use of irregular whitespace. + */ + noIrregularWhitespaceCss?: RuleConfiguration_for_Null; /** * Enforce that a label element or component has a text label and an associated input. */ @@ -2533,6 +2537,7 @@ export type Category = | "lint/nursery/noInvalidDirectionInLinearGradient" | "lint/nursery/noInvalidPositionAtImportRule" | "lint/nursery/noIrregularWhitespace" + | "lint/nursery/noIrregularWhitespaceCss" | "lint/nursery/noLabelWithoutControl" | "lint/nursery/noMisplacedAssertion" | "lint/nursery/noMissingGenericFamilyKeyword" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 65dd047a290d..4bca8b03e4e4 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1870,6 +1870,13 @@ { "type": "null" } ] }, + "noIrregularWhitespaceCss": { + "description": "Disallows the use of irregular whitespace.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noLabelWithoutControl": { "description": "Enforce that a label element or component has a text label and an associated input.", "anyOf": [