diff --git a/.changeset/unlucky-dolphins-fetch.md b/.changeset/unlucky-dolphins-fetch.md new file mode 100644 index 0000000000..b21872e6de --- /dev/null +++ b/.changeset/unlucky-dolphins-fetch.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Added: `ignore: ["inside-block"]` and `splitList` to `selector-disallowed-list` diff --git a/lib/rules/selector-disallowed-list/README.md b/lib/rules/selector-disallowed-list/README.md index ba430d5060..4cf479abd9 100644 --- a/lib/rules/selector-disallowed-list/README.md +++ b/lib/rules/selector-disallowed-list/README.md @@ -66,3 +66,50 @@ a ```css a[href] {} ``` + +## Optional secondary options + +### `splitList: true | false` (default: `false`) + +Split selector lists into individual selectors. + +For example, with `true`. + +Given: + +```json +[".foo"] +``` + +The following pattern is considered a problem: + + +```css +.bar, .foo {} +``` + +The following pattern is _not_ considered a problem: + + +```css +.bar .foo {} +``` + +### `ignore: ["inside-block"]` + +Ignore selectors that are inside a block. + +Given: + +```json +[".foo"] +``` + +The following pattern is _not_ considered a problem: + + +```css +.bar { + .foo {} +} +``` diff --git a/lib/rules/selector-disallowed-list/__tests__/index.js b/lib/rules/selector-disallowed-list/__tests__/index.js index 1987ef7f82..3668436749 100644 --- a/lib/rules/selector-disallowed-list/__tests__/index.js +++ b/lib/rules/selector-disallowed-list/__tests__/index.js @@ -66,6 +66,63 @@ testRule({ ], }); +testRule({ + ruleName, + config: [/^\.(foo)[,\s]?(?!\w).*$/, { ignore: ['inside-block'], splitList: true }], + accept: [ + { + code: 'a.foo {}', + }, + { + code: 'a\n>\n.foo {}', + }, + { + code: '.bar > a > .foo {}', + }, + { + code: '.bar { .foo {} }', + }, + { + code: '.fooo {}', + }, + ], + + reject: [ + { + code: '.foo {}', + message: messages.rejected('.foo'), + line: 1, + column: 1, + endLine: 1, + endColumn: 5, + }, + { + code: '.foo > .bar {}', + message: messages.rejected('.foo > .bar'), + line: 1, + column: 1, + endLine: 1, + endColumn: 12, + }, + { + code: '.foo, .bar {}', + message: messages.rejected('.foo'), + line: 1, + column: 1, + endLine: 1, + endColumn: 5, + }, + { + code: '.bar, .foo {}', + message: messages.rejected('.foo'), + line: 1, + column: 7, + endColumn: 11, + endLine: 1, + }, + ], +}); + testRule({ ruleName, config: /\.foo[^>]*>.*\.bar/, diff --git a/lib/rules/selector-disallowed-list/index.js b/lib/rules/selector-disallowed-list/index.js index 74981ba922..66ae43acc6 100644 --- a/lib/rules/selector-disallowed-list/index.js +++ b/lib/rules/selector-disallowed-list/index.js @@ -3,9 +3,10 @@ const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); const report = require('../../utils/report'); +const optionsMatches = require('../../utils/optionsMatches'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); -const { isRegExp, isString } = require('../../utils/validateTypes'); +const { isRegExp, isString, isBoolean } = require('../../utils/validateTypes'); const ruleName = 'selector-disallowed-list'; @@ -17,38 +18,74 @@ const meta = { url: 'https://stylelint.io/user-guide/rules/list/selector-disallowed-list', }; -/** @type {import('stylelint').Rule>} */ -const rule = (primary) => { +/** @type {import('stylelint').Rule, { splitList: boolean, ignore: string[] }>} */ +const rule = (primary, secondaryOptions) => { return (root, result) => { - const validOptions = validateOptions(result, ruleName, { - actual: primary, - possible: [isString, isRegExp], - }); + const validOptions = validateOptions( + result, + ruleName, + { + actual: primary, + possible: [isString, isRegExp], + }, + { + actual: secondaryOptions, + possible: { + ignore: ['inside-block'], + splitList: [isBoolean], + }, + optional: true, + }, + ); if (!validOptions) { return; } + const ignoreInsideBlock = optionsMatches(secondaryOptions, 'ignore', 'inside-block'); + const splitList = secondaryOptions && secondaryOptions.splitList; + root.walkRules((ruleNode) => { if (!isStandardSyntaxRule(ruleNode)) { return; } - const { selector, raws } = ruleNode; + if (ignoreInsideBlock) { + const { parent } = ruleNode; + const isInsideBlock = parent && parent.type !== 'root'; - if (!matchesStringOrRegExp(selector, primary)) { - return; + if (isInsideBlock) { + return; + } } - const word = (raws.selector && raws.selector.raw) || selector; + if (splitList) { + ruleNode.selectors.forEach((selector) => { + if (matchesStringOrRegExp(selector, primary)) { + report({ + result, + ruleName, + message: messages.rejected(selector), + node: ruleNode, + word: selector, + }); + } + }); + } else { + const { selector, raws } = ruleNode; - report({ - result, - ruleName, - message: messages.rejected(selector), - node: ruleNode, - word, - }); + if (matchesStringOrRegExp(selector, primary)) { + const word = (raws.selector && raws.selector.raw) || selector; + + report({ + result, + ruleName, + message: messages.rejected(selector), + node: ruleNode, + word, + }); + } + } }); }; };