Skip to content

Commit adaa397

Browse files
authored
feat: add no-unmatchable-selectors rule (#301)
* feat: add `no-unmatchable-selectors` rule * add background examples and clarify coercion
1 parent 69a76b1 commit adaa397

File tree

4 files changed

+381
-0
lines changed

4 files changed

+381
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export default defineConfig([
7676
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
7777
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
7878
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
79+
| [`no-unmatchable-selectors`](./docs/rules/no-unmatchable-selectors.md) | Disallow unmatchable selectors | yes |
7980
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
8081
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
8182
| [`selector-complexity`](./docs/rules/selector-complexity.md) | Disallow and limit CSS selectors | no |
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# no-unmatchable-selectors
2+
3+
Disallow unmatchable selectors.
4+
5+
## Background
6+
7+
An unmatchable selector is one that can never match any element in any document. These are effectively dead code and usually indicate mistakes.
8+
9+
For example:
10+
11+
- `a:nth-child(0)` — the `An+B` formula never produces a positive position (≥ 1).
12+
- `a:nth-child(-n)` — a negative step with no offset never yields a positive position.
13+
14+
## Rule Details
15+
16+
This rule reports selectors that can never match any element.
17+
18+
It currently checks:
19+
20+
- `:nth-*()` pseudo-classes whose `An+B` formulas cannot produce a positive position (≥ 1).
21+
22+
Examples of **incorrect** code:
23+
24+
<!-- prettier-ignore -->
25+
```css
26+
/* eslint css/no-unmatchable-selectors: "error" */
27+
28+
a:nth-child(0) {}
29+
a:nth-child(-n) {}
30+
a:nth-last-child(0 of .active) {}
31+
a:nth-of-type(0n) {}
32+
a:nth-last-of-type(0n+0) {}
33+
```
34+
35+
Examples of **correct** code:
36+
37+
<!-- prettier-ignore -->
38+
```css
39+
/* eslint css/no-unmatchable-selectors: "error" */
40+
41+
a:nth-child(1) {}
42+
a:nth-child(even) {}
43+
a:nth-child(odd) {}
44+
a:nth-last-child(1 of .active) {}
45+
a:nth-of-type(1n) {}
46+
a:nth-last-of-type(1n+0) {}
47+
```
48+
49+
## When Not to Use It
50+
51+
If you intentionally use selectors that can never match (for example, as temporary placeholders during development), then you can safely disable this rule.
52+
53+
## Prior Art
54+
55+
- [`selector-anb-no-unmatchable`](https://stylelint.io/user-guide/rules/selector-anb-no-unmatchable/)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @fileoverview Rule to disallow unmatchable selectors.
3+
* @author TKDev7
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Type Definitions
8+
//-----------------------------------------------------------------------------
9+
10+
/**
11+
* @import { CSSRuleDefinition } from "../types.js"
12+
* @typedef {"unmatchableSelector"} NoUnmatchableSelectorsMessageIds
13+
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoUnmatchableSelectorsMessageIds }>} NoUnmatchableSelectorsRuleDefinition
14+
*/
15+
16+
//-----------------------------------------------------------------------------
17+
// Rule Definition
18+
//-----------------------------------------------------------------------------
19+
20+
/** @type {NoUnmatchableSelectorsRuleDefinition} */
21+
export default {
22+
meta: {
23+
type: "problem",
24+
25+
docs: {
26+
description: "Disallow unmatchable selectors",
27+
recommended: true,
28+
url: "https://github.com/eslint/css/blob/main/docs/rules/no-unmatchable-selectors.md",
29+
},
30+
31+
messages: {
32+
unmatchableSelector:
33+
"Unexpected unmatchable selector '{{selector}}'.",
34+
},
35+
},
36+
37+
create(context) {
38+
const { sourceCode } = context;
39+
40+
return {
41+
AnPlusB(node) {
42+
// Either node.a or node.b can be null; Number(null) === 0.
43+
// This coercion is intentional so that omitted coefficients are treated as 0.
44+
const a = Number(node.a);
45+
const b = Number(node.b);
46+
47+
if (a <= 0 && b <= 0) {
48+
const pseudo = sourceCode.getParent(
49+
sourceCode.getParent(node),
50+
);
51+
52+
context.report({
53+
loc: pseudo.loc,
54+
messageId: "unmatchableSelector",
55+
data: { selector: sourceCode.getText(pseudo) },
56+
});
57+
}
58+
},
59+
};
60+
},
61+
};
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/**
2+
* @fileoverview Tests for no-unmatchable-selectors rule.
3+
* @author TKDev7
4+
*/
5+
6+
//------------------------------------------------------------------------------
7+
// Imports
8+
//------------------------------------------------------------------------------
9+
10+
import rule from "../../src/rules/no-unmatchable-selectors.js";
11+
import css from "../../src/index.js";
12+
import { RuleTester } from "eslint";
13+
14+
//------------------------------------------------------------------------------
15+
// Tests
16+
//------------------------------------------------------------------------------
17+
18+
const ruleTester = new RuleTester({
19+
plugins: { css },
20+
language: "css/css",
21+
});
22+
23+
ruleTester.run("no-unmatchable-selectors", rule, {
24+
valid: [
25+
"li:nth-child(1) {}",
26+
"li:nth-child(n) {}",
27+
"li:nth-child(-n+2) {}",
28+
"li:nth-child(-2n+1) {}",
29+
"li:nth-child(0n+1) {}",
30+
"li:nth-child(2n) {}",
31+
"li:nth-child(2n+0) {}",
32+
"li:nth-child(2n-0) {}",
33+
"li:nth-child(2n+2) {}",
34+
"li:nth-child(1 of a) {}",
35+
"li:nth-last-child(1) {}",
36+
"li:nth-of-type(1) {}",
37+
"li:nth-last-of-type(1) {}",
38+
"li:nth-child(odd) {}",
39+
"li:nth-child(even) {}",
40+
],
41+
invalid: [
42+
{
43+
code: "li:nth-child(0) {}",
44+
errors: [
45+
{
46+
messageId: "unmatchableSelector",
47+
data: { selector: ":nth-child(0)" },
48+
line: 1,
49+
column: 3,
50+
endLine: 1,
51+
endColumn: 16,
52+
},
53+
],
54+
},
55+
{
56+
code: "li:nth-child(0n) {}",
57+
errors: [
58+
{
59+
messageId: "unmatchableSelector",
60+
data: { selector: ":nth-child(0n)" },
61+
line: 1,
62+
column: 3,
63+
endLine: 1,
64+
endColumn: 17,
65+
},
66+
],
67+
},
68+
{
69+
code: "li:nth-child(+0n) {}",
70+
errors: [
71+
{
72+
messageId: "unmatchableSelector",
73+
data: { selector: ":nth-child(+0n)" },
74+
line: 1,
75+
column: 3,
76+
endLine: 1,
77+
endColumn: 18,
78+
},
79+
],
80+
},
81+
{
82+
code: "li:nth-child(-0n) {}",
83+
errors: [
84+
{
85+
messageId: "unmatchableSelector",
86+
data: { selector: ":nth-child(-0n)" },
87+
line: 1,
88+
column: 3,
89+
endLine: 1,
90+
endColumn: 18,
91+
},
92+
],
93+
},
94+
{
95+
code: "li:nth-child(0n+0) {}",
96+
errors: [
97+
{
98+
messageId: "unmatchableSelector",
99+
data: { selector: ":nth-child(0n+0)" },
100+
line: 1,
101+
column: 3,
102+
endLine: 1,
103+
endColumn: 19,
104+
},
105+
],
106+
},
107+
{
108+
code: "li:nth-child(0n-0) {}",
109+
errors: [
110+
{
111+
messageId: "unmatchableSelector",
112+
data: { selector: ":nth-child(0n-0)" },
113+
line: 1,
114+
column: 3,
115+
endLine: 1,
116+
endColumn: 19,
117+
},
118+
],
119+
},
120+
{
121+
code: "li:nth-child(-0n-0) {}",
122+
errors: [
123+
{
124+
messageId: "unmatchableSelector",
125+
data: { selector: ":nth-child(-0n-0)" },
126+
line: 1,
127+
column: 3,
128+
endLine: 1,
129+
endColumn: 20,
130+
},
131+
],
132+
},
133+
{
134+
code: "li:nth-child(0n-2) {}",
135+
errors: [
136+
{
137+
messageId: "unmatchableSelector",
138+
data: { selector: ":nth-child(0n-2)" },
139+
line: 1,
140+
column: 3,
141+
endLine: 1,
142+
endColumn: 19,
143+
},
144+
],
145+
},
146+
{
147+
code: "li:nth-child(-n) {}",
148+
errors: [
149+
{
150+
messageId: "unmatchableSelector",
151+
data: { selector: ":nth-child(-n)" },
152+
line: 1,
153+
column: 3,
154+
endLine: 1,
155+
endColumn: 17,
156+
},
157+
],
158+
},
159+
{
160+
code: "li:nth-child(-2n) {}",
161+
errors: [
162+
{
163+
messageId: "unmatchableSelector",
164+
data: { selector: ":nth-child(-2n)" },
165+
line: 1,
166+
column: 3,
167+
endLine: 1,
168+
endColumn: 18,
169+
},
170+
],
171+
},
172+
{
173+
code: "li:nth-child(-3n+0) {}",
174+
errors: [
175+
{
176+
messageId: "unmatchableSelector",
177+
data: { selector: ":nth-child(-3n+0)" },
178+
line: 1,
179+
column: 3,
180+
endLine: 1,
181+
endColumn: 20,
182+
},
183+
],
184+
},
185+
{
186+
code: "li:nth-child(-1) {}",
187+
errors: [
188+
{
189+
messageId: "unmatchableSelector",
190+
data: { selector: ":nth-child(-1)" },
191+
line: 1,
192+
column: 3,
193+
endLine: 1,
194+
endColumn: 17,
195+
},
196+
],
197+
},
198+
{
199+
code: "li:nth-child(0 of a) {}",
200+
errors: [
201+
{
202+
messageId: "unmatchableSelector",
203+
data: { selector: ":nth-child(0 of a)" },
204+
line: 1,
205+
column: 3,
206+
endLine: 1,
207+
endColumn: 21,
208+
},
209+
],
210+
},
211+
{
212+
code: "li:nth-last-child(0) {}",
213+
errors: [
214+
{
215+
messageId: "unmatchableSelector",
216+
data: { selector: ":nth-last-child(0)" },
217+
line: 1,
218+
column: 3,
219+
endLine: 1,
220+
endColumn: 21,
221+
},
222+
],
223+
},
224+
{
225+
code: "li:nth-of-type(0) {}",
226+
errors: [
227+
{
228+
messageId: "unmatchableSelector",
229+
data: { selector: ":nth-of-type(0)" },
230+
line: 1,
231+
column: 3,
232+
endLine: 1,
233+
endColumn: 18,
234+
},
235+
],
236+
},
237+
{
238+
code: "li:nth-last-of-type(0) {}",
239+
errors: [
240+
{
241+
messageId: "unmatchableSelector",
242+
data: { selector: ":nth-last-of-type(0)" },
243+
line: 1,
244+
column: 3,
245+
endLine: 1,
246+
endColumn: 23,
247+
},
248+
],
249+
},
250+
{
251+
code: "li:nth-child(0), li:nth-child(1) {}",
252+
errors: [
253+
{
254+
messageId: "unmatchableSelector",
255+
data: { selector: ":nth-child(0)" },
256+
line: 1,
257+
column: 3,
258+
endLine: 1,
259+
endColumn: 16,
260+
},
261+
],
262+
},
263+
],
264+
});

0 commit comments

Comments
 (0)