|
| 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; |
0 commit comments