diff --git a/stylelint-config.json b/stylelint-config.json index 7b4cdf09ab2..fa7e7cbd7a0 100644 --- a/stylelint-config.json +++ b/stylelint-config.json @@ -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, { diff --git a/tools/stylelint/no-ampersand-beyond-selector-start/index.js b/tools/stylelint/no-ampersand-beyond-selector-start/index.js new file mode 100644 index 00000000000..ac35a220d93 --- /dev/null +++ b/tools/stylelint/no-ampersand-beyond-selector-start/index.js @@ -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; diff --git a/tools/stylelint/selector-nested-pattern-scoped/index.js b/tools/stylelint/selector-nested-pattern-scoped/index.js deleted file mode 100644 index 18f6d0d8481..00000000000 --- a/tools/stylelint/selector-nested-pattern-scoped/index.js +++ /dev/null @@ -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;