diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b6140d00ed62..5f50cdf379f1 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2712,6 +2712,9 @@ pub struct Nursery { #[doc = "Disallow unknown CSS units."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_unit: Option>, + #[doc = "Disallow unmatchable An+B selectors."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unmatchable_anb_selector: Option>, #[doc = "Disallow initializing variables to undefined."] #[serde(skip_serializing_if = "Option::is_none")] pub no_useless_undefined_initialization: @@ -2777,6 +2780,7 @@ impl Nursery { "noUnknownFunction", "noUnknownSelectorPseudoElement", "noUnknownUnit", + "noUnmatchableAnbSelector", "noUselessUndefinedInitialization", "useArrayLiterals", "useConsistentBuiltinInstantiation", @@ -2800,6 +2804,7 @@ impl Nursery { "noUnknownFunction", "noUnknownSelectorPseudoElement", "noUnknownUnit", + "noUnmatchableAnbSelector", "useGenericFontNames", ]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ @@ -2816,7 +2821,8 @@ impl Nursery { 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[20]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -2848,6 +2854,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), 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]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2969,46 +2976,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_undefined_initialization.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[21])); } } - 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[22])); } } - 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[23])); } } - 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[24])); } } - if let Some(rule) = self.use_explicit_length_check.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[25])); } } - if let Some(rule) = self.use_generic_font_names.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[26])); } } - 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[27])); } } - 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[28])); } } + 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[29])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3118,46 +3130,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_undefined_initialization.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[21])); } } - 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[22])); } } - 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[23])); } } - 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[24])); } } - if let Some(rule) = self.use_explicit_length_check.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[25])); } } - if let Some(rule) = self.use_generic_font_names.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[26])); } } - 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[27])); } } - 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[28])); } } + 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[29])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3278,6 +3295,10 @@ impl Nursery { .no_unknown_unit .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noUnmatchableAnbSelector" => self + .no_unmatchable_anb_selector + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUselessUndefinedInitialization" => self .no_useless_undefined_initialization .as_ref() diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 2ad4f01e119f..d58af8788cca 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -11,6 +11,7 @@ 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 no_unmatchable_anb_selector; pub mod use_generic_font_names; declare_group! { @@ -26,6 +27,7 @@ declare_group! { self :: no_unknown_function :: NoUnknownFunction , self :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement , self :: no_unknown_unit :: NoUnknownUnit , + self :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector , self :: use_generic_font_names :: UseGenericFontNames , ] } diff --git a/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs b/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs new file mode 100644 index 000000000000..e7a5193254c8 --- /dev/null +++ b/crates/biome_css_analyze/src/lint/nursery/no_unmatchable_anb_selector.rs @@ -0,0 +1,123 @@ +use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource}; +use biome_console::markup; +use biome_css_syntax::{ + AnyCssPseudoClassNth, CssPseudoClassFunctionSelectorList, CssPseudoClassNthSelector, +}; +use biome_rowan::{AstNode, SyntaxNodeCast}; + +declare_rule! { + /// Disallow unmatchable An+B selectors. + /// + /// Selectors that always evaluate to 0 will not match any elements. + /// For more details about the An+B syntax, see: + /// https://www.w3.org/TR/css-syntax-3/#anb-microsyntax + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// a:nth-child(0) {} + /// ``` + /// + /// ```css,expect_diagnostic + /// a:nth-last-child(0n) {} + /// ``` + /// + /// ```css,expect_diagnostic + /// a:nth-of-type(0n+0) {} + /// ``` + /// + /// ```css,expect_diagnostic + /// a:nth-last-of-type(0 of a) {} + /// ``` + /// + /// ### Valid + /// + /// ```css + /// a:nth-child(1) {} + /// ``` + /// + /// ```css + /// a:nth-last-child(1n) {} + /// ``` + /// + /// ```css + /// a:nth-of-type(1n+0) {} + /// ``` + /// + /// ```css + /// a:nth-last-of-type(1 of a) {} + /// ``` + /// + pub NoUnmatchableAnbSelector { + version: "next", + name: "noUnmatchableAnbSelector", + recommended: true, + sources: &[RuleSource::Stylelint("selector-anb-no-unmatchable")], + } +} + +impl Rule for NoUnmatchableAnbSelector { + type Query = Ast; + type State = CssPseudoClassNthSelector; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node = ctx.query(); + let nth = node.nth().ok()?; + if is_unmatchable(&nth) && !is_within_not_pseudo_class(&nth) { + return Some(node.clone()); + } + None + } + + fn diagnostic(_: &RuleContext, node: &Self::State) -> Option { + let span = node.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "This selector will never match any elements." + }, + ) + .note(markup! { + "Avoid using An+B selectors that always evaluate to 0." + }).note(markup! { + "For more details, see ""the official spec for An+B selectors""." + }) + ) + } +} + +fn is_unmatchable(nth: &AnyCssPseudoClassNth) -> bool { + match nth { + AnyCssPseudoClassNth::CssPseudoClassNthIdentifier(_) => false, + AnyCssPseudoClassNth::CssPseudoClassNth(nth) => { + let coefficient = nth.value(); + let constant = nth.offset(); + match (coefficient, constant) { + (Some(a), Some(b)) => a.text() == "0" && b.text() == "0", + (Some(a), None) => a.text() == "0", + _ => false, + } + } + AnyCssPseudoClassNth::CssPseudoClassNthNumber(nth) => nth.text() == "0", + } +} + +// Check if the nth selector is effective within a `not` pseudo class +// Example: a:not(:nth-child(0)) returns true +// a:not(:not(:nth-child(0))) returns false +fn is_within_not_pseudo_class(node: &AnyCssPseudoClassNth) -> bool { + let number_of_not = node + .syntax() + .ancestors() + .filter_map(|n| n.cast::()) + .filter_map(|n| n.name().ok()) + .filter(|n| n.text() == "not") + .count(); + number_of_not % 2 == 1 +} diff --git a/crates/biome_css_analyze/src/options.rs b/crates/biome_css_analyze/src/options.rs index 7c3fc0b8b39f..a8902cadb16a 100644 --- a/crates/biome_css_analyze/src/options.rs +++ b/crates/biome_css_analyze/src/options.rs @@ -16,5 +16,6 @@ pub type NoUnknownFunction = pub type NoUnknownSelectorPseudoElement = < lint :: nursery :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement as biome_analyze :: Rule > :: Options ; pub type NoUnknownUnit = ::Options; +pub type NoUnmatchableAnbSelector = < lint :: nursery :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector as biome_analyze :: Rule > :: Options ; pub type UseGenericFontNames = ::Options; diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css new file mode 100644 index 000000000000..e311b0b71c0d --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css @@ -0,0 +1,14 @@ +a:nth-child(0) {} +a:nth-child(0n) {} +a:nth-child(+0n) {} +a:nth-child(-0n) {} +a:nth-child(0n+0) {} +a:nth-child(0n-0) {} +a:nth-child(-0n-0) {} +a:nth-child(0 of a) {} +a:nth-child(0), a:nth-child(1) {} +a:nth-last-child(0) {} +a:nth-of-type(0) {} +a:nth-last-of-type(0) {} +a:nth-child(0n):nth-child(-n+5) {} +a:nth-last-child(0),a:nth-last-child(n+5) ~ li {} diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap new file mode 100644 index 000000000000..6e390f281647 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/invalid.css.snap @@ -0,0 +1,228 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: invalid.css +--- +# Input +```css +a:nth-child(0) {} +a:nth-child(0n) {} +a:nth-child(+0n) {} +a:nth-child(-0n) {} +a:nth-child(0n+0) {} +a:nth-child(0n-0) {} +a:nth-child(-0n-0) {} +a:nth-child(0 of a) {} +a:nth-child(0), a:nth-child(1) {} +a:nth-last-child(0) {} +a:nth-of-type(0) {} +a:nth-last-of-type(0) {} +a:nth-child(0n):nth-child(-n+5) {} +a:nth-last-child(0),a:nth-last-child(n+5) ~ li {} + +``` + +# Diagnostics +``` +invalid.css:1:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + > 1 │ a:nth-child(0) {} + │ ^ + 2 │ a:nth-child(0n) {} + 3 │ a:nth-child(+0n) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:2:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 1 │ a:nth-child(0) {} + > 2 │ a:nth-child(0n) {} + │ ^^ + 3 │ a:nth-child(+0n) {} + 4 │ a:nth-child(-0n) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:3:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 1 │ a:nth-child(0) {} + 2 │ a:nth-child(0n) {} + > 3 │ a:nth-child(+0n) {} + │ ^^^ + 4 │ a:nth-child(-0n) {} + 5 │ a:nth-child(0n+0) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:4:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 2 │ a:nth-child(0n) {} + 3 │ a:nth-child(+0n) {} + > 4 │ a:nth-child(-0n) {} + │ ^^^ + 5 │ a:nth-child(0n+0) {} + 6 │ a:nth-child(0n-0) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:8:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 6 │ a:nth-child(0n-0) {} + 7 │ a:nth-child(-0n-0) {} + > 8 │ a:nth-child(0 of a) {} + │ ^^^^^^ + 9 │ a:nth-child(0), a:nth-child(1) {} + 10 │ a:nth-last-child(0) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:9:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 7 │ a:nth-child(-0n-0) {} + 8 │ a:nth-child(0 of a) {} + > 9 │ a:nth-child(0), a:nth-child(1) {} + │ ^ + 10 │ a:nth-last-child(0) {} + 11 │ a:nth-of-type(0) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:10:18 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 8 │ a:nth-child(0 of a) {} + 9 │ a:nth-child(0), a:nth-child(1) {} + > 10 │ a:nth-last-child(0) {} + │ ^ + 11 │ a:nth-of-type(0) {} + 12 │ a:nth-last-of-type(0) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:11:15 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 9 │ a:nth-child(0), a:nth-child(1) {} + 10 │ a:nth-last-child(0) {} + > 11 │ a:nth-of-type(0) {} + │ ^ + 12 │ a:nth-last-of-type(0) {} + 13 │ a:nth-child(0n):nth-child(-n+5) {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:12:20 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 10 │ a:nth-last-child(0) {} + 11 │ a:nth-of-type(0) {} + > 12 │ a:nth-last-of-type(0) {} + │ ^ + 13 │ a:nth-child(0n):nth-child(-n+5) {} + 14 │ a:nth-last-child(0),a:nth-last-child(n+5) ~ li {} + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:13:13 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 11 │ a:nth-of-type(0) {} + 12 │ a:nth-last-of-type(0) {} + > 13 │ a:nth-child(0n):nth-child(-n+5) {} + │ ^^ + 14 │ a:nth-last-child(0),a:nth-last-child(n+5) ~ li {} + 15 │ + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` + +``` +invalid.css:14:18 lint/nursery/noUnmatchableAnbSelector ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This selector will never match any elements. + + 12 │ a:nth-last-of-type(0) {} + 13 │ a:nth-child(0n):nth-child(-n+5) {} + > 14 │ a:nth-last-child(0),a:nth-last-child(n+5) ~ li {} + │ ^ + 15 │ + + i Avoid using An+B selectors that always evaluate to 0. + + i For more details, see the official spec for An+B selectors. + + +``` diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/valid.css b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/valid.css new file mode 100644 index 000000000000..66b3cbf64dec --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/valid.css @@ -0,0 +1,16 @@ +a:nth-child(1) {} +a:nth-child(2n) {} +a:nth-child(0n+1) {} +a:nth-child(0n-1) {} +a:nth-child(2n+0) {} +a:nth-child(2n+2) {} +a:nth-child(2n-0) {} +a:nth-child(1 of a) {} +a:nth-last-child(1) {} +a:nth-of-type(1) {} +a:nth-last-of-type(1) {} +a:nth-child(even) {} +a:nth-child(odd) {} +a:not(:nth-child(0)) {} +a:nth-child(n+1):nth-child(-n+5) {} +a:nth-last-child(n+5),a:nth-last-child(n+5) ~ li {} \ No newline at end of file diff --git a/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/valid.css.snap b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/valid.css.snap new file mode 100644 index 000000000000..654a287cacb4 --- /dev/null +++ b/crates/biome_css_analyze/tests/specs/nursery/noUnmatchableAnbSelector/valid.css.snap @@ -0,0 +1,23 @@ +--- +source: crates/biome_css_analyze/tests/spec_tests.rs +expression: valid.css +--- +# Input +```css +a:nth-child(1) {} +a:nth-child(2n) {} +a:nth-child(0n+1) {} +a:nth-child(0n-1) {} +a:nth-child(2n+0) {} +a:nth-child(2n+2) {} +a:nth-child(2n-0) {} +a:nth-child(1 of a) {} +a:nth-last-child(1) {} +a:nth-of-type(1) {} +a:nth-last-of-type(1) {} +a:nth-child(even) {} +a:nth-child(odd) {} +a:not(:nth-child(0)) {} +a:nth-child(n+1):nth-child(-n+5) {} +a:nth-last-child(n+5),a:nth-last-child(n+5) ~ li {} +``` diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 7b11222c242b..a3a3763e30e2 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -133,6 +133,7 @@ define_categories! { "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/noUnmatchableAnbSelector": "https://biomejs.dev/linter/rules/no-unmatchable-anb-selector", "lint/nursery/noUselessUndefinedInitialization": "https://biomejs.dev/linter/rules/no-useless-undefined-initialization", "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 4be08ef8e688..39e20858d19d 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -992,6 +992,10 @@ export interface Nursery { * Disallow unknown CSS units. */ noUnknownUnit?: RuleConfiguration_for_Null; + /** + * Disallow unmatchable An+B selectors. + */ + noUnmatchableAnbSelector?: RuleConfiguration_for_Null; /** * Disallow initializing variables to undefined. */ @@ -2016,6 +2020,7 @@ export type Category = | "lint/nursery/noUnknownFunction" | "lint/nursery/noUnknownSelectorPseudoElement" | "lint/nursery/noUnknownUnit" + | "lint/nursery/noUnmatchableAnbSelector" | "lint/nursery/noUselessUndefinedInitialization" | "lint/nursery/useArrayLiterals" | "lint/nursery/useBiomeSuppressionComment" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index d07313a3c211..6e258cdb6080 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1580,6 +1580,13 @@ { "type": "null" } ] }, + "noUnmatchableAnbSelector": { + "description": "Disallow unmatchable An+B selectors.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUselessUndefinedInitialization": { "description": "Disallow initializing variables to undefined.", "anyOf": [