Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignoreSelectors: [] to selector-max-compound-selectors #7544

Merged
merged 18 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/big-trains-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": minor
---

Added: `ignoreSelectors: []` to `selector-max-compound-selectors`
40 changes: 40 additions & 0 deletions lib/rules/selector-max-compound-selectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,43 @@ div {}
```css
.foo + div :not (a b ~ c) {} /* `a b ~ c` is inside `:not()`, so it is evaluated separately */
```

## Optional secondary options

### `ignoreSelectors: ["/regex/", /regex/, "non-regex"]`

Ignore some compound selectors. This may be useful for deep selectors like Vue's `::v-deep` or Angular's `::ng-deep` that behave more like combinators than compound selectors.

For example, with `2`.

Given:

```json
["::v-deep", "/ignored/"]
```

The following patterns are considered problems:

<!-- prettier-ignore -->
```css
.foo .bar ::v-deep .baz {}
```

The following patterns are _not_ considered problems:

<!-- prettier-ignore -->
```css
.foo::v-deep .bar {}
```

<!-- prettier-ignore -->
```css
.foo ::v-deep .baz {}
```

<!-- prettier-ignore -->
```css
.some {
&-ignored-class ::v-deep > .bar {}
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
}
```
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
79 changes: 79 additions & 0 deletions lib/rules/selector-max-compound-selectors/__tests__/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,82 @@ testRule({
},
],
});

// Testing ignoreSelectors
testRule({
ruleName,
config: [2, { ignoreSelectors: ['::v-deep', '/-moz-.*/', /-screen$/] }],
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved

accept: [
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
{
code: '::v-deep .foo .bar {}',
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
description: 'ignored selector at the beginning',
},
{
code: '.foo ::v-deep .bar {}',
description: 'ignored selector in the middle',
},
{
code: '.foo ::v-deep > .bar {}',
description: 'ignored selector in the middle followed by a combinator',
},
{
code: '.foo::v-deep .bar {}',
description: 'ignored selector as pseudo-element',
},
{
code: '.foo::v-deep > .bar {}',
description: 'ignored selector as pseudo-element followed by a combinator',
},
{
code: '.foo > ::v-deep .bar {}',
description: 'ignored selector preceded by a combinator',
},
{
code: '.foo input::-moz-placeholder {}',
description: 'selector ignored by string regex',
},
{
code: '.foo :-webkit-full-screen a {}',
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved
description: 'selector ignored by regex',
},
{
code: '.foo::v-deep :-webkit-full-screen a ::-moz-placeholder {}',
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved
description: 'multiple ignored selectors',
},
{
code: '.foo { & ::v-deep > .bar {} }',
description: 'nested ignored selector',
},
Copy link
Member

@Mouvedia Mouvedia Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question
major

How should '.foo .baz .bar.quz {}' behave for 2 ignore [".bar", ".quz"]?
i.e. does it remove one by one from the compound selector or does it skip it if it's not exactly a match?
Either way is fine with me but it needs to be documented either with a > [!NOTE] or an example in the README.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
],

reject: [
{
code: '.foo .bar ::v-deep .baz {}',
description: 'ignored selector in the middle',
message: messages.expected('.foo .bar ::v-deep .baz', 2),
line: 1,
column: 1,
endLine: 1,
endColumn: 24,
},
{
code: '.foo .bar::v-deep .baz {}',
description: 'ignored selector as pseudo-element',
message: messages.expected('.foo .bar::v-deep .baz', 2),
line: 1,
column: 1,
endLine: 1,
endColumn: 23,
},
{
code: '.foo { & ::v-deep > .bar .baz {} }',
description: 'nested ignored selector',
message: messages.expected('.foo ::v-deep > .bar .baz', 2),
line: 1,
column: 8,
endLine: 1,
endColumn: 33,
},
],
});
28 changes: 23 additions & 5 deletions lib/rules/selector-max-compound-selectors/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
'use strict';

const resolvedNestedSelector = require('postcss-resolve-nested-selector');
const validateTypes = require('../../utils/validateTypes.cjs');
const isContextFunctionalPseudoClass = require('../../utils/isContextFunctionalPseudoClass.cjs');
const isNonNegativeInteger = require('../../utils/isNonNegativeInteger.cjs');
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule.cjs');
const optionsMatches = require('../../utils/optionsMatches.cjs');
const parseSelector = require('../../utils/parseSelector.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
Expand All @@ -25,12 +27,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: {
ignoreSelectors: [validateTypes.isString, validateTypes.isRegExp],
},
optional: true,
},
);

if (!validOptions) {
return;
Expand All @@ -44,6 +57,7 @@ const rule = (primary) => {
*/
function checkSelector(selectorNode, ruleNode) {
let compoundCount = 1;
let previousNodeWasCompoundSeparator = true; // first node is implicitly preceded by a separator

selectorNode.each((childNode) => {
// Only traverse inside actual selectors and context functional pseudo-classes
Expand All @@ -54,7 +68,11 @@ const rule = (primary) => {
// Compound selectors are separated by combinators, so increase count when meeting one
if (childNode.type === 'combinator') {
compoundCount++;
} else if (optionsMatches(secondaryOptions, 'ignoreSelectors', childNode.toString())) {
compoundCount -= previousNodeWasCompoundSeparator ? 1 : 0;
}

previousNodeWasCompoundSeparator = childNode.type === 'combinator';
});

if (
Expand Down
28 changes: 23 additions & 5 deletions lib/rules/selector-max-compound-selectors/index.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import resolvedNestedSelector from 'postcss-resolve-nested-selector';

import { isRegExp, isString } from '../../utils/validateTypes.mjs';
import isContextFunctionalPseudoClass from '../../utils/isContextFunctionalPseudoClass.mjs';
import isNonNegativeInteger from '../../utils/isNonNegativeInteger.mjs';
import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
import optionsMatches from '../../utils/optionsMatches.mjs';
import parseSelector from '../../utils/parseSelector.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
Expand All @@ -22,12 +24,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: {
ignoreSelectors: [isString, isRegExp],
},
optional: true,
},
);

if (!validOptions) {
return;
Expand All @@ -41,6 +54,7 @@ const rule = (primary) => {
*/
function checkSelector(selectorNode, ruleNode) {
let compoundCount = 1;
let previousNodeWasCompoundSeparator = true; // first node is implicitly preceded by a separator
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved

selectorNode.each((childNode) => {
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved
// Only traverse inside actual selectors and context functional pseudo-classes
Expand All @@ -51,7 +65,11 @@ const rule = (primary) => {
// Compound selectors are separated by combinators, so increase count when meeting one
if (childNode.type === 'combinator') {
compoundCount++;
} else if (optionsMatches(secondaryOptions, 'ignoreSelectors', childNode.toString())) {
compoundCount -= previousNodeWasCompoundSeparator ? 1 : 0;
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved
}

previousNodeWasCompoundSeparator = childNode.type === 'combinator';
});

if (
Expand Down