Skip to content

Commit

Permalink
Added: selector-max-universal rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jun 20, 2017
1 parent 7031fe8 commit c7616f4
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/user-guide/example-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ You might want to learn a little about [how rules are named and how they work to
"selector-max-class": int,
"selector-max-compound-selectors": int,
"selector-max-specificity": string,
"selector-max-universal": int,
"selector-nested-pattern": string,
"selector-no-attribute": true,
"selector-no-combinator": true,
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Here are all the rules within stylelint, grouped by the [*thing*](http://apps.wo
- [`selector-max-class`](../../lib/rules/selector-max-class/README.md): Limit the number of classes in a selector.
- [`selector-max-compound-selectors`](../../lib/rules/selector-max-compound-selectors/README.md): Limit the number of compound selectors in a selector.
- [`selector-max-specificity`](../../lib/rules/selector-max-specificity/README.md): Limit the specificity of selectors.
- [`selector-max-universal`](../../lib/rules/selector-max-universal/README.md): Limit the number of universal selectors in a selector.
- [`selector-nested-pattern`](../../lib/rules/selector-nested-pattern/README.md): Specify a pattern for the selectors of rules nested within rules.
- [`selector-no-attribute`](../../lib/rules/selector-no-attribute/README.md): Disallow attribute selectors.
- [`selector-no-combinator`](../../lib/rules/selector-no-combinator/README.md): Disallow combinators in selectors.
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const selectorMaxCompoundSelectors = require("./selector-max-compound-selectors"
const selectorMaxEmptyLines = require("./selector-max-empty-lines")
const selectorMaxClass = require("./selector-max-class")
const selectorMaxSpecificity = require("./selector-max-specificity")
const selectorMaxUniversal = require("./selector-max-universal")
const selectorNestedPattern = require("./selector-nested-pattern")
const selectorNoAttribute = require("./selector-no-attribute")
const selectorNoCombinator = require("./selector-no-combinator")
Expand Down Expand Up @@ -316,6 +317,7 @@ module.exports = {
"selector-max-compound-selectors": selectorMaxCompoundSelectors,
"selector-max-empty-lines": selectorMaxEmptyLines,
"selector-max-specificity": selectorMaxSpecificity,
"selector-max-universal": selectorMaxUniversal,
"selector-nested-pattern": selectorNestedPattern,
"selector-no-attribute": selectorNoAttribute,
"selector-no-empty": selectorNoEmpty,
Expand Down
66 changes: 66 additions & 0 deletions lib/rules/selector-max-universal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# selector-max-universal

Limit the number of universal selectors in a selector.

```css
* {}
/** ↑
* This universal selector */
```

This rule resolves nested selectors before counting the number of universal selectors. Each selector in a [selector list](https://www.w3.org/TR/selectors4/#selector-list) is evaluated separately.

The `:not()` pseudo-class is also evaluated separately. The rule processes the argument as if it were an independent selector, and the result does not count toward the total for the entire selector.

## Options

`int`: Maximum universal selectors allowed.

For example, with `2`:

The following patterns are considered violations:

```css
*.foo *.bar *.baz {}
```

```css
*.foo *.bar {
& *.baz {}
}
```

```css
*.foo *.bar {
& > * {}
}
```

```css
/* `*` is inside `:not()`, so it is evaluated separately */
.foo:not(*) {}
```

The following patterns are *not* considered violations:

```css
* {}
```

```css
* * {}
```

```css
.foo * {}
```

```css
*.foo * {}
```

```css
/* each selector in a selector list is evaluated separately */
*.foo *,
*.bar * {}
```
161 changes: 161 additions & 0 deletions lib/rules/selector-max-universal/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"use strict"

const messages = require("..").messages
const ruleName = require("..").ruleName
const rules = require("../../../rules")

const rule = rules[ruleName]

// Sanity checks
testRule(rule, {
ruleName,
config: [0],

accept: [ {
code: "foo {}",
}, {
code: ".bar {}",
}, {
code: "foo .bar {}",
}, {
code: "#foo {}",
}, {
code: "[foo] {}",
}, {
code: ":root { --foo: 1px; }",
description: "custom property in root",
}, {
code: "html { --foo: 1px; }",
description: "custom property in selector",
}, {
code: ":root { --custom-property-set: {} }",
description: "custom property set in root",
}, {
code: "html { --custom-property-set: {} }",
description: "custom property set in selector",
} ],

reject: [ {
code: "* {}",
message: messages.expected("*", 0),
line: 1,
column: 1,
}, {
code: ".bar * {}",
message: messages.expected(".bar *", 0),
line: 1,
column: 1,
}, {
code: "*.bar {}",
message: messages.expected("*.bar", 0),
line: 1,
column: 1,
}, {
code: "* [lang^=en] {}",
message: messages.expected("* [lang^=en]", 0),
line: 1,
column: 1,
}, {
code: "*[lang^=en] {}",
message: messages.expected("*[lang^=en]", 0),
line: 1,
column: 1,
}, {
code: ".foo, .bar, *.baz {}",
message: messages.expected("*.baz", 0),
line: 1,
column: 13,
}, {
code: "* #id {}",
message: messages.expected("* #id", 0),
line: 1,
column: 1,
}, {
code: "*#id {}",
message: messages.expected("*#id", 0),
line: 1,
column: 1,
}, {
code: ".foo* {}",
message: messages.expected(".foo*", 0),
line: 1,
column: 1,
}, {
code: "*:hover {}",
message: messages.expected("*:hover", 0),
line: 1,
column: 1,
}, {
code: ":not(*) {}",
message: messages.expected("*", 0),
line: 1,
column: 6,
} ],
})

// Standard tests
testRule(rule, {
ruleName,
config: [2],

accept: [ {
code: "* {}",
description: "fewer than max universal selectors",
}, {
code: "*:hover {}",
description: "pseudo selectors",
}, {
code: "* * {}",
description: "compound selector",
}, {
code: "*, \n* {}",
description: "multiple selectors: fewer than max universal selectors",
}, {
code: "* *, \n* * {}",
description: "multiple selectors: exactly max universal selectors",
}, {
code: "* *:not(*) {}",
description: ":not(): outside and inside",
}, {
code: "* { * {} }",
description: "nested selectors",
}, {
code: "* { * > & {} }",
description: "nested selectors: parent selector",
}, {
code: "*, * { & > * {} }",
description: "nested selectors: superfluous parent selector",
}, {
code: "@media print { * * {} }",
description: "media query: parent",
}, {
code: "* { @media print { * {} } }",
description: "media query: nested",
} ],

reject: [ {
code: "* * * {}",
description: "compound selector: greater than max universal selectors",
message: messages.expected("* * *", 2),
line: 1,
column: 1,
}, {
code: "*, \n* * * {}",
description: "multiple selectors: greater than max classes",
message: messages.expected("* * *", 2),
line: 2,
column: 1,
}, {
code: "* * *:not(*) {}",
description: ":not(): greater than max universal selectors, outside",
message: messages.expected("* * *:not(*)", 2),
line: 1,
column: 1,
}, {
code: "* { &:hover > * * {} }",
description: "nested selectors: greater than max universal selectors",
message: messages.expected("*:hover > * *", 2),
line: 1,
column: 5,
} ],
})
75 changes: 75 additions & 0 deletions lib/rules/selector-max-universal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use strict"

const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule")
const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector")
const parseSelector = require("../../utils/parseSelector")
const report = require("../../utils/report")
const ruleMessages = require("../../utils/ruleMessages")
const validateOptions = require("../../utils/validateOptions")
const resolvedNestedSelector = require("postcss-resolve-nested-selector")

const ruleName = "selector-max-universal"

const messages = ruleMessages(ruleName, {
expected: (selector, max) => `Expected "${selector}" to have no more than ${max} universal ${max === 1 ? "selector" : "selectors"}`,
})

function rule(max) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: max,
possible: [
function (max) {
return typeof max === "number" && max >= 0
},
],
})
if (!validOptions) {
return
}

function checkSelector(selectorNode, ruleNode) {
const count = selectorNode.reduce((total, childNode) => {
// Only traverse inside actual selectors and :not()
if (childNode.type === "selector" || childNode.value === ":not") {
checkSelector(childNode, ruleNode)
}

return total += (childNode.type === "universal" ? 1 : 0)
}, 0)

if (selectorNode.type !== "root" && selectorNode.type !== "pseudo" && count > max) {
report({
ruleName,
result,
node: ruleNode,
message: messages.expected(selectorNode, max),
word: selectorNode,
})
}
}

root.walkRules(ruleNode => {
if (!isStandardSyntaxRule(ruleNode)) {
return
}
if (!isStandardSyntaxSelector(ruleNode.selector)) {
return
}
if (ruleNode.nodes.some(node => [ "rule", "atrule" ].indexOf(node.type) !== -1)) {
// Skip unresolved nested selectors
return
}

ruleNode.selectors.forEach(selector => {
resolvedNestedSelector(selector, ruleNode).forEach(resolvedSelector => {
parseSelector(resolvedSelector, result, ruleNode, container => checkSelector(container, ruleNode))
})
})
})
}
}

rule.ruleName = ruleName
rule.messages = messages
module.exports = rule
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,37 @@
"node": ">=4.2.1"
},
"dependencies": {
"autoprefixer": "^6.0.0",
"balanced-match": "^0.4.0",
"chalk": "^1.1.1",
"autoprefixer": "^6.7.7",
"balanced-match": "^0.4.2",
"chalk": "^1.1.3",
"colorguard": "^1.2.0",
"cosmiconfig": "^2.1.1",
"debug": "^2.6.0",
"doiuse": "^2.4.1",
"cosmiconfig": "^2.1.3",
"debug": "^2.6.8",
"doiuse": "^2.6.0",
"execall": "^1.0.0",
"file-entry-cache": "^2.0.0",
"get-stdin": "^5.0.0",
"globby": "^6.0.0",
"get-stdin": "^5.0.1",
"globby": "^6.1.0",
"globjoin": "^0.1.4",
"html-tags": "^2.0.0",
"ignore": "^3.2.0",
"ignore": "^3.3.3",
"imurmurhash": "^0.1.4",
"known-css-properties": "^0.2.0",
"lodash": "^4.17.4",
"log-symbols": "^1.0.2",
"mathml-tag-names": "^2.0.0",
"meow": "^3.3.0",
"meow": "^3.7.0",
"micromatch": "^2.3.11",
"normalize-selector": "^0.2.0",
"pify": "^2.3.0",
"postcss": "^5.0.20",
"postcss": "^5.2.17",
"postcss-less": "^0.14.0",
"postcss-media-query-parser": "^0.2.0",
"postcss-media-query-parser": "^0.2.3",
"postcss-reporter": "^3.0.0",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-scss": "^0.4.0",
"postcss-selector-parser": "^2.1.1",
"postcss-value-parser": "^3.1.1",
"postcss-scss": "^0.4.1",
"postcss-selector-parser": "^2.2.3",
"postcss-value-parser": "^3.3.0",
"resolve-from": "^3.0.0",
"specificity": "^0.3.0",
"string-width": "^2.0.0",
Expand Down

0 comments on commit c7616f4

Please sign in to comment.