Skip to content

Commit

Permalink
New rule selector-pseudo-class-no-unknown.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed May 10, 2016
1 parent 913afee commit 8c808d0
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 5 deletions.
1 change: 1 addition & 0 deletions 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
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/example-config.md
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/rules.md
Expand Up @@ -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.
Expand Down
79 changes: 79 additions & 0 deletions 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 { }
```
97 changes: 97 additions & 0 deletions 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,
}],
})
63 changes: 63 additions & 0 deletions 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)
})
}
}
65 changes: 65 additions & 0 deletions 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()
})
10 changes: 8 additions & 2 deletions src/utils/__tests__/isKnownPseudoElement-test.js
Expand Up @@ -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"))
Expand All @@ -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()
})
1 change: 1 addition & 0 deletions src/utils/index.js
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions 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())
}

0 comments on commit 8c808d0

Please sign in to comment.