diff --git a/lib/rules/selector-max-id/README.md b/lib/rules/selector-max-id/README.md index 8ad12f1a62..301fe42f45 100644 --- a/lib/rules/selector-max-id/README.md +++ b/lib/rules/selector-max-id/README.md @@ -74,3 +74,32 @@ The following patterns are _not_ considered violations: /* `#bar` is inside `:not()`, so it is evaluated separately */ #foo #bar:not(#baz) {} ``` + +### `ignoreContextFunctionalPseudoClasses: ["/regex/", /regex/, "non-regex"]` + +Ignore selectors inside of specified [functional pseudo-classes](https://drafts.csswg.org/selectors-4/#pseudo-classes) that provide [evaluation contexts](https://drafts.csswg.org/selectors-4/#specificity-rules). + +Given: + +```js +[":not", /^:(h|H)as$/]; +``` + +The following patterns are considered violations: + + +```css +a:matches(#foo) {} +``` + +While the following patters are _not_ considered violations: + + +```css +a:not(#foo) {} +``` + + +```css +a:has(#foo) {} +``` diff --git a/lib/rules/selector-max-id/__tests__/index.js b/lib/rules/selector-max-id/__tests__/index.js index cec638d00d..6647ccc659 100644 --- a/lib/rules/selector-max-id/__tests__/index.js +++ b/lib/rules/selector-max-id/__tests__/index.js @@ -459,3 +459,28 @@ testRule({ }, ], }); + +testRule({ + ruleName, + config: [0, { ignoreContextFunctionalPseudoClasses: [':not', /^:(h|H)as$/] }], + + accept: [ + { + code: 'a:not(#foo) {}', + description: 'selector within ignored pseudo-class (string input)', + }, + { + code: 'a:has(#foo) {}', + description: 'selector within ignored pseudo-class (regex input)', + }, + ], + reject: [ + { + code: 'a:matches(#foo) {}', + description: 'selector within non-ignored pseudo class', + message: messages.expected('#foo', 0), + line: 1, + column: 11, + }, + ], +}); diff --git a/lib/rules/selector-max-id/index.js b/lib/rules/selector-max-id/index.js index 0114f7b4fb..bf608bb3dc 100644 --- a/lib/rules/selector-max-id/index.js +++ b/lib/rules/selector-max-id/index.js @@ -2,8 +2,10 @@ 'use strict'; +const _ = require('lodash'); const isLogicalCombination = require('../../utils/isLogicalCombination'); const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); +const optionsMatches = require('../../utils/optionsMatches'); const parseSelector = require('../../utils/parseSelector'); const report = require('../../utils/report'); const resolvedNestedSelector = require('postcss-resolve-nested-selector'); @@ -17,16 +19,27 @@ const messages = ruleMessages(ruleName, { `Expected "${selector}" to have no more than ${max} ID ${max === 1 ? 'selector' : 'selectors'}`, }); -function rule(max) { +function rule(max, options) { return (root, result) => { - const validOptions = validateOptions(result, ruleName, { - actual: max, - possible: [ - function (max) { - return typeof max === 'number' && max >= 0; + const validOptions = validateOptions( + result, + ruleName, + { + actual: max, + possible: [ + function (max) { + return typeof max === 'number' && max >= 0; + }, + ], + }, + { + actual: options, + possible: { + ignoreContextFunctionalPseudoClasses: [_.isString, _.isRegExp], }, - ], - }); + optional: true, + }, + ); if (!validOptions) { return; @@ -34,8 +47,12 @@ function rule(max) { function checkSelector(selectorNode, ruleNode) { const count = selectorNode.reduce((total, childNode) => { - // Only traverse inside actual selectors and logical combinations - if (childNode.type === 'selector' || isLogicalCombination(childNode)) { + // Only traverse inside actual selectors and logical combinations that are not part of ignored functional pseudo-classes + if ( + childNode.type === 'selector' || + (isLogicalCombination(childNode) && + !isIgnoredContextFunctionalPseudoClass(childNode, options)) + ) { checkSelector(childNode, ruleNode); } @@ -53,6 +70,13 @@ function rule(max) { } } + function isIgnoredContextFunctionalPseudoClass(node, options) { + return ( + node.type === 'pseudo' && + optionsMatches(options, 'ignoreContextFunctionalPseudoClasses', node.value) + ); + } + root.walkRules((ruleNode) => { if (!isStandardSyntaxRule(ruleNode)) { return;