diff --git a/.changeset/shy-ghosts-report.md b/.changeset/shy-ghosts-report.md new file mode 100644 index 0000000000..1963fc1487 --- /dev/null +++ b/.changeset/shy-ghosts-report.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Added: `ignoreRules` to `max-nesting-depth` diff --git a/lib/rules/max-nesting-depth/README.md b/lib/rules/max-nesting-depth/README.md index 1ddb413b80..4f06ea2b4b 100644 --- a/lib/rules/max-nesting-depth/README.md +++ b/lib/rules/max-nesting-depth/README.md @@ -413,3 +413,61 @@ a { } } ``` + +### `ignoreRules: ["/regex/", /regex/, "string"]` + +Ignore rules matching with the specified selectors. + +For example, with `1` and given: + +```json +[".my-selector", "/^.ignored-sel/"] +``` + +The following patterns are _not_ considered problems: + + +```css +a { + .my-selector { /* ignored */ + b { /* 1 */ + top: 0; + } + } +} +``` + + +```css +a { + .my-selector, .ignored-selector { /* ignored */ + b { /* 1 */ + top: 0; + } + } +} +``` + +The following patterns are considered problems: + + +```css +a { + .not-ignored-selector { /* 1 */ + b { /* 2 */ + top: 0; + } + } +} +``` + + +```css +a { + .my-selector, .not-ignored-selector { /* 1 */ + b { /* 2 */ + top: 0; + } + } +} +``` diff --git a/lib/rules/max-nesting-depth/__tests__/index.mjs b/lib/rules/max-nesting-depth/__tests__/index.mjs index 44c042c2b9..df0ac6eb8a 100644 --- a/lib/rules/max-nesting-depth/__tests__/index.mjs +++ b/lib/rules/max-nesting-depth/__tests__/index.mjs @@ -270,6 +270,113 @@ testRule({ ], }); +testRule({ + ruleName, + config: [1, { ignoreRules: [/^.some-sel/, '.my-selector'] }], + + accept: [ + { + code: 'a { b { top: 0; }}', + description: 'No ignored selector', + }, + { + code: 'a { b { .my-selector { top: 0; }}}', + description: 'One ignored selector, ignored selector deepest', + }, + { + code: 'a { b { .my-selector { .some-selector { top: 0; }}}}', + description: 'Many ignored selectors', + }, + { + code: 'a { .some-selector { b { top: 0; }}}', + description: 'One ignored selector, ignored selector in the middle of tree', + }, + { + code: 'a { b { .some-selector { .some-sel { .my-selector { top: 0; }}}}}', + description: 'Many ignored selectors, ignored selectors in the middle of tree', + }, + { + code: 'a { .some-sel { .my-selector { top: 0; b { bottom: 0; }}}}', + description: + 'Many ignored selectors, ignored selectors in the middle of tree, one block has property and block', + }, + { + code: 'a { b { .my-selector, .some-sel { top: 0; }}}', + description: 'One selector has only ignored rules', + }, + ], + + reject: [ + { + code: 'a { b { .my-selector c { top: 0; }}}', + message: messages.expected(1), + description: 'One selector has an ignored rule alongside not ignored rule', + }, + { + code: 'a { b { c { top: 0; }}}', + message: messages.expected(1), + description: 'No ignored selectors', + }, + { + code: 'a { .my-selector { b { c { top: 0; }}}}', + message: messages.expected(1), + description: 'One ignored selector', + }, + { + code: 'a { b { .some-sel { .my-selector { .some-selector { c { top: 0; }}}}}}', + message: messages.expected(1), + description: 'Many ignored selectors, but even with ignoring depth is too much', + }, + { + code: 'a { b { .not-ignore-selector { color: #64FFDA; }}}', + message: messages.expected(1), + description: 'Not ignored selector', + }, + { + code: 'a { b { .my-selector, c { top: 0; }}}', + message: messages.expected(1), + description: + 'One selector has an ignored rule alongside not ignored rule, shorthand and same property', + }, + { + code: stripIndent` + .foo { + .baz { + .my-selector { + opacity: 0.4; + } + .bar { + color: red; + } + } + }`, + message: messages.expected(1), + description: + 'One selector has an ignored rule alongside not ignored rule, different properties', + line: 6, + column: 3, + }, + ], +}); + +testRule({ + ruleName, + config: [ + 1, + { + ignoreRules: [/^.some-sel/, '.my-selector'], + ignorePseudoClasses: ['hover', '/^--custom-.*$/'], + }, + ], + + accept: [ + { + code: 'a { &:--custom-pseudo, .my-selector { b { top: 0; } } }', + description: 'ignored pseudo-class alongside ignored selector', + }, + ], +}); + testRule({ ruleName, config: [1], diff --git a/lib/rules/max-nesting-depth/index.js b/lib/rules/max-nesting-depth/index.js index a1211c94f9..e20dbf5fe4 100644 --- a/lib/rules/max-nesting-depth/index.js +++ b/lib/rules/max-nesting-depth/index.js @@ -28,6 +28,13 @@ const rule = (primary, secondaryOptions) => { const isIgnoreAtRule = (node) => isAtRule(node) && optionsMatches(secondaryOptions, 'ignoreAtRules', node.name); + /** + * @param {import('postcss').Node} node + */ + const isIgnoreRule = (node) => { + return isRule(node) && optionsMatches(secondaryOptions, 'ignoreRules', node.selector); + }; + return (root, result) => { const validOptions = validateOptions( result, @@ -42,6 +49,7 @@ const rule = (primary, secondaryOptions) => { possible: { ignore: ['blockless-at-rules', 'pseudo-classes'], ignoreAtRules: [isString, isRegExp], + ignoreRules: [isString, isRegExp], ignorePseudoClasses: [isString, isRegExp], }, }, @@ -60,6 +68,10 @@ const rule = (primary, secondaryOptions) => { return; } + if (isIgnoreRule(statement)) { + return; + } + if (!hasBlock(statement)) { return; } @@ -120,10 +132,24 @@ const rule = (primary, secondaryOptions) => { * @param {string[]} selectors * @returns {boolean} */ - function containsIgnoredPseudoClassesOnly(selectors) { - if (!(secondaryOptions && secondaryOptions.ignorePseudoClasses)) return false; + function containsIgnoredPseudoClassesOrRulesOnly(selectors) { + if ( + !( + secondaryOptions && + (secondaryOptions.ignorePseudoClasses || secondaryOptions.ignoreRules) + ) + ) + return false; return selectors.every((selector) => { + if ( + secondaryOptions.ignoreRules && + optionsMatches(secondaryOptions, 'ignoreRules', selector) + ) + return true; + + if (!secondaryOptions.ignorePseudoClasses) return false; + const pseudoRule = extractPseudoRule(selector); if (!pseudoRule) return false; @@ -139,7 +165,7 @@ const rule = (primary, secondaryOptions) => { (optionsMatches(secondaryOptions, 'ignore', 'pseudo-classes') && isRule(node) && containsPseudoClassesOnly(node.selector)) || - (isRule(node) && containsIgnoredPseudoClassesOnly(node.selectors)) + (isRule(node) && containsIgnoredPseudoClassesOrRulesOnly(node.selectors)) ) { return nestingDepth(parent, level); }