diff --git a/README.md b/README.md index de4be224..795697e6 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ Please also see the [example configs](./docs/examples/) for special cases. - [`selector-nest-combinators`](./src/rules/selector-nest-combinators/README.md): Require or disallow nesting of combinators in selectors. - [`selector-no-redundant-nesting-selector`](./src/rules/selector-no-redundant-nesting-selector/README.md): Disallow redundant nesting selectors (`&`). +- [`selector-no-union-class-name`](./src/rules/selector-no-union-class-name/README.md): Disallow union class names with the parent selector (`&`). ### General / Sheet diff --git a/src/rules/index.js b/src/rules/index.js index 73a1bf75..3cedbd78 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -41,6 +41,7 @@ import partialNoImport from "./partial-no-import"; import percentPlaceholderPattern from "./percent-placeholder-pattern"; import selectorNestCombinators from "./selector-nest-combinators"; import selectorNoRedundantNestingSelector from "./selector-no-redundant-nesting-selector"; +import selectorNoUnionClassName from "./selector-no-union-class-name"; export default { "at-extend-no-missing-placeholder": atExtendNoMissingPlaceholder, @@ -85,5 +86,6 @@ export default { "percent-placeholder-pattern": percentPlaceholderPattern, "partial-no-import": partialNoImport, "selector-nest-combinators": selectorNestCombinators, - "selector-no-redundant-nesting-selector": selectorNoRedundantNestingSelector + "selector-no-redundant-nesting-selector": selectorNoRedundantNestingSelector, + "selector-no-union-class-name": selectorNoUnionClassName }; diff --git a/src/rules/selector-no-union-class-name/README.md b/src/rules/selector-no-union-class-name/README.md new file mode 100644 index 00000000..ef773c94 --- /dev/null +++ b/src/rules/selector-no-union-class-name/README.md @@ -0,0 +1,46 @@ +# selector-no-union-class-name + +Disallow union class names with the parent selector (`&`). + +```scss +.class { + &-union { +//↑ +// This type usage of `&` + } +} +``` + +The following patterns are considered warnings: + +```scss +.class { + &-union {} +} +``` + +```scss +.class { + &_union {} +} +``` + +```scss +.class { + &union {} +} +``` + +The following patterns are *not* considered warnings: + +```scss +.class { + &.foo {} +} +``` + +```scss +.class { + & p {} +} +``` diff --git a/src/rules/selector-no-union-class-name/__tests__/index.js b/src/rules/selector-no-union-class-name/__tests__/index.js new file mode 100644 index 00000000..90e0658e --- /dev/null +++ b/src/rules/selector-no-union-class-name/__tests__/index.js @@ -0,0 +1,68 @@ +import rule, { ruleName, messages } from ".."; + +testRule(rule, { + ruleName, + config: [undefined], + syntax: "scss", + + accept: [ + { + code: ` + .class { + &.foo {} + } + `, + description: "when an ampersand is chained with another class name" + }, + { + code: ` + .class span { + &-union {} + } + `, + description: "when an ampersand parent is not class name" + }, + { + code: ` + .class { + & span {} + } + `, + description: "when an ampersand is chained with conbinator" + } + ], + + reject: [ + { + code: ` + .class { + &-union {} + } + `, + line: 3, + message: messages.rejected, + description: "when an ampersand is chained with union class name (hyphen)" + }, + { + code: ` + .class { + &_union {} + } + `, + line: 3, + message: messages.rejected, + description: + "when an ampersand is chained with union class name (underscore)" + }, + { + code: ` + .class { + &union {} + } + `, + line: 3, + message: messages.rejected, + description: "when an ampersand is chained with union class name (direct)" + } + ] +}); diff --git a/src/rules/selector-no-union-class-name/index.js b/src/rules/selector-no-union-class-name/index.js new file mode 100644 index 00000000..1c6f8a3c --- /dev/null +++ b/src/rules/selector-no-union-class-name/index.js @@ -0,0 +1,51 @@ +import { utils } from "stylelint"; +import { namespace, parseSelector } from "../../utils"; +import { isClassName, isCombinator } from "postcss-selector-parser"; + +export const ruleName = namespace("selector-no-union-class-name"); + +export const messages = utils.ruleMessages(ruleName, { + rejected: "Unexpected union class name with the parent selector (&)" +}); + +export default function(actual) { + return function(root, result) { + const validOptions = utils.validateOptions(result, ruleName, { actual }); + + if (!validOptions) { + return; + } + + root.walkRules(/&/, rule => { + const parentNodes = []; + + parseSelector(rule.parent.selector, result, rule, fullSelector => { + fullSelector.walk(node => parentNodes.push(node)); + }); + + const lastParentNode = parentNodes[parentNodes.length - 1]; + + if (!isClassName(lastParentNode)) return; + + parseSelector(rule.selector, result, rule, fullSelector => { + fullSelector.walkNesting(node => { + const next = node.next(); + + if (!next) return; + + if (isCombinator(next)) return; + + if (isClassName(next)) return; + + utils.report({ + ruleName, + result, + node: rule, + message: messages.rejected, + index: node.sourceIndex + }); + }); + }); + }); + }; +}