-
-
Notifications
You must be signed in to change notification settings - Fork 934
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added:
selector-max-combinators
rule.
- Loading branch information
1 parent
970206e
commit 52c184e
Showing
6 changed files
with
348 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
} ], | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-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 |