Skip to content

Commit

Permalink
Added: selector-max-combinators rule. (#2658)
Browse files Browse the repository at this point in the history
  • Loading branch information
evilebottnawi authored and jeddy3 committed Jun 26, 2017
1 parent 970206e commit 56f9fca
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/user-guide/example-config.md
Expand Up @@ -134,6 +134,7 @@ You might want to learn a little about [how rules are named and how they work to
"selector-list-comma-space-before": "always"|"never"|"always-single-line"|"never-single-line",
"selector-max-attribute": int,
"selector-max-class": int,
"selector-max-combinators": int,
"selector-max-compound-selectors": int,
"selector-max-empty-lines": int,
"selector-max-id": int,
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/rules.md
Expand Up @@ -165,6 +165,7 @@ Here are all the rules within stylelint, grouped by the [*thing*](http://apps.wo
- [`selector-id-pattern`](../../lib/rules/selector-id-pattern/README.md): Specify a pattern for id selectors.
- [`selector-max-attribute`](../../lib/rules/selector-max-attribute/README.md): Limit the number of attribute selectors in a selector.
- [`selector-max-class`](../../lib/rules/selector-max-class/README.md): Limit the number of classes in a selector.
- [`selector-max-combinators`](../../lib/rules/selector-max-combinators/README.md): Limit the number of combinators 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-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.
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/index.js
Expand Up @@ -138,6 +138,7 @@ const selectorListCommaSpaceAfter = require("./selector-list-comma-space-after")
const selectorListCommaSpaceBefore = require("./selector-list-comma-space-before")
const selectorMaxAttribute = require("./selector-max-attribute")
const selectorMaxClass = require("./selector-max-class")
const selectorMaxCombinators = require("./selector-max-combinators")
const selectorMaxCompoundSelectors = require("./selector-max-compound-selectors")
const selectorMaxEmptyLines = require("./selector-max-empty-lines")
const selectorMaxId = require("./selector-max-id")
Expand Down Expand Up @@ -319,6 +320,7 @@ module.exports = {
"selector-list-comma-space-before": selectorListCommaSpaceBefore,
"selector-max-attribute": selectorMaxAttribute,
"selector-max-class": selectorMaxClass,
"selector-max-combinators": selectorMaxCombinators,
"selector-max-compound-selectors": selectorMaxCompoundSelectors,
"selector-max-empty-lines": selectorMaxEmptyLines,
"selector-max-id": selectorMaxId,
Expand Down
63 changes: 63 additions & 0 deletions lib/rules/selector-max-combinators/README.md
@@ -0,0 +1,63 @@
# selector-max-combinators

Limit the number of combinators in a selector.

```css
a > b + c ~ d e { color: pink; }
/** ↑ ↑ ↑ ↑
* These are combinators */
```

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

## Options

`int`: Maximum combinators selectors allowed.

For example, with `2`:

The following patterns are considered violations:

```css
a b ~ c + d {}
```

```css
a b ~ c {
& > d {}
}
```

```css
a b {
& ~ c {
& + d {}
}
}
```

The following patterns are *not* considered violations:

```css
a {}
```

```css
a b {}
```

```css
a b ~ c {}
```

```css
a b {
& ~ c {}
}
```

```css
/* each selector in a selector list is evaluated separately */
a b,
c > d {}
```
206 changes: 206 additions & 0 deletions lib/rules/selector-max-combinators/__tests__/index.js
@@ -0,0 +1,206 @@
"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 {}",
}, {
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: "foo bar {}",
message: messages.expected("foo bar", 0),
line: 1,
column: 1,
}, {
code: "foo + bar {}",
message: messages.expected("foo + bar", 0),
line: 1,
column: 1,
}, {
code: "foo > bar {}",
message: messages.expected("foo > bar", 0),
line: 1,
column: 1,
}, {
code: "foo ~ bar {}",
message: messages.expected("foo ~ bar", 0),
line: 1,
column: 1,
}, {
code: "foo bar, .baz {}",
message: messages.expected("foo bar", 0),
line: 1,
column: 1,
}, {
code: ".foo, bar baz {}",
message: messages.expected("bar baz", 0),
line: 1,
column: 7,
}, {
code: "\t.foo,\n\tbar baz {}",
message: messages.expected("bar baz", 0),
line: 2,
column: 2,
}, {
code: "foo#bar ~ baz {}",
message: messages.expected("foo#bar ~ baz", 0),
line: 1,
column: 1,
} ],
})

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

accept: [ {
code: "foo bar {}",
description: "fewer than max combinators",
}, {
code: "foo bar:hover {}",
description: "pseudo selectors",
}, {
code: "foo bar, \nbaz quux {}",
description: "multiple selectors: fewer than max combinators",
}, {
code: "foo bar:not(baz) {}",
description: ":not(): outside",
}, {
code: "foo { bar {} }",
description: "nested selectors",
}, {
code: "foo { bar { baz {} } }",
description: "nested selectors",
}, {
code: "foo { bar > & {} }",
description: "nested selectors: parent selector",
}, {
code: "foo, bar { & > quux {} }",
description: "nested selectors: superfluous parent selector",
}, {
code: "@media print { foo bar {} }",
description: "media query: parent",
}, {
code: "foo { @media print { bar {} } }",
description: "media query: nested",
} ],

reject: [ {
code: "foo bar baz quux {}",
description: "compound selector: greater than max combinators",
message: messages.expected("foo bar baz quux", 2),
line: 1,
column: 1,
}, {
code: "foo, \nbar baz quux bat {}",
description: "multiple selectors: greater than max classes",
message: messages.expected("bar baz quux bat", 2),
line: 2,
column: 1,
}, {
code: "foo bar baz quux:not(bat) {}",
description: ":not(): greater than max combinators, outside",
message: messages.expected("foo bar baz quux:not(bat)", 2),
line: 1,
column: 1,
}, {
code: "foo { bar { baz { quux {} } } }",
description: "nested selectors: greater than max combinators",
message: messages.expected("foo bar baz quux", 2),
line: 1,
column: 19,
}, {
code: "foo { bar > & baz ~ quux {} }",
description: "nested selectors: parent selector",
message: messages.expected("bar > foo baz ~ quux", 2),
line: 1,
column: 7,
}, {
code: "foo, bar { & > baz ~ quux + bat {} }",
description: "nested selectors: superfluous parent selector",
message: messages.expected("foo > baz ~ quux + bat", 2),
line: 1,
column: 12,
} ],
})

testRule(rule, {
ruleName,
config: [true],
skipBasicChecks: true,
syntax: "scss",

accept: [ {
code: "@keyframes spin { #{50% - $n} {} }",
}, {
code: "@for $n from 1 through 10 { .n-#{$n} { content: \"n: #{1 + 1}\"; } }",
description: "ignore sass interpolation inside @for",
}, {
code: "@for $n from 1 through 10 { .n#{$n}-#{$n} { content: \"n: #{1 + 1}\"; } }",
description: "ignore multiple sass interpolations in a selector inside @for",
}, {
code: "@for $n from 1 through 10 { .n#{$n}n#{$n} { content: \"n: #{1 + 1}\"; } }",
description: "ignore multiple sass interpolations in a selector inside @for",
}, {
code: "@each $n in $vals { .n-#{$n} { content: \"n: #{1 + 1}\"; } }",
description: "ignore sass interpolation inside @each",
}, {
code: "@while $n < 10 { .n-#{$n} { content: \"n: #{1 + 1}\"; } }",
description: "ignore sass interpolation inside @while",
}, {
code: "div:nth-child(#{map-get($foo, bar)}) {}",
description: "ignore sass map-get interpolation",
} ],
})

testRule(rule, {
ruleName,
config: [true],
skipBasicChecks: true,
syntax: "less",

accept: [ {
code: ".for(@n: 1) when (@n <= 10) { .n-@{n} { content: %(\"n: %d\", 1 + 1); } .for(@n + 1); }",
description: "ignore Less interpolation inside .for",
}, {
code: ".for(@n: 1) when (@n <= 10) { .n-@{n}-@{n} { content: %(\"n: %d\", 1 + 1); } .for(@n + 1); }",
description: "ignore multiple Less interpolations in a selector inside .for",
}, {
code: ".for(@n: 1) when (@n <= 10) { .n-@{n}n@{n} { content: %(\"n: %d\", 1 + 1); } .for(@n + 1); }",
description: "ignore multiple Less interpolations in a selector inside .for",
}, {
code: ".each(@vals, @n: 1) when (@n <= length(@vals)) { @val: extract(@vals, @n); .n-@{val} { content: %(\"n: %d\", 1 + 1); } .each(@vals, @n + 1); }",
description: "ignore Less interpolation inside .each",
}, {
code: ".while(@n: 0) when (@n < 10) { .n-@{n} { content: %(\"n: %d\", 1 + 1); } .while(@n + 1) }",
description: "ignore Less interpolation inside .while",
} ],
})
75 changes: 75 additions & 0 deletions lib/rules/selector-max-combinators/index.js
@@ -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-combinators"

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

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
if (childNode.type === "selector") {
checkSelector(childNode, ruleNode)
}

return total += (childNode.type === "combinator" ? 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

0 comments on commit 56f9fca

Please sign in to comment.