Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default defineConfig([
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
| [`no-unmatchable-selectors`](./docs/rules/no-unmatchable-selectors.md) | Disallow unmatchable selectors | yes |
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
| [`selector-complexity`](./docs/rules/selector-complexity.md) | Disallow and limit CSS selectors | no |
Expand Down
55 changes: 55 additions & 0 deletions docs/rules/no-unmatchable-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# no-unmatchable-selectors

Disallow unmatchable selectors.

## Background

An unmatchable selector is one that can never match any element in any document. These are effectively dead code and usually indicate mistakes.

For example:

- `a:nth-child(0)` — the `An+B` formula never produces a positive position (≥ 1).
- `a:nth-child(-n)` — a negative step with no offset never yields a positive position.

## Rule Details

This rule reports selectors that can never match any element.

It currently checks:

- `:nth-*()` pseudo-classes whose `An+B` formulas cannot produce a positive position (≥ 1).

Examples of **incorrect** code:

<!-- prettier-ignore -->
```css
/* eslint css/no-unmatchable-selectors: "error" */

a:nth-child(0) {}
a:nth-child(-n) {}
a:nth-last-child(0 of .active) {}
a:nth-of-type(0n) {}
a:nth-last-of-type(0n+0) {}
```

Examples of **correct** code:

<!-- prettier-ignore -->
```css
/* eslint css/no-unmatchable-selectors: "error" */

a:nth-child(1) {}
a:nth-child(even) {}
a:nth-child(odd) {}
a:nth-last-child(1 of .active) {}
a:nth-of-type(1n) {}
a:nth-last-of-type(1n+0) {}
```

## When Not to Use It

If you intentionally use selectors that can never match (for example, as temporary placeholders during development), then you can safely disable this rule.

## Prior Art

- [`selector-anb-no-unmatchable`](https://stylelint.io/user-guide/rules/selector-anb-no-unmatchable/)
61 changes: 61 additions & 0 deletions src/rules/no-unmatchable-selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @fileoverview Rule to disallow unmatchable selectors.
* @author TKDev7
*/

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/**
* @import { CSSRuleDefinition } from "../types.js"
* @typedef {"unmatchableSelector"} NoUnmatchableSelectorsMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoUnmatchableSelectorsMessageIds }>} NoUnmatchableSelectorsRuleDefinition
*/

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------

/** @type {NoUnmatchableSelectorsRuleDefinition} */
export default {
meta: {
type: "problem",

docs: {
description: "Disallow unmatchable selectors",
recommended: true,
url: "https://github.com/eslint/css/blob/main/docs/rules/no-unmatchable-selectors.md",
},

messages: {
unmatchableSelector:
"Unexpected unmatchable selector '{{selector}}'.",
},
},

create(context) {
const { sourceCode } = context;

return {
AnPlusB(node) {
// Either node.a or node.b can be null; Number(null) === 0.
// This coercion is intentional so that omitted coefficients are treated as 0.
const a = Number(node.a);
const b = Number(node.b);

if (a <= 0 && b <= 0) {
const pseudo = sourceCode.getParent(
sourceCode.getParent(node),
);

context.report({
loc: pseudo.loc,
messageId: "unmatchableSelector",
data: { selector: sourceCode.getText(pseudo) },
});
}
},
};
},
};
264 changes: 264 additions & 0 deletions tests/rules/no-unmatchable-selectors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/**
* @fileoverview Tests for no-unmatchable-selectors rule.
* @author TKDev7
*/

//------------------------------------------------------------------------------
// Imports
//------------------------------------------------------------------------------

import rule from "../../src/rules/no-unmatchable-selectors.js";
import css from "../../src/index.js";
import { RuleTester } from "eslint";

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester({
plugins: { css },
language: "css/css",
});

ruleTester.run("no-unmatchable-selectors", rule, {
valid: [
"li:nth-child(1) {}",
"li:nth-child(n) {}",
"li:nth-child(-n+2) {}",
"li:nth-child(-2n+1) {}",
"li:nth-child(0n+1) {}",
"li:nth-child(2n) {}",
"li:nth-child(2n+0) {}",
"li:nth-child(2n-0) {}",
"li:nth-child(2n+2) {}",
"li:nth-child(1 of a) {}",
"li:nth-last-child(1) {}",
"li:nth-of-type(1) {}",
"li:nth-last-of-type(1) {}",
"li:nth-child(odd) {}",
"li:nth-child(even) {}",
],
invalid: [
{
code: "li:nth-child(0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 16,
},
],
},
{
code: "li:nth-child(0n) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0n)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 17,
},
],
},
{
code: "li:nth-child(+0n) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(+0n)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 18,
},
],
},
{
code: "li:nth-child(-0n) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(-0n)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 18,
},
],
},
{
code: "li:nth-child(0n+0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0n+0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 19,
},
],
},
{
code: "li:nth-child(0n-0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0n-0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 19,
},
],
},
{
code: "li:nth-child(-0n-0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(-0n-0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 20,
},
],
},
{
code: "li:nth-child(0n-2) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0n-2)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 19,
},
],
},
{
code: "li:nth-child(-n) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(-n)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 17,
},
],
},
{
code: "li:nth-child(-2n) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(-2n)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 18,
},
],
},
{
code: "li:nth-child(-3n+0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(-3n+0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 20,
},
],
},
{
code: "li:nth-child(-1) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(-1)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 17,
},
],
},
{
code: "li:nth-child(0 of a) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0 of a)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 21,
},
],
},
{
code: "li:nth-last-child(0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-last-child(0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 21,
},
],
},
{
code: "li:nth-of-type(0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-of-type(0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 18,
},
],
},
{
code: "li:nth-last-of-type(0) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-last-of-type(0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 23,
},
],
},
{
code: "li:nth-child(0), li:nth-child(1) {}",
errors: [
{
messageId: "unmatchableSelector",
data: { selector: ":nth-child(0)" },
line: 1,
column: 3,
endLine: 1,
endColumn: 16,
},
],
},
],
});