Skip to content
Open
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
50 changes: 50 additions & 0 deletions docs/rules/no-unmatchable-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's give a couple of examples here.


## 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/)
59 changes: 59 additions & 0 deletions src/rules/no-unmatchable-selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @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) {
const a = Number(node.a);
const b = Number(node.b);
Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment here describing what's happening? Either node.a or node.b can be null, in which case Number() converts it into 0. I don't think it's obvious from the code that this is an intentional behavior and not an oversight.


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,
},
],
},
],
});