Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added: selector-max-universal rule. #2653

Merged
merged 1 commit into from
Jun 26, 2017
Merged
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 docs/user-guide/example-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ You might want to learn a little about [how rules are named and how they work to
"selector-max-empty-lines": int,
"selector-max-id": 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 @@ -169,6 +169,7 @@ Here are all the rules within stylelint, grouped by the [*thing*](http://apps.wo
- [`selector-max-empty-lines`](../../lib/rules/selector-max-empty-lines/README.md): Limit the number of adjacent empty lines within selectors.
- [`selector-max-id`](../../lib/rules/selector-max-id/README.md): Limit the number of id 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 @@ -142,6 +142,7 @@ const selectorMaxCompoundSelectors = require("./selector-max-compound-selectors"
const selectorMaxEmptyLines = require("./selector-max-empty-lines")
const selectorMaxId = require("./selector-max-id")
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 @@ -322,6 +323,7 @@ module.exports = {
"selector-max-empty-lines": selectorMaxEmptyLines,
"selector-max-id": selectorMaxId,
"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
* * * {}
```

```css
* * {
& * {}
}
```

```css
* * {
& > * {}
}
```

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 * {}
```

```css
/* `*` is inside `:not()`, so it is evaluated separately */
Copy link
Member

Choose a reason for hiding this comment

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

Please change to:

* > * .foo:not(*) {}

* > * .foo:not(*) {}
```
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 universal selectors",
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"remark-preset-lint-recommended": "^2.0.0",
"remark-validate-links": "^6.0.0",
"request": "^2.69.0",
"strip-ansi": "^4.0.0"
"strip-ansi": "^3.0.1"
},
"scripts": {
"benchmark-rule": "node scripts/benchmark-rule.js",
Expand Down