Skip to content

Commit

Permalink
Add ignoreRules to max-nesting-depth (#7215)
Browse files Browse the repository at this point in the history
Closes #6529 (and thus, closes #2696 and closes #4805).

1. I've add co-author annotations to credit @sjarva and @ybiquitous with their contributions in the original PR; we should keep these when/if we merge. Also happy to merge this back into #6529 if @sjarva would like; I think we should give her the authorship for this commit if possible.
2. I noticed that there's a possibility where if both `ignoreRules` and `ignorePseudoClasses` are used together (as siblings), the original PR does not properly treat that combination as fully ignored. I've added a test case to verify that and then fix it in cfa98ab. The code isn't very elegant, so suggestions are welcome!


---------

Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com>
Co-authored-by: Senja Jarva <senja@jarva.fi>
  • Loading branch information
3 people committed Oct 7, 2023
1 parent 4bfee38 commit e468814
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-ghosts-report.md
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `ignoreRules` to `max-nesting-depth`
58 changes: 58 additions & 0 deletions lib/rules/max-nesting-depth/README.md
Expand Up @@ -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:

<!-- prettier-ignore -->
```css
a {
.my-selector { /* ignored */
b { /* 1 */
top: 0;
}
}
}
```

<!-- prettier-ignore -->
```css
a {
.my-selector, .ignored-selector { /* ignored */
b { /* 1 */
top: 0;
}
}
}
```

The following patterns are considered problems:

<!-- prettier-ignore -->
```css
a {
.not-ignored-selector { /* 1 */
b { /* 2 */
top: 0;
}
}
}
```

<!-- prettier-ignore -->
```css
a {
.my-selector, .not-ignored-selector { /* 1 */
b { /* 2 */
top: 0;
}
}
}
```
107 changes: 107 additions & 0 deletions lib/rules/max-nesting-depth/__tests__/index.mjs
Expand Up @@ -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],
Expand Down
32 changes: 29 additions & 3 deletions lib/rules/max-nesting-depth/index.js
Expand Up @@ -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,
Expand All @@ -42,6 +49,7 @@ const rule = (primary, secondaryOptions) => {
possible: {
ignore: ['blockless-at-rules', 'pseudo-classes'],
ignoreAtRules: [isString, isRegExp],
ignoreRules: [isString, isRegExp],
ignorePseudoClasses: [isString, isRegExp],
},
},
Expand All @@ -60,6 +68,10 @@ const rule = (primary, secondaryOptions) => {
return;
}

if (isIgnoreRule(statement)) {
return;
}

if (!hasBlock(statement)) {
return;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down

0 comments on commit e468814

Please sign in to comment.