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
d229a67
commit 02ad62b
Showing
7 changed files
with
361 additions
and
25 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,77 @@ | ||
# selector-max-attribute | ||
|
||
Limit the number of attribute selectors in a selector. | ||
|
||
```css | ||
[rel="external"] {} | ||
/** ↑ | ||
* This type of selector */ | ||
``` | ||
|
||
This rule resolves nested selectors before counting the number of attribute 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 attribute selectors allowed. | ||
|
||
For example, with `2`: | ||
|
||
The following patterns are considered warnings: | ||
|
||
```css | ||
[type="number"][name="quality"][data-attribute="value"] {} | ||
``` | ||
|
||
```css | ||
[type="number"][name="quality"][disabled] {} | ||
``` | ||
|
||
```css | ||
[type="number"][name="quality"] { | ||
& [data-attribute="value"] {} | ||
} | ||
``` | ||
|
||
```css | ||
[type="number"][name="quality"] { | ||
& [disabled] {} | ||
} | ||
``` | ||
|
||
```css | ||
[type="number"][name="quality"] { | ||
& > [data-attribute="value"] {} | ||
} | ||
``` | ||
|
||
```css | ||
/* `[data-attribute="value"][disabled]` is inside `:not()`, so it is evaluated separately */ | ||
input:not([type="text"][data-attribute="value"][disabled]) {} | ||
``` | ||
|
||
The following patterns are *not* considered warnings: | ||
|
||
```css | ||
[type="text"] {} | ||
``` | ||
|
||
```css | ||
[type="text"][name="message"] {} | ||
``` | ||
|
||
```css | ||
[type="text"][disabled] | ||
``` | ||
|
||
```css | ||
/* each selector in a selector list is evaluated separately */ | ||
[type="text"][name="message"], | ||
[type="number"][name="quality"] {} | ||
``` | ||
|
||
```css | ||
/* `[data-attribute="value"][disabled]` is inside `:not()`, so it is evaluated separately */ | ||
[type="text"][name="message"]:not([data-attribute="value"][disabled]) {} | ||
``` |
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,180 @@ | ||
"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: ":root { --custom-property-set: {} }", | ||
} ], | ||
|
||
reject: [ { | ||
code: "[foo] {}", | ||
message: messages.expected("[foo]", 0), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "a[rel=\"external\"] {}", | ||
message: messages.expected("a[rel=\"external\"]", 0), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "a, .foo[type=\"text\"] {}", | ||
message: messages.expected(".foo[type=\"text\"]", 0), | ||
line: 1, | ||
column: 4, | ||
}, { | ||
code: "a > [foo] {}", | ||
message: messages.expected("a > [foo]", 0), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "a[rel='external'] {}", | ||
message: messages.expected("a[rel='external']", 0), | ||
line: 1, | ||
column: 1, | ||
} ], | ||
}) | ||
|
||
// Standard tests | ||
testRule(rule, { | ||
ruleName, | ||
config: [2], | ||
|
||
accept: [ { | ||
code: "[type=\"text\"] {}", | ||
description: "fewer than max classes", | ||
}, { | ||
code: "[type=\"text\"][disabled]:hover {}", | ||
description: "pseudo selectors", | ||
}, { | ||
code: "[type=\"text\"][name=\"message\"] {}", | ||
description: "exactly max classes", | ||
}, { | ||
code: "[type=\"text\"][disabled] {}", | ||
description: "exactly max classes", | ||
}, { | ||
code: "[type=\"text\"] [type=\"number\"] {}", | ||
description: "compound selector", | ||
}, { | ||
code: "[type=\"text\"], \n[type=\"number\"] {}", | ||
description: "multiple selectors: fewer than max classes", | ||
}, { | ||
code: "[type=\"text\"][name=\"message\"], \n[type=\"password\"][name=\"password\"] {}", | ||
description: "multiple selectors: exactly max classes", | ||
}, { | ||
code: "[type=\"text\"][disabled], \n[type=\"password\"][disabled] {}", | ||
description: "multiple selectors: exactly max classes", | ||
}, { | ||
code: "[type=\"text\"][name=\"message\"]:not([type=\"number\"][name=\"quality\"]) {}", | ||
description: ":not(): inside and outside", | ||
}, { | ||
code: "[type=\"text\"] { [name=\"message\"] {} }", | ||
description: "nested selectors", | ||
}, { | ||
code: "[type=\"text\"] { [name=\"message\"] > & {} }", | ||
description: "nested selectors: parent selector", | ||
}, { | ||
code: "[type=\"text\"], [name=\"message\"] { & > [data-attribute=\"value\"] {} }", | ||
description: "nested selectors: superfluous parent selector", | ||
}, { | ||
code: "@media print { [type=\"text\"][name=\"message\"] {} }", | ||
description: "media query: parent", | ||
}, { | ||
code: "[type=\"text\"] { @media print { [name=\"message\"] {} } }", | ||
description: "media query: nested", | ||
} ], | ||
|
||
reject: [ { | ||
code: "[type=\"text\"][name=\"message\"][data-attribute=\"value\"] {}", | ||
description: "greater than max classes", | ||
message: messages.expected("[type=\"text\"][name=\"message\"][data-attribute=\"value\"]", 2), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "[type=\"text\"][name=\"message\"][disabled] {}", | ||
description: "greater than max classes with attribute selector without value", | ||
message: messages.expected("[type=\"text\"][name=\"message\"][disabled]", 2), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "[type=\"text\"] [name=\"message\"] [data-attribute=\"value\"] {}", | ||
description: "compound selector: greater than max classes", | ||
message: messages.expected("[type=\"text\"] [name=\"message\"] [data-attribute=\"value\"]", 2), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "[type=\"text\"], \n[type=\"number\"][name=\"quality\"][data-attribute=\"value\"] {}", | ||
description: "multiple selectors: greater than max classes", | ||
message: messages.expected("[type=\"number\"][name=\"quality\"][data-attribute=\"value\"]", 2), | ||
line: 2, | ||
column: 1, | ||
}, { | ||
code: "[type=\"text\"], \n[type=\"number\"][name=\"quality\"][disabled] {}", | ||
description: "multiple selectors: greater than max classes", | ||
message: messages.expected("[type=\"number\"][name=\"quality\"][disabled]", 2), | ||
line: 2, | ||
column: 1, | ||
}, { | ||
code: ":not([type=\"text\"][name=\"message\"][data-attribute=\"value\"]) {}", | ||
description: ":not(): greater than max classes, inside", | ||
message: messages.expected("[type=\"text\"][name=\"message\"][data-attribute=\"value\"]", 2), | ||
line: 1, | ||
column: 6, | ||
}, { | ||
code: ":not([type=\"text\"][name=\"message\"][disabled]) {}", | ||
description: ":not(): greater than max classes, inside", | ||
message: messages.expected("[type=\"text\"][name=\"message\"][disabled]", 2), | ||
line: 1, | ||
column: 6, | ||
}, { | ||
code: "[type=\"text\"][name=\"message\"][data-attribute=\"value\"] :not([data-attribute-2=\"value\"]) {}", | ||
description: ":not(): greater than max classes, outside", | ||
message: messages.expected("[type=\"text\"][name=\"message\"][data-attribute=\"value\"] :not([data-attribute-2=\"value\"])", 2), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "[type=\"text\"][name=\"message\"][data-attribute=\"value\"]:not([disabled]) {}", | ||
description: ":not(): greater than max classes, outside", | ||
message: messages.expected("[type=\"text\"][name=\"message\"][data-attribute=\"value\"]:not([disabled])", 2), | ||
line: 1, | ||
column: 1, | ||
}, { | ||
code: "[type=\"text\"] { &:hover > [data-attribute=\"value\"][data-attribute-2=\"value\"] {} }", | ||
description: "nested selectors: greater than max classes", | ||
message: messages.expected("[type=\"text\"]:hover > [data-attribute=\"value\"][data-attribute-2=\"value\"]", 2), | ||
line: 1, | ||
column: 17, | ||
} ], | ||
}) | ||
|
||
// SCSS tests | ||
testRule(rule, { | ||
ruleName, | ||
config: [0], | ||
syntax: "scss", | ||
|
||
accept: [ { | ||
code: ".[#{$interpolation}] {}", | ||
description: "scss: ignore variable interpolation", | ||
}, { | ||
code: "#{$interpolation}[type=\"text\"] {}", | ||
description: "scss: ignore variable interpolation", | ||
}, { | ||
code: "[type=\"text\"]#{$interpolation} { margin: { left: 0; top: 0; }; }", | ||
description: "scss: nested properties", | ||
} ], | ||
}) |
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-attribute" | ||
|
||
const messages = ruleMessages(ruleName, { | ||
expected: (selector, max) => `Expected "${selector}" to have no more than ${max} attribute selector(s)`, | ||
}) | ||
|
||
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 === "attribute" ? 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 |
Oops, something went wrong.