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.
This commit is contained in:
Kristiyan Kostadinov
2018-04-26 01:04:06 -04:00
committed by Andrew Seguin
parent 39341eab1f
commit b590cec04b
3 changed files with 68 additions and 49 deletions
+2 -3
View File
@@ -1,7 +1,7 @@
{
"plugins": [
"./tools/stylelint/no-prefixes/no-prefixes.js",
"./tools/stylelint/selector-nested-pattern-scoped/index.js",
"./tools/stylelint/no-ampersand-beyond-selector-start/index.js",
"./tools/stylelint/selector-no-deep/index.js",
"./tools/stylelint/no-nested-mixin/index.js",
"./tools/stylelint/no-concrete-rules/index.js",
@@ -11,8 +11,7 @@
"material/no-prefixes": [["last 2 versions", "not ie <= 10", "not ie_mob <= 10"]],
"material/selector-no-deep": true,
"material/no-nested-mixin": true,
"material/selector-nested-pattern-scoped": [".*[^&]$", {
"message": "The & operator is not allowed at the end of theme selectors.",
"material/no-ampersand-beyond-selector-start": [true, {
"filePattern": "-theme\\.scss$"
}],
"material/no-concrete-rules": [true, {
@@ -0,0 +1,66 @@
const stylelint = require('stylelint');
const path = require('path');
const isStandardSyntaxRule = require('stylelint/lib/utils/isStandardSyntaxRule');
const isStandardSyntaxSelector = require('stylelint/lib/utils/isStandardSyntaxSelector');
const ruleName = 'material/no-ampersand-beyond-selector-start';
const messages = stylelint.utils.ruleMessages(ruleName, {
expected: () => 'Ampersand is only allowed at the beginning of a selector',
});
/**
* Stylelint rule that doesn't allow for an ampersand to be used anywhere
* except at the start of a selector. Skips private mixins.
*
* Based off the `selector-nested-pattern` Stylelint rule.
* Source: https://github.com/stylelint/stylelint/blob/master/lib/rules/selector-nested-pattern/
*/
const plugin = stylelint.createPlugin(ruleName, (isEnabled, options) => {
return (root, result) => {
if (!isEnabled) return;
const filePattern = new RegExp(options.filePattern);
const fileName = path.basename(root.source.input.file);
if (!filePattern.test(fileName)) return;
root.walkRules(rule => {
if (
rule.parent.type === 'rule' &&
isStandardSyntaxRule(rule) &&
isStandardSyntaxSelector(rule.selector) &&
// Using the ampersand at the beginning is fine, anything else can cause issues in themes.
rule.selector.indexOf('&') > 0) {
const mixinName = getClosestMixinName(rule);
// Skip rules inside private mixins.
if (!mixinName || !mixinName.startsWith('_')) {
stylelint.utils.report({
result,
ruleName,
message: messages.expected(),
node: rule
});
}
}
});
};
/** Walks up the AST and finds the name of the closest mixin. */
function getClosestMixinName(node) {
let parent = node.parent;
while (parent) {
if (parent.type === 'atrule' && parent.name === 'mixin') {
return parent.params;
}
parent = parent.parent;
}
}
});
plugin.ruleName = ruleName;
plugin.messages = messages;
module.exports = plugin;
@@ -1,46 +0,0 @@
const stylelint = require('stylelint');
const path = require('path');
const isStandardSyntaxRule = require('stylelint/lib/utils/isStandardSyntaxRule');
const isStandardSyntaxSelector = require('stylelint/lib/utils/isStandardSyntaxSelector');
const ruleName = 'material/selector-nested-pattern-scoped';
const messages = stylelint.utils.ruleMessages(ruleName, {
expected: selector => `Expected nested selector '${selector}' to match specified pattern`,
});
/**
* Re-implementation of the `selector-nested-pattern` Stylelint rule, allowing us
* to scope it to a particular set of files via the custom `filePattern` option. The
* primary use-case is to be able to apply the rule only to theme files.
*
* Reference: https://stylelint.io/user-guide/rules/selector-nested-pattern/
* Source: https://github.com/stylelint/stylelint/blob/master/lib/rules/selector-nested-pattern/
*/
const plugin = stylelint.createPlugin(ruleName, (pattern, options) => {
return (root, result) => {
const selectorPattern = new RegExp(pattern);
const filePattern = new RegExp(options.filePattern);
const fileName = path.basename(root.source.input.file);
if (!filePattern.test(fileName)) return;
root.walkRules(rule => {
if (rule.parent.type === 'rule' &&
isStandardSyntaxRule(rule) &&
isStandardSyntaxSelector(rule.selector) &&
!selectorPattern.test(rule.selector)) {
stylelint.utils.report({
result,
ruleName,
message: messages.expected(rule.selector),
node: rule
});
}
});
};
});
plugin.ruleName = ruleName;
plugin.messages = messages;
module.exports = plugin;