Skip to content

Commit cb0182e

Browse files
authored
feat(linter): implement NoUnmatchableAnbSelector (#2706)
1 parent f77ab54 commit cb0182e

File tree

11 files changed

+458
-17
lines changed

11 files changed

+458
-17
lines changed

crates/biome_configuration/src/linter/rules.rs

+38-17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_css_analyze/src/lint/nursery.rs

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod no_important_in_keyframe;
1111
pub mod no_unknown_function;
1212
pub mod no_unknown_selector_pseudo_element;
1313
pub mod no_unknown_unit;
14+
pub mod no_unmatchable_anb_selector;
1415
pub mod use_generic_font_names;
1516

1617
declare_group! {
@@ -26,6 +27,7 @@ declare_group! {
2627
self :: no_unknown_function :: NoUnknownFunction ,
2728
self :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement ,
2829
self :: no_unknown_unit :: NoUnknownUnit ,
30+
self :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector ,
2931
self :: use_generic_font_names :: UseGenericFontNames ,
3032
]
3133
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic, RuleSource};
2+
use biome_console::markup;
3+
use biome_css_syntax::{
4+
AnyCssPseudoClassNth, CssPseudoClassFunctionSelectorList, CssPseudoClassNthSelector,
5+
};
6+
use biome_rowan::{AstNode, SyntaxNodeCast};
7+
8+
declare_rule! {
9+
/// Disallow unmatchable An+B selectors.
10+
///
11+
/// Selectors that always evaluate to 0 will not match any elements.
12+
/// For more details about the An+B syntax, see:
13+
/// https://www.w3.org/TR/css-syntax-3/#anb-microsyntax
14+
///
15+
/// ## Examples
16+
///
17+
/// ### Invalid
18+
///
19+
/// ```css,expect_diagnostic
20+
/// a:nth-child(0) {}
21+
/// ```
22+
///
23+
/// ```css,expect_diagnostic
24+
/// a:nth-last-child(0n) {}
25+
/// ```
26+
///
27+
/// ```css,expect_diagnostic
28+
/// a:nth-of-type(0n+0) {}
29+
/// ```
30+
///
31+
/// ```css,expect_diagnostic
32+
/// a:nth-last-of-type(0 of a) {}
33+
/// ```
34+
///
35+
/// ### Valid
36+
///
37+
/// ```css
38+
/// a:nth-child(1) {}
39+
/// ```
40+
///
41+
/// ```css
42+
/// a:nth-last-child(1n) {}
43+
/// ```
44+
///
45+
/// ```css
46+
/// a:nth-of-type(1n+0) {}
47+
/// ```
48+
///
49+
/// ```css
50+
/// a:nth-last-of-type(1 of a) {}
51+
/// ```
52+
///
53+
pub NoUnmatchableAnbSelector {
54+
version: "next",
55+
name: "noUnmatchableAnbSelector",
56+
recommended: true,
57+
sources: &[RuleSource::Stylelint("selector-anb-no-unmatchable")],
58+
}
59+
}
60+
61+
impl Rule for NoUnmatchableAnbSelector {
62+
type Query = Ast<CssPseudoClassNthSelector>;
63+
type State = CssPseudoClassNthSelector;
64+
type Signals = Option<Self::State>;
65+
type Options = ();
66+
67+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
68+
let node = ctx.query();
69+
let nth = node.nth().ok()?;
70+
if is_unmatchable(&nth) && !is_within_not_pseudo_class(&nth) {
71+
return Some(node.clone());
72+
}
73+
None
74+
}
75+
76+
fn diagnostic(_: &RuleContext<Self>, node: &Self::State) -> Option<RuleDiagnostic> {
77+
let span = node.range();
78+
Some(
79+
RuleDiagnostic::new(
80+
rule_category!(),
81+
span,
82+
markup! {
83+
"This selector will never match any elements."
84+
},
85+
)
86+
.note(markup! {
87+
"Avoid using An+B selectors that always evaluate to 0."
88+
}).note(markup! {
89+
"For more details, see "<Hyperlink href="https://www.w3.org/TR/css-syntax-3/#anb-microsyntax">"the official spec for An+B selectors"</Hyperlink>"."
90+
})
91+
)
92+
}
93+
}
94+
95+
fn is_unmatchable(nth: &AnyCssPseudoClassNth) -> bool {
96+
match nth {
97+
AnyCssPseudoClassNth::CssPseudoClassNthIdentifier(_) => false,
98+
AnyCssPseudoClassNth::CssPseudoClassNth(nth) => {
99+
let coefficient = nth.value();
100+
let constant = nth.offset();
101+
match (coefficient, constant) {
102+
(Some(a), Some(b)) => a.text() == "0" && b.text() == "0",
103+
(Some(a), None) => a.text() == "0",
104+
_ => false,
105+
}
106+
}
107+
AnyCssPseudoClassNth::CssPseudoClassNthNumber(nth) => nth.text() == "0",
108+
}
109+
}
110+
111+
// Check if the nth selector is effective within a `not` pseudo class
112+
// Example: a:not(:nth-child(0)) returns true
113+
// a:not(:not(:nth-child(0))) returns false
114+
fn is_within_not_pseudo_class(node: &AnyCssPseudoClassNth) -> bool {
115+
let number_of_not = node
116+
.syntax()
117+
.ancestors()
118+
.filter_map(|n| n.cast::<CssPseudoClassFunctionSelectorList>())
119+
.filter_map(|n| n.name().ok())
120+
.filter(|n| n.text() == "not")
121+
.count();
122+
number_of_not % 2 == 1
123+
}

crates/biome_css_analyze/src/options.rs

+1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ pub type NoUnknownFunction =
1616
pub type NoUnknownSelectorPseudoElement = < lint :: nursery :: no_unknown_selector_pseudo_element :: NoUnknownSelectorPseudoElement as biome_analyze :: Rule > :: Options ;
1717
pub type NoUnknownUnit =
1818
<lint::nursery::no_unknown_unit::NoUnknownUnit as biome_analyze::Rule>::Options;
19+
pub type NoUnmatchableAnbSelector = < lint :: nursery :: no_unmatchable_anb_selector :: NoUnmatchableAnbSelector as biome_analyze :: Rule > :: Options ;
1920
pub type UseGenericFontNames =
2021
<lint::nursery::use_generic_font_names::UseGenericFontNames as biome_analyze::Rule>::Options;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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) {}
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) {}
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 {}

0 commit comments

Comments
 (0)