diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index bf9a47a74a80..19b5a58c1891 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2705,6 +2705,10 @@ pub struct Nursery { #[doc = "Disallow unknown CSS value functions."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_function: Option>, + #[doc = "Disallow unknown pseudo-element selectors."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unknown_selector_pseudo_element: + Option>, #[doc = "Disallow unknown CSS units."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_unit: Option>, @@ -2768,6 +2772,7 @@ impl Nursery { "noRestrictedImports", "noUndeclaredDependencies", "noUnknownFunction", + "noUnknownSelectorPseudoElement", "noUnknownUnit", "noUselessUndefinedInitialization", "useArrayLiterals", @@ -2789,6 +2794,7 @@ impl Nursery { "noFlatMapIdentity", "noImportantInKeyframe", "noUnknownFunction", + "noUnknownSelectorPseudoElement", "noUnknownUnit", "useGenericFontNames", ]; @@ -2805,7 +2811,8 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -2835,6 +2842,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2946,46 +2954,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - 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[19])); } } - if let Some(rule) = self.no_useless_undefined_initialization.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[20])); } } - if let Some(rule) = self.use_array_literals.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[21])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_array_literals.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_default_switch_clause.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[23])); } } - if let Some(rule) = self.use_generic_font_names.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[24])); } } - if let Some(rule) = self.use_import_restrictions.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[25])); } } - if let Some(rule) = self.use_sorted_classes.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[26])); } } + 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[27])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3085,46 +3098,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - 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[19])); } } - if let Some(rule) = self.no_useless_undefined_initialization.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[20])); } } - if let Some(rule) = self.use_array_literals.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[21])); } } - if let Some(rule) = self.use_consistent_builtin_instantiation.as_ref() { + if let Some(rule) = self.use_array_literals.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_default_switch_clause.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[23])); } } - if let Some(rule) = self.use_generic_font_names.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[24])); } } - if let Some(rule) = self.use_import_restrictions.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[25])); } } - if let Some(rule) = self.use_sorted_classes.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[26])); } } + 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[27])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3237,6 +3255,10 @@ impl Nursery { .no_unknown_function .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noUnknownSelectorPseudoElement" => self + .no_unknown_selector_pseudo_element + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUnknownUnit" => self .no_unknown_unit .as_ref() diff --git a/crates/biome_css_analyze/src/keywords.rs b/crates/biome_css_analyze/src/keywords.rs index 49751d015f7f..8998688e3074 100644 --- a/crates/biome_css_analyze/src/keywords.rs +++ b/crates/biome_css_analyze/src/keywords.rs @@ -759,6 +759,104 @@ pub const FUNCTION_KEYWORDS: [&str; 671] = [ "xywh", ]; +// These are the ones that can have single-colon notation +pub const LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS: [&str; 4] = + ["before", "after", "first-line", "first-letter"]; + +pub const VENDOR_SPECIFIC_PSEUDO_ELEMENTS: [&str; 66] = [ + "-moz-focus-inner", + "-moz-focus-outer", + "-moz-list-bullet", + "-moz-meter-bar", + "-moz-placeholder", + "-moz-progress-bar", + "-moz-range-progress", + "-moz-range-thumb", + "-moz-range-track", + "-ms-browse", + "-ms-check", + "-ms-clear", + "-ms-expand", + "-ms-fill", + "-ms-fill-lower", + "-ms-fill-upper", + "-ms-reveal", + "-ms-thumb", + "-ms-ticks-after", + "-ms-ticks-before", + "-ms-tooltip", + "-ms-track", + "-ms-value", + "-webkit-color-swatch", + "-webkit-color-swatch-wrapper", + "-webkit-calendar-picker-indicator", + "-webkit-clear-button", + "-webkit-date-and-time-value", + "-webkit-datetime-edit", + "-webkit-datetime-edit-ampm-field", + "-webkit-datetime-edit-day-field", + "-webkit-datetime-edit-fields-wrapper", + "-webkit-datetime-edit-hour-field", + "-webkit-datetime-edit-millisecond-field", + "-webkit-datetime-edit-minute-field", + "-webkit-datetime-edit-month-field", + "-webkit-datetime-edit-second-field", + "-webkit-datetime-edit-text", + "-webkit-datetime-edit-week-field", + "-webkit-datetime-edit-year-field", + "-webkit-details-marker", + "-webkit-distributed", + "-webkit-file-upload-button", + "-webkit-input-placeholder", + "-webkit-keygen-select", + "-webkit-meter-bar", + "-webkit-meter-even-less-good-value", + "-webkit-meter-inner-element", + "-webkit-meter-optimum-value", + "-webkit-meter-suboptimum-value", + "-webkit-progress-bar", + "-webkit-progress-inner-element", + "-webkit-progress-value", + "-webkit-search-cancel-button", + "-webkit-search-decoration", + "-webkit-search-results-button", + "-webkit-search-results-decoration", + "-webkit-slider-runnable-track", + "-webkit-slider-thumb", + "-webkit-textfield-decoration-container", + "-webkit-validation-bubble", + "-webkit-validation-bubble-arrow", + "-webkit-validation-bubble-arrow-clipper", + "-webkit-validation-bubble-heading", + "-webkit-validation-bubble-message", + "-webkit-validation-bubble-text-block", +]; + +pub const SHADOW_TREE_PSEUDO_ELEMENTS: [&str; 1] = ["part"]; + +pub const OTHER_PSEUDO_ELEMENTS: [&str; 18] = [ + "backdrop", + "content", + "cue", + "file-selector-button", + "grammar-error", + "highlight", + "marker", + "placeholder", + "selection", + "shadow", + "slotted", + "spelling-error", + "target-text", + "view-transition", + "view-transition-group", + "view-transition-image-pair", + "view-transition-new", + "view-transition-old", +]; + +pub const VENDER_PREFIXES: [&str; 4] = ["-webkit-", "-moz-", "-ms-", "-o-"]; + #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 5455fea8eba7..2ad4f01e119f 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_duplicate_font_names; pub mod no_duplicate_selectors_keyframe_block; pub mod no_important_in_keyframe; pub mod no_unknown_function; +pub mod no_unknown_selector_pseudo_element; pub mod no_unknown_unit; pub mod use_generic_font_names; @@ -23,6 +24,7 @@ declare_group! { self :: no_duplicate_selectors_keyframe_block :: NoDuplicateSelectorsKeyframeBlock , self :: no_important_in_keyframe :: NoImportantInKeyframe , self :: no_unknown_function :: NoUnknownFunction , + self :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement , self :: no_unknown_unit :: NoUnknownUnit , self :: use_generic_font_names :: UseGenericFontNames , ] diff --git a/crates/biome_css_analyze/src/lint/nursery/no_unknown_selector_pseudo_element.rs b/crates/biome_css_analyze/src/lint/nursery/no_unknown_selector_pseudo_element.rs new file mode 100644 index 000000000000..586049bf5dad --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_unknown_selector_pseudo_element.rs @@ -0,0 +1,108 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_css_syntax::{AnyCssPseudoElement, CssPseudoElementSelector}; +use biome_rowan::AstNode; + +use crate::utils::{is_pseudo_elements, vender_prefix}; + +declare_rule! { + /// Disallow unknown pseudo-element selectors. + /// + /// For details on known CSS pseudo-elements, see the [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements#list_of_pseudo-elements). + /// + /// This rule ignores vendor-prefixed pseudo-element selectors. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// a::pseudo {} + /// ``` + /// + /// ```css,expect_diagnostic + /// a::PSEUDO {} + /// ``` + /// + /// ```css,expect_diagnostic + /// a::element {} + /// ``` + /// + /// ### Valid + /// + /// ```css + /// a:before {} + /// ``` + /// + /// ```css + /// a::before {} + /// ``` + /// + /// ```css + /// ::selection {} + /// ``` + /// + /// ```css + /// input::-moz-placeholder {} + /// ``` + /// + pub NoUnknownSelectorPseudoElement { + version: "next", + name: "noUnknownSelectorPseudoElement", + recommended: true, + sources: &[RuleSource::Stylelint("selector-pseudo-element-no-unknown")], + } +} + +impl Rule for NoUnknownSelectorPseudoElement { + type Query = Ast; + type State = AnyCssPseudoElement; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node: &CssPseudoElementSelector = ctx.query(); + let pseudo_element = node.element().ok()?; + + let pseudo_element_name = match &pseudo_element { + AnyCssPseudoElement::CssBogusPseudoElement(element) => element.text(), + AnyCssPseudoElement::CssPseudoElementFunctionIdentifier(ident) => { + ident.name().ok()?.text().to_string() + } + AnyCssPseudoElement::CssPseudoElementFunctionSelector(selector) => selector.text(), + AnyCssPseudoElement::CssPseudoElementIdentifier(ident) => { + ident.name().ok()?.text().to_string() + } + }; + + if !vender_prefix(pseudo_element_name.as_str()).is_empty() + || is_pseudo_elements(pseudo_element_name.to_lowercase().as_str()) + { + return None; + } + + Some(pseudo_element) + } + + fn diagnostic(_: &RuleContext, element: &Self::State) -> Option { + let span = element.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Unexpected unknown pseudo-elements: "{ element.text() } + }, + ) + .note(markup! { + "See ""MDN web docs"" for more details." + }) + .footer_list( + markup! { + "Use a known pseudo-elements instead, such as:" + }, + &["after", "backdrop", "before", "etc."], + ), + ) + } +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index cacfd2fec994..7c3fc0b8b39f 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -13,6 +13,7 @@ pub type NoDuplicateSelectorsKeyframeBlock = < lint :: nursery :: no_duplicate_s pub type NoImportantInKeyframe = < lint :: nursery :: no_important_in_keyframe :: NoImportantInKeyframe as biome_analyze :: Rule > :: Options ; pub type NoUnknownFunction = ::Options; +pub type NoUnknownSelectorPseudoElement = < lint :: nursery :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement as biome_analyze :: Rule > :: Options ; pub type NoUnknownUnit = ::Options; pub type UseGenericFontNames = diff --git a/crates/biome_css_analyze/src/utils.rs b/crates/biome_css_analyze/src/utils.rs index f8ddb421a082..ce6063ff6109 100644 --- a/crates/biome_css_analyze/src/utils.rs +++ b/crates/biome_css_analyze/src/utils.rs @@ -1,8 +1,9 @@ use crate::keywords::{ BASIC_KEYWORDS, FONT_FAMILY_KEYWORDS, FONT_SIZE_KEYWORDS, FONT_STRETCH_KEYWORDS, FONT_STYLE_KEYWORDS, FONT_VARIANTS_KEYWORDS, FONT_WEIGHT_ABSOLUTE_KEYWORDS, - FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, LINE_HEIGHT_KEYWORDS, - SYSTEM_FAMILY_NAME_KEYWORDS, + FONT_WEIGHT_NUMERIC_KEYWORDS, FUNCTION_KEYWORDS, LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS, + LINE_HEIGHT_KEYWORDS, OTHER_PSEUDO_ELEMENTS, SHADOW_TREE_PSEUDO_ELEMENTS, + SYSTEM_FAMILY_NAME_KEYWORDS, VENDER_PREFIXES, VENDOR_SPECIFIC_PSEUDO_ELEMENTS, }; use biome_css_syntax::{AnyCssGenericComponentValue, AnyCssValue, CssGenericComponentValueList}; use biome_rowan::{AstNode, SyntaxNodeCast}; @@ -109,3 +110,20 @@ pub fn is_function_keyword(value: &str) -> bool { pub fn is_custom_function(value: &str) -> bool { value.starts_with("--") } + +// Returns the vendor prefix extracted from an input string. +pub fn vender_prefix(prop: &str) -> String { + for prefix in VENDER_PREFIXES.iter() { + if prop.starts_with(prefix) { + return (*prefix).to_string(); + } + } + String::new() +} + +pub fn is_pseudo_elements(prop: &str) -> bool { + LEVEL_ONE_AND_TWO_PSEUDO_ELEMENTS.contains(&prop) + || VENDOR_SPECIFIC_PSEUDO_ELEMENTS.contains(&prop) + || SHADOW_TREE_PSEUDO_ELEMENTS.contains(&prop) + || OTHER_PSEUDO_ELEMENTS.contains(&prop) +} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/invalid.css new file mode 100644 index 000000000000..7e2e9670bbc8 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/invalid.css @@ -0,0 +1,8 @@ +a::pseudo { } +a::Pseudo { } +a::pSeUdO { } +a::PSEUDO { } +a::element { } +a:hover::element { } +a, +b > .foo::error { } diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/invalid.css.snap new file mode 100644 index 000000000000..3ff3a1f4d8a4 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/invalid.css.snap @@ -0,0 +1,181 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +a::pseudo { } +a::Pseudo { } +a::pSeUdO { } +a::PSEUDO { } +a::element { } +a:hover::element { } +a, +b > .foo::error { } + +``` + +# Diagnostics +``` +invalid.css:1:4 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: pseudo + + > 1 │ a::pseudo { } + │ ^^^^^^ + 2 │ a::Pseudo { } + 3 │ a::pSeUdO { } + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` + +``` +invalid.css:2:4 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: Pseudo + + 1 │ a::pseudo { } + > 2 │ a::Pseudo { } + │ ^^^^^^ + 3 │ a::pSeUdO { } + 4 │ a::PSEUDO { } + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` + +``` +invalid.css:3:4 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: pSeUdO + + 1 │ a::pseudo { } + 2 │ a::Pseudo { } + > 3 │ a::pSeUdO { } + │ ^^^^^^ + 4 │ a::PSEUDO { } + 5 │ a::element { } + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` + +``` +invalid.css:4:4 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: PSEUDO + + 2 │ a::Pseudo { } + 3 │ a::pSeUdO { } + > 4 │ a::PSEUDO { } + │ ^^^^^^ + 5 │ a::element { } + 6 │ a:hover::element { } + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` + +``` +invalid.css:5:4 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: element + + 3 │ a::pSeUdO { } + 4 │ a::PSEUDO { } + > 5 │ a::element { } + │ ^^^^^^^ + 6 │ a:hover::element { } + 7 │ a, + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` + +``` +invalid.css:6:10 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: element + + 4 │ a::PSEUDO { } + 5 │ a::element { } + > 6 │ a:hover::element { } + │ ^^^^^^^ + 7 │ a, + 8 │ b > .foo::error { } + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` + +``` +invalid.css:8:11 lint/nursery/noUnknownSelectorPseudoElement ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Unexpected unknown pseudo-elements: error + + 6 │ a:hover::element { } + 7 │ a, + > 8 │ b > .foo::error { } + │ ^^^^^ + 9 │ + + i See MDN web docs for more details. + + i Use a known pseudo-elements instead, such as: + + - after + - backdrop + - before + - etc. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/valid.css new file mode 100644 index 000000000000..7fa10a8eefa9 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/valid.css @@ -0,0 +1,32 @@ +a:before { } +a:Before { } +a:bEfOrE { } +a:BEFORE { } +a:after { } +a:first-letter { } +a:first-line { } +a::before { } +a::Before { } +a::bEfOrE { } +a::BEFORE { } +a::after { } +a::first-letter { } +a::first-line { } +::selection { } +a::spelling-error { } +a::grammar-error { } +li::marker { } +div::shadow { } +div::content { } +input::-moz-placeholder { } +input::-moz-test { } +a:hover { } +a:focus { } +a:hover::before { } +a:hover::-moz-placeholder { } +a,\nb > .foo::before { } +:root { --foo: 1px; } +html { --foo: 1px; } +:root { --custom-property-set: {} } +html { --custom-property-set: {} } +a::part(shadow-part) { } diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/valid.css.snap new file mode 100644 index 000000000000..a5f4e0f3ab37 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnknownSelectorPseudoElement/valid.css.snap @@ -0,0 +1,40 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +a:before { } +a:Before { } +a:bEfOrE { } +a:BEFORE { } +a:after { } +a:first-letter { } +a:first-line { } +a::before { } +a::Before { } +a::bEfOrE { } +a::BEFORE { } +a::after { } +a::first-letter { } +a::first-line { } +::selection { } +a::spelling-error { } +a::grammar-error { } +li::marker { } +div::shadow { } +div::content { } +input::-moz-placeholder { } +input::-moz-test { } +a:hover { } +a:focus { } +a:hover::before { } +a:hover::-moz-placeholder { } +a,\nb > .foo::before { } +:root { --foo: 1px; } +html { --foo: 1px; } +:root { --custom-property-set: {} } +html { --custom-property-set: {} } +a::part(shadow-part) { } + +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 00036e8365e4..218c023ec030 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/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", + "lint/nursery/noUnknownSelectorPseudoElement": "https://biomejs.dev/linter/rules/no-unknown-selector-pseudo-element", "lint/nursery/noUnknownUnit": "https://biomejs.dev/linter/rules/no-unknown-unit", "lint/nursery/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization", "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 567161017c31..82549c69344a 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -984,6 +984,10 @@ export interface Nursery { * Disallow unknown CSS value functions. */ noUnknownFunction?: RuleConfiguration_for_Null; + /** + * Disallow unknown pseudo-element selectors. + */ + noUnknownSelectorPseudoElement?: RuleConfiguration_for_Null; /** * Disallow unknown CSS units. */ @@ -2006,6 +2010,7 @@ export type Category = | "lint/nursery/noTypeOnlyImportAttributes" | "lint/nursery/noUndeclaredDependencies" | "lint/nursery/noUnknownFunction" + | "lint/nursery/noUnknownSelectorPseudoElement" | "lint/nursery/noUnknownUnit" | "lint/nursery/noUselessUndefinedInitialization" | "lint/nursery/useArrayLiterals" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 0ce6cebde88f..a383697a81b9 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1566,6 +1566,13 @@ { "type": "null" } ] }, + "noUnknownSelectorPseudoElement": { + "description": "Disallow unknown pseudo-element selectors.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUnknownUnit": { "description": "Disallow unknown CSS units.", "anyOf": [