-
-
Notifications
You must be signed in to change notification settings - Fork 946
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7031fe8
commit c7616f4
Showing
7 changed files
with
321 additions
and
15 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,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 * {} | ||
``` |
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,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, | ||
} ], | ||
}) |
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-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 |
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