diff --git a/README.md b/README.md index f8140ff..f0f1c18 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/rules/no-unmatchable-selectors.md b/docs/rules/no-unmatchable-selectors.md new file mode 100644 index 0000000..b3b59d4 --- /dev/null +++ b/docs/rules/no-unmatchable-selectors.md @@ -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. + +## 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: + + +```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: + + +```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/) diff --git a/src/rules/no-unmatchable-selectors.js b/src/rules/no-unmatchable-selectors.js new file mode 100644 index 0000000..2708973 --- /dev/null +++ b/src/rules/no-unmatchable-selectors.js @@ -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); + + if (a <= 0 && b <= 0) { + const pseudo = sourceCode.getParent( + sourceCode.getParent(node), + ); + + context.report({ + loc: pseudo.loc, + messageId: "unmatchableSelector", + data: { selector: sourceCode.getText(pseudo) }, + }); + } + }, + }; + }, +}; diff --git a/tests/rules/no-unmatchable-selectors.test.js b/tests/rules/no-unmatchable-selectors.test.js new file mode 100644 index 0000000..c2b6067 --- /dev/null +++ b/tests/rules/no-unmatchable-selectors.test.js @@ -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, + }, + ], + }, + ], +});