diff --git a/lib/rules/selector-max-universal/README.md b/lib/rules/selector-max-universal/README.md index 5424b55d2d..5a387b7b44 100644 --- a/lib/rules/selector-max-universal/README.md +++ b/lib/rules/selector-max-universal/README.md @@ -74,3 +74,24 @@ The following patterns are _not_ considered problems: /* `*` is inside `:not()`, so it is evaluated separately */ * > * .foo:not(*) {} ``` + +## Optional secondary options + +### `ignoreAfterCombinators: ["array", "of", "combinators"]` + +Ignore universal selectors that come after one of the specified combinators. + +Given: + +```json +[">", "+"] +``` + +For example, with `2`. + +The following pattern is _not_ considered a problem: + + +```css +* * > * {} +``` diff --git a/lib/rules/selector-max-universal/__tests__/index.js b/lib/rules/selector-max-universal/__tests__/index.js index 7796624f7d..fd35306c1f 100644 --- a/lib/rules/selector-max-universal/__tests__/index.js +++ b/lib/rules/selector-max-universal/__tests__/index.js @@ -186,6 +186,18 @@ testRule({ code: '* { @media print { * {} } }', description: 'media query: nested', }, + { + code: '* + * {}', + description: 'adjacent sibling combinator compound selector', + }, + { + code: '* > * {}', + description: 'child combinator compound selector', + }, + { + code: '* ~ * {}', + description: 'general sibling combinator compound selector', + }, ], reject: [ @@ -198,6 +210,35 @@ testRule({ endLine: 1, endColumn: 6, }, + { + code: '* + * + * {}', + description: + 'adjacent sibling combinator compound selector: greater than max universal selectors', + message: messages.expected('* + * + *', 2), + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + { + code: '* > * > * {}', + description: 'child combinator compound selector: greater than max universal selectors', + message: messages.expected('* > * > *', 2), + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, + { + code: '* ~ * ~ * {}', + description: + 'general sibling combinator compound selector: greater than max universal selectors', + message: messages.expected('* ~ * ~ *', 2), + line: 1, + column: 1, + endLine: 1, + endColumn: 10, + }, { code: '*, \n* * * {}', description: 'multiple selectors: greater than max universal selectors', @@ -228,6 +269,65 @@ testRule({ ], }); +testRule({ + ruleName, + config: [1, { ignoreAfterCombinators: ['>'] }], + + accept: [ + { + code: '*.foo {}', + }, + { + code: '*.foo > * {}', + }, + ], + + reject: [ + { + code: '*.foo * {}', + message: messages.expected('*.foo *', 1), + line: 1, + column: 1, + endLine: 1, + endColumn: 8, + }, + ], +}); + +testRule({ + ruleName, + config: [0, { ignoreAfterCombinators: ['~', '+', '>', ' '] }], + + accept: [ + { + code: '.foo {}', + }, + { + code: '.foo * {}', + }, + { + code: '.foo ~ * {}', + }, + { + code: '.foo + * {}', + }, + { + code: '.foo > * {}', + }, + ], + + reject: [ + { + code: '* {}', + message: messages.expected('*', 0), + line: 1, + column: 1, + endLine: 1, + endColumn: 2, + }, + ], +}); + testRule({ ruleName, config: [0], diff --git a/lib/rules/selector-max-universal/index.js b/lib/rules/selector-max-universal/index.js index eca5823a58..f5b2a2bb5b 100644 --- a/lib/rules/selector-max-universal/index.js +++ b/lib/rules/selector-max-universal/index.js @@ -8,6 +8,8 @@ const resolvedNestedSelector = require('postcss-resolve-nested-selector'); const ruleMessages = require('../../utils/ruleMessages'); const selectorParser = require('postcss-selector-parser'); const validateOptions = require('../../utils/validateOptions'); +const optionsMatches = require('../../utils/optionsMatches'); +const { isString } = require('../../utils/validateTypes'); const ruleName = 'selector-max-universal'; @@ -23,12 +25,23 @@ const meta = { }; /** @type {import('stylelint').Rule} */ -const rule = (primary) => { +const rule = (primary, secondaryOptions) => { return (root, result) => { - const validOptions = validateOptions(result, ruleName, { - actual: primary, - possible: isNonNegativeInteger, - }); + const validOptions = validateOptions( + result, + ruleName, + { + actual: primary, + possible: isNonNegativeInteger, + }, + { + actual: secondaryOptions, + possible: { + ignoreAfterCombinators: [isString], + }, + optional: true, + }, + ); if (!validOptions) { return; @@ -46,7 +59,14 @@ const rule = (primary) => { checkSelector(childNode, ruleNode); } - if (childNode.type === 'universal') total += 1; + const prevChildNode = childNode.prev(); + const prevChildNodeValue = prevChildNode && prevChildNode.value; + + if (childNode.type === 'universal') { + if (!optionsMatches(secondaryOptions, 'ignoreAfterCombinators', prevChildNodeValue)) { + total += 1; + } + } return total; }, 0);