From 8c808d02d0cca720029fa94614a37a4228fa0b14 Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Sun, 8 May 2016 21:51:59 +0300 Subject: [PATCH] New rule `selector-pseudo-class-no-unknown`. --- CHANGELOG.md | 1 + docs/user-guide/example-config.md | 1 + docs/user-guide/rules.md | 1 + .../README.md | 79 +++++++++++++++ .../__tests__/index.js | 97 +++++++++++++++++++ .../selector-pseudo-class-no-unknown/index.js | 63 ++++++++++++ .../__tests__/isKnownPseudoClass-test.js | 65 +++++++++++++ .../__tests__/isKnownPseudoElement-test.js | 10 +- src/utils/index.js | 1 + src/utils/isKnownPseudoClass.js | 27 ++++++ src/utils/isKnownPseudoElement.js | 16 ++- 11 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 src/rules/selector-pseudo-class-no-unknown/README.md create mode 100644 src/rules/selector-pseudo-class-no-unknown/__tests__/index.js create mode 100644 src/rules/selector-pseudo-class-no-unknown/index.js create mode 100644 src/utils/__tests__/isKnownPseudoClass-test.js create mode 100644 src/utils/isKnownPseudoClass.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b247fe5c..0096d87edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Head +- Added: `selector-pseudo-class-no-unknown` rule. - Fixed: `declaration-block-no-ignored-properties` now detects use of `min-width` and `max-width` with inline, table-row, table-row-group, table-column and table-column-group elements. # 6.3.3 diff --git a/docs/user-guide/example-config.md b/docs/user-guide/example-config.md index 49430ee1f0..52a4f6f492 100644 --- a/docs/user-guide/example-config.md +++ b/docs/user-guide/example-config.md @@ -127,6 +127,7 @@ You might want to learn a little about [how rules are named and how they work to "selector-no-universal": true, "selector-no-vendor-prefix": true, "selector-pseudo-class-case": "lower"|"upper", + "selector-pseudo-class-no-unknown": true, "selector-pseudo-class-parentheses-space-inside": "always"|"never", "selector-pseudo-element-case": "lower"|"upper", "selector-pseudo-element-colon-notation": "single"|"double", diff --git a/docs/user-guide/rules.md b/docs/user-guide/rules.md index 95330255ae..fbc16e5c46 100644 --- a/docs/user-guide/rules.md +++ b/docs/user-guide/rules.md @@ -150,6 +150,7 @@ Here are all the rules within stylelint, grouped by the [*thing*](http://apps.wo - [`selector-no-universal`](../../src/rules/selector-no-universal/README.md): Disallow the universal selector. - [`selector-no-vendor-prefix`](../../src/rules/selector-no-vendor-prefix/README.md): Disallow vendor prefixes for selectors. - [`selector-pseudo-class-case`](../../src/rules/selector-pseudo-class-case/README.md): Specify lowercase or uppercase for pseudo-class selectors. +- [`selector-pseudo-class-no-unknown`](../../src/rules/selector-pseudo-class-no-unknown/README.md): Disallow unknown pseudo-class selectors. - [`selector-pseudo-class-parentheses-space-inside`](../../src/rules/selector-pseudo-class-parentheses-space-inside/README.md): Require a single space or disallow whitespace on the inside of the parentheses within pseudo-class selectors. - [`selector-pseudo-element-case`](../../src/rules/selector-pseudo-element-case/README.md): Specify lowercase or uppercase for pseudo-element selectors. - [`selector-pseudo-element-colon-notation`](../../src/rules/selector-pseudo-element-colon-notation/README.md): Specify single or double colon notation for applicable pseudo-elements. diff --git a/src/rules/selector-pseudo-class-no-unknown/README.md b/src/rules/selector-pseudo-class-no-unknown/README.md new file mode 100644 index 0000000000..74e437bc70 --- /dev/null +++ b/src/rules/selector-pseudo-class-no-unknown/README.md @@ -0,0 +1,79 @@ +# selector-pseudo-class-no-unknown + +Disallow unknown pseudo-class selectors. + +```css + a:hover {} +/** ↑ + * This pseudo-class selector */ +``` + +All vendor-prefixes pseudo-class selectors are ignored. + +The following patterns are considered warnings: + +```css +a:unknown { } +``` + +```css +a:UNKNOWN { } +``` + +```css +a:hoverr { } +``` + +The following patterns are *not* considered warnings: + +```css +a:hover { } +``` + +```css +a:focus { } +``` + +```css +:not(p) { } +``` + +```css +input:-moz-placeholder { } +``` + +## Optional options + +### `ignorePseudoClasses: ["array", "of", "pseudo-classes"]` + +Allow unknown pseudo-class selectors. + +For example, given: + +```js +["pseudo-class"] +``` + +The following patterns are considered warnings: + +```css +a:unknown { } +``` + +The following patterns are *not* considered warnings: + +```css +a:pseudo-class { } +``` + +```css +a:hover { } +``` + +```css +:not(p) { } +``` + +```css +input:-moz-placeholder { } +``` diff --git a/src/rules/selector-pseudo-class-no-unknown/__tests__/index.js b/src/rules/selector-pseudo-class-no-unknown/__tests__/index.js new file mode 100644 index 0000000000..c64e52cb1c --- /dev/null +++ b/src/rules/selector-pseudo-class-no-unknown/__tests__/index.js @@ -0,0 +1,97 @@ +import { testRule } from "../../../testUtils" + +import rule, { ruleName, messages } from ".." + +testRule(rule, { + ruleName, + config: [true], + skipBasicChecks: true, + + accept: [ { + code: "a:hover { }", + }, { + code: "a:Hover { }", + }, { + code: "a:hOvEr { }", + }, { + code: "a:HOVER { }", + }, { + code: "a:before { }", + }, { + code: "a::before { }", + }, { + code: "input:not([type='submit']) { }", + }, { + code: ":matches(section, article, aside, nav) h1 { }", + }, { + code: "section:has(h1, h2, h3, h4, h5, h6) { }", + }, { + code: ":root { }", + }, { + code: "p:has(img):not(:has(:not(img))) { }", + }, { + code: "div.sidebar:has(*:nth-child(5)):not(:has(*:nth-child(6))) { }", + }, { + code: "div :nth-child(2 of .widget) { }", + }, { + code: "a:hover::before { }", + }, { + code: "a:-moz-placeholder { }", + }, { + code: "a,\nb > .foo:hover { }", + } ], + + reject: [ { + code: "a:unknown { }", + message: messages.rejected(":unknown"), + line: 1, + column: 2, + }, { + code: "a:Unknown { }", + message: messages.rejected(":Unknown"), + line: 1, + column: 2, + }, { + code: "a:uNkNoWn { }", + message: messages.rejected(":uNkNoWn"), + line: 1, + column: 2, + }, { + code: "a:UNKNOWN { }", + message: messages.rejected(":UNKNOWN"), + line: 1, + column: 2, + }, { + code: "a:pseudo-class { }", + message: messages.rejected(":pseudo-class"), + line: 1, + column: 2, + }, { + code: "a:unknown::before { }", + message: messages.rejected(":unknown"), + line: 1, + column: 2, + }, { + code: "a,\nb > .foo:error { }", + message: messages.rejected(":error"), + line: 2, + column: 9, + } ], +}) + +testRule(rule, { + ruleName, + config: [ true, { ignorePseudoClasses: ["unknown"] } ], + skipBasicChecks: true, + + accept: [{ + code: "a:unknown { }", + }], + + reject: [{ + code: "a:pseudo-class { }", + message: messages.rejected(":pseudo-class"), + line: 1, + column: 2, + }], +}) diff --git a/src/rules/selector-pseudo-class-no-unknown/index.js b/src/rules/selector-pseudo-class-no-unknown/index.js new file mode 100644 index 0000000000..10ca3fe7ca --- /dev/null +++ b/src/rules/selector-pseudo-class-no-unknown/index.js @@ -0,0 +1,63 @@ +import { isString } from "lodash" +import selectorParser from "postcss-selector-parser" +import { vendor } from "postcss" +import { + isKnownPseudoClass, + isKnownPseudoElement, + report, + ruleMessages, + validateOptions, +} from "../../utils" + +export const ruleName = "selector-pseudo-class-no-unknown" + +export const messages = ruleMessages(ruleName, { + rejected: (u) => `Unexpected unknown pseudo-class selector "${u}"`, +}) + +export default function (actual, options) { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { actual }, { + actual: options, + possible: { + ignorePseudoClasses: [isString], + }, + optional: true, + }) + if (!validOptions) { return } + + root.walkRules(rule => { + const selector = rule.selector + + if (selector.indexOf(":") === -1) { return } + + selectorParser(selectorTree => { + selectorTree.walkPseudos(pseudoNode => { + const pseudoClass = pseudoNode.value + + // Ignore pseudo-elements + if (pseudoClass.indexOf("::") !== -1) { return } + + const pseudoClassName = pseudoClass.replace(/:+/, "") + + if (vendor.prefix(pseudoClassName) + || isKnownPseudoClass(pseudoClassName) + || isKnownPseudoElement(pseudoClassName) + ) { return } + + const ignorePseudoElements = options && options.ignorePseudoClasses || [] + + if (ignorePseudoElements.indexOf(pseudoClassName) !== -1) { return } + + report({ + message: messages.rejected(pseudoClass), + node: rule, + index: pseudoNode.sourceIndex, + ruleName, + result, + }) + }) + }).process(selector) + }) + } +} diff --git a/src/utils/__tests__/isKnownPseudoClass-test.js b/src/utils/__tests__/isKnownPseudoClass-test.js new file mode 100644 index 0000000000..d3bc191d69 --- /dev/null +++ b/src/utils/__tests__/isKnownPseudoClass-test.js @@ -0,0 +1,65 @@ +import test from "tape" +import isKnownPseudoClass from "../isKnownPseudoClass" + +test("isKnownUnit", t => { + t.notOk(isKnownPseudoClass("unknown")) + t.notOk(isKnownPseudoClass("uNkNoWn")) + t.notOk(isKnownPseudoClass("UNKNOWN")) + t.notOk(isKnownPseudoClass("pseudo-class")) + + t.ok(isKnownPseudoClass("active")) + t.ok(isKnownPseudoClass("aCtIvE")) + t.ok(isKnownPseudoClass("ACTIVE")) + t.ok(isKnownPseudoClass("any-link")) + t.ok(isKnownPseudoClass("blank")) + t.ok(isKnownPseudoClass("checked")) + t.ok(isKnownPseudoClass("contains")) + t.ok(isKnownPseudoClass("current")) + t.ok(isKnownPseudoClass("default")) + t.ok(isKnownPseudoClass("dir")) + t.ok(isKnownPseudoClass("disabled")) + t.ok(isKnownPseudoClass("drop")) + t.ok(isKnownPseudoClass("empty")) + t.ok(isKnownPseudoClass("enabled")) + t.ok(isKnownPseudoClass("first-child")) + t.ok(isKnownPseudoClass("first-of-type")) + t.ok(isKnownPseudoClass("focus")) + t.ok(isKnownPseudoClass("focus-within")) + t.ok(isKnownPseudoClass("fullscreen")) + t.ok(isKnownPseudoClass("future")) + t.ok(isKnownPseudoClass("has")) + t.ok(isKnownPseudoClass("hover")) + t.ok(isKnownPseudoClass("indeterminate")) + t.ok(isKnownPseudoClass("in-range")) + t.ok(isKnownPseudoClass("invalid")) + t.ok(isKnownPseudoClass("lang")) + t.ok(isKnownPseudoClass("last-child")) + t.ok(isKnownPseudoClass("last-of-type")) + t.ok(isKnownPseudoClass("link")) + t.ok(isKnownPseudoClass("matches")) + t.ok(isKnownPseudoClass("not")) + t.ok(isKnownPseudoClass("nth-child")) + t.ok(isKnownPseudoClass("nth-column")) + t.ok(isKnownPseudoClass("nth-last-child")) + t.ok(isKnownPseudoClass("nth-last-column")) + t.ok(isKnownPseudoClass("nth-last-of-type")) + t.ok(isKnownPseudoClass("nth-of-type")) + t.ok(isKnownPseudoClass("only-child")) + t.ok(isKnownPseudoClass("only-of-type")) + t.ok(isKnownPseudoClass("optional")) + t.ok(isKnownPseudoClass("out-of-range")) + t.ok(isKnownPseudoClass("past")) + t.ok(isKnownPseudoClass("placeholder-shown")) + t.ok(isKnownPseudoClass("read-only")) + t.ok(isKnownPseudoClass("read-write")) + t.ok(isKnownPseudoClass("required")) + t.ok(isKnownPseudoClass("root")) + t.ok(isKnownPseudoClass("scope")) + t.ok(isKnownPseudoClass("target")) + t.ok(isKnownPseudoClass("user-error")) + t.ok(isKnownPseudoClass("user-invalid")) + t.ok(isKnownPseudoClass("val")) + t.ok(isKnownPseudoClass("valid")) + t.ok(isKnownPseudoClass("visited")) + t.end() +}) diff --git a/src/utils/__tests__/isKnownPseudoElement-test.js b/src/utils/__tests__/isKnownPseudoElement-test.js index 7be6917d4a..92472de4e6 100644 --- a/src/utils/__tests__/isKnownPseudoElement-test.js +++ b/src/utils/__tests__/isKnownPseudoElement-test.js @@ -3,8 +3,8 @@ import isKnownPseudoElement from "../isKnownPseudoElement" test("isKnownUnit", t => { t.notOk(isKnownPseudoElement("pseudo")) - t.notOk(isKnownPseudoElement("pseudo")) - t.notOk(isKnownPseudoElement("element")) + t.notOk(isKnownPseudoElement("pSeUdO")) + t.notOk(isKnownPseudoElement("PSEUDO")) t.notOk(isKnownPseudoElement("element")) t.ok(isKnownPseudoElement("before")) t.ok(isKnownPseudoElement("bEfOrE")) @@ -24,5 +24,11 @@ test("isKnownUnit", t => { t.ok(isKnownPseudoElement("placeholder")) t.ok(isKnownPseudoElement("shadow")) t.ok(isKnownPseudoElement("content")) + + t.ok(isKnownPseudoElement("first-line", { only: "oneColonNotation" })) + t.ok(isKnownPseudoElement("first-letter", { only: "oneColonNotation" })) + t.ok(isKnownPseudoElement("before", { only: "oneColonNotation" })) + t.ok(isKnownPseudoElement("after", { only: "oneColonNotation" })) + t.notOk(isKnownPseudoElement("selection", { only: "oneColonNotation" })) t.end() }) diff --git a/src/utils/index.js b/src/utils/index.js index 6af99c154c..ecd1a44fa9 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -14,6 +14,7 @@ export { default as hasEmptyBlock } from "./hasEmptyBlock" export { default as isAutoprefixable } from "./isAutoprefixable" export { default as isCustomProperty } from "./isCustomProperty" export { default as isKeyframeRule } from "./isKeyframeRule" +export { default as isKnownPseudoClass } from "./isKnownPseudoClass" export { default as isKnownPseudoElement } from "./isKnownPseudoElement" export { default as isKnownUnit } from "./isKnownUnit" export { default as isLowerSpecificity } from "./isLowerSpecificity" diff --git a/src/utils/isKnownPseudoClass.js b/src/utils/isKnownPseudoClass.js new file mode 100644 index 0000000000..61f3ba6d28 --- /dev/null +++ b/src/utils/isKnownPseudoClass.js @@ -0,0 +1,27 @@ +/** + * Check is known pseudo-class + * + * @param {string} Pseudo-class + * @return {boolean} If `true`, the unit is known + */ + +// https://drafts.csswg.org/selectors-4/#overview +const knownPseudoClasses = new Set([ + "active", "any-link", "blank", "checked", + "contains", "current", "default", "dir", + "disabled", "drop", "empty", "enabled", + "first-child", "first-of-type", "focus", "focus-within", + "fullscreen", "future", "has", "hover", + "indeterminate", "in-range", "invalid", "lang", + "last-child", "last-of-type", "link", "matches", + "not", "nth-child", "nth-column", "nth-last-child", + "nth-last-column", "nth-last-of-type", "nth-of-type", "only-child", + "only-of-type", "optional", "out-of-range", "past", + "placeholder-shown", "read-only", "read-write", "required", + "root", "scope", "target", "user-error", + "user-invalid", "val", "valid", "visited", +]) + +export default function (pseudoClass) { + return knownPseudoClasses.has(pseudoClass.toLowerCase()) +} diff --git a/src/utils/isKnownPseudoElement.js b/src/utils/isKnownPseudoElement.js index 6bb84ad214..5fae0c2fec 100644 --- a/src/utils/isKnownPseudoElement.js +++ b/src/utils/isKnownPseudoElement.js @@ -6,13 +6,23 @@ */ // https://drafts.csswg.org/selectors-4/#overview -const knownPseudoElements = new Set([ +const knownOneColonNotationPseudoElements = new Set([ + "first-line", "first-letter", "before", "after", +]) + +const knownTwoColonNotationPseudoElements = new Set([ "before", "after", "first-line", "first-letter", "selection", "spelling-error", "grammar-error", "backdrop", "marker", "placeholder", "shadow", "content", ]) -export default function (pseudoElement) { - return knownPseudoElements.has(pseudoElement.toLowerCase()) +export default function (pseudoElement, { only = false } = {}) { + const pseudoElementLowerCase = pseudoElement.toLowerCase() + + if (only && only === "oneColonNotation") { + return knownOneColonNotationPseudoElements.has(pseudoElementLowerCase) + } + + return knownTwoColonNotationPseudoElements.has(pseudoElementLowerCase) }