Skip to content

Commit b590cec

Browse files
crisbetoandrewseguin
authored andcommitted
build: add stylelint rule to validate ampersands in theme selectors (#10949)
Replaces the `selector-nested-pattern-scoped` Stylelint rule with a new rule called `no-ampersand-beyond-selector-start`. The new rule will enforce that ampersands can only be used at the start of selectors in theme mixins. This will catch issues like #10889 and #10904 where having an ampersand anywhere else in the selector will work in most cases, but will break down once the mixin is wrapped in a selector.
1 parent 39341ea commit b590cec

File tree

3 files changed

+68
-49
lines changed

3 files changed

+68
-49
lines changed

stylelint-config.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"plugins": [
33
"./tools/stylelint/no-prefixes/no-prefixes.js",
4-
"./tools/stylelint/selector-nested-pattern-scoped/index.js",
4+
"./tools/stylelint/no-ampersand-beyond-selector-start/index.js",
55
"./tools/stylelint/selector-no-deep/index.js",
66
"./tools/stylelint/no-nested-mixin/index.js",
77
"./tools/stylelint/no-concrete-rules/index.js",
@@ -11,8 +11,7 @@
1111
"material/no-prefixes": [["last 2 versions", "not ie <= 10", "not ie_mob <= 10"]],
1212
"material/selector-no-deep": true,
1313
"material/no-nested-mixin": true,
14-
"material/selector-nested-pattern-scoped": [".*[^&]$", {
15-
"message": "The & operator is not allowed at the end of theme selectors.",
14+
"material/no-ampersand-beyond-selector-start": [true, {
1615
"filePattern": "-theme\\.scss$"
1716
}],
1817
"material/no-concrete-rules": [true, {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const stylelint = require('stylelint');
2+
const path = require('path');
3+
const isStandardSyntaxRule = require('stylelint/lib/utils/isStandardSyntaxRule');
4+
const isStandardSyntaxSelector = require('stylelint/lib/utils/isStandardSyntaxSelector');
5+
6+
const ruleName = 'material/no-ampersand-beyond-selector-start';
7+
const messages = stylelint.utils.ruleMessages(ruleName, {
8+
expected: () => 'Ampersand is only allowed at the beginning of a selector',
9+
});
10+
11+
/**
12+
* Stylelint rule that doesn't allow for an ampersand to be used anywhere
13+
* except at the start of a selector. Skips private mixins.
14+
*
15+
* Based off the `selector-nested-pattern` Stylelint rule.
16+
* Source: https://github.com/stylelint/stylelint/blob/master/lib/rules/selector-nested-pattern/
17+
*/
18+
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
19+
return (root, result) => {
20+
if (!isEnabled) return;
21+
22+
const filePattern = new RegExp(options.filePattern);
23+
const fileName = path.basename(root.source.input.file);
24+
25+
if (!filePattern.test(fileName)) return;
26+
27+
root.walkRules(rule => {
28+
if (
29+
rule.parent.type === 'rule' &&
30+
isStandardSyntaxRule(rule) &&
31+
isStandardSyntaxSelector(rule.selector) &&
32+
// Using the ampersand at the beginning is fine, anything else can cause issues in themes.
33+
rule.selector.indexOf('&') > 0) {
34+
35+
const mixinName = getClosestMixinName(rule);
36+
37+
// Skip rules inside private mixins.
38+
if (!mixinName || !mixinName.startsWith('_')) {
39+
stylelint.utils.report({
40+
result,
41+
ruleName,
42+
message: messages.expected(),
43+
node: rule
44+
});
45+
}
46+
}
47+
});
48+
};
49+
50+
/** Walks up the AST and finds the name of the closest mixin. */
51+
function getClosestMixinName(node) {
52+
let parent = node.parent;
53+
54+
while (parent) {
55+
if (parent.type === 'atrule' && parent.name === 'mixin') {
56+
return parent.params;
57+
}
58+
59+
parent = parent.parent;
60+
}
61+
}
62+
});
63+
64+
plugin.ruleName = ruleName;
65+
plugin.messages = messages;
66+
module.exports = plugin;

tools/stylelint/selector-nested-pattern-scoped/index.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)