Skip to content

Commit

Permalink
Fix selector-max-class end positions (#7590)
Browse files Browse the repository at this point in the history
* Fix `selector-max-class` end positions

* Create silver-actors-jam.md
  • Loading branch information
romainmenke committed Apr 1, 2024
1 parent e872ac1 commit 3edaa03
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-actors-jam.md
@@ -0,0 +1,5 @@
---
"stylelint": patch
---

Fixed: `selector-max-class` end positions
18 changes: 7 additions & 11 deletions lib/rules/selector-max-class/__tests__/index.mjs
Expand Up @@ -125,11 +125,11 @@ testRule({
{
code: ':not(.ab.cd.ef) {}',
description: ':not(): greater than max classes, inside',
message: messages.expected('.ab.cd.ef', 2),
message: messages.expected(':not(.ab.cd.ef)', 2),
line: 1,
column: 6,
column: 1,
endLine: 1,
endColumn: 15,
endColumn: 16,
},
{
code: '.ab.cd.ef :not(.gh) {}',
Expand All @@ -143,24 +143,20 @@ testRule({
{
code: '.ab { &:hover > .ef.gh {} }',
description: 'nested selectors: greater than max classes',
message: messages.expected('.ab:hover > .ef.gh', 2),
message: messages.expected('&:hover > .ef.gh', 2),
line: 1,
column: 7,
endLine: 1,
endColumn: 26,
// see #6234
// endColumn: 23,
endColumn: 23,
},
{
code: '.ab { @include test { .ef { &:hover .gh {} } } }',
description: 'nested selectors: inside at-rule',
message: messages.expected('.ab .ef:hover .gh', 2),
message: messages.expected('&:hover .gh', 2),
line: 1,
column: 29,
endLine: 1,
endColumn: 43,
// see #6234
// endColumn: 40,
endColumn: 40,
},
],
});
Expand Down
37 changes: 20 additions & 17 deletions lib/rules/selector-max-class/index.cjs
Expand Up @@ -2,11 +2,11 @@
// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
'use strict';

const resolvedNestedSelector = require('postcss-resolve-nested-selector');
const flattenNestedSelectorsForRule = require('../../utils/flattenNestedSelectorsForRule.cjs');
const isContextFunctionalPseudoClass = require('../../utils/isContextFunctionalPseudoClass.cjs');
const isNonNegativeInteger = require('../../utils/isNonNegativeInteger.cjs');
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule.cjs');
const parseSelector = require('../../utils/parseSelector.cjs');
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
const validateOptions = require('../../utils/validateOptions.cjs');
Expand Down Expand Up @@ -35,14 +35,15 @@ const rule = (primary) => {
}

/**
* @param {import('postcss-selector-parser').Container<string | undefined>} selectorNode
* @param {import('postcss').Rule} ruleNode
* @param {import('postcss-selector-parser').Container<string | undefined>} resolvedSelectorNode
* @param {import('postcss-selector-parser').Container<string | undefined>} selectorNode
* @param {import('postcss').Rule} ruleNode
*/
function checkSelector(selectorNode, ruleNode) {
const count = selectorNode.reduce((total, childNode) => {
function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) {
const count = resolvedSelectorNode.reduce((total, childNode) => {
// Only traverse inside actual selectors and context functional pseudo-classes
if (childNode.type === 'selector' || isContextFunctionalPseudoClass(childNode)) {
checkSelector(childNode, ruleNode);
checkSelector(childNode, selectorNode, ruleNode);
}

if (childNode.type === 'class') total += 1;
Expand All @@ -51,15 +52,17 @@ const rule = (primary) => {
}, 0);

if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > primary) {
const selector = selectorNode.toString();
const index = selectorNode.first?.sourceIndex ?? 0;
const selectorStr = selectorNode.toString().trim();

report({
ruleName,
result,
node: ruleNode,
message: messages.expected,
messageArgs: [selector, primary],
word: selector,
messageArgs: [selectorStr, primary],
index,
endIndex: index + selectorStr.length,
});
}
}
Expand All @@ -69,13 +72,13 @@ const rule = (primary) => {
return;
}

for (const selector of ruleNode.selectors) {
for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
parseSelector(resolvedSelector, result, ruleNode, (container) =>
checkSelector(container, ruleNode),
);
}
}
if (!isStandardSyntaxSelector(ruleNode.selector)) return;

flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
resolvedSelectors.forEach((resolvedSelector) => {
checkSelector(resolvedSelector, selector, ruleNode);
});
});
});
};
};
Expand Down
38 changes: 20 additions & 18 deletions lib/rules/selector-max-class/index.mjs
@@ -1,9 +1,8 @@
import resolvedNestedSelector from 'postcss-resolve-nested-selector';

import flattenNestedSelectorsForRule from '../../utils/flattenNestedSelectorsForRule.mjs';
import isContextFunctionalPseudoClass from '../../utils/isContextFunctionalPseudoClass.mjs';
import isNonNegativeInteger from '../../utils/isNonNegativeInteger.mjs';
import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
import parseSelector from '../../utils/parseSelector.mjs';
import isStandardSyntaxSelector from '../../utils/isStandardSyntaxSelector.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
import validateOptions from '../../utils/validateOptions.mjs';
Expand Down Expand Up @@ -32,14 +31,15 @@ const rule = (primary) => {
}

/**
* @param {import('postcss-selector-parser').Container<string | undefined>} selectorNode
* @param {import('postcss').Rule} ruleNode
* @param {import('postcss-selector-parser').Container<string | undefined>} resolvedSelectorNode
* @param {import('postcss-selector-parser').Container<string | undefined>} selectorNode
* @param {import('postcss').Rule} ruleNode
*/
function checkSelector(selectorNode, ruleNode) {
const count = selectorNode.reduce((total, childNode) => {
function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) {
const count = resolvedSelectorNode.reduce((total, childNode) => {
// Only traverse inside actual selectors and context functional pseudo-classes
if (childNode.type === 'selector' || isContextFunctionalPseudoClass(childNode)) {
checkSelector(childNode, ruleNode);
checkSelector(childNode, selectorNode, ruleNode);
}

if (childNode.type === 'class') total += 1;
Expand All @@ -48,15 +48,17 @@ const rule = (primary) => {
}, 0);

if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > primary) {
const selector = selectorNode.toString();
const index = selectorNode.first?.sourceIndex ?? 0;
const selectorStr = selectorNode.toString().trim();

report({
ruleName,
result,
node: ruleNode,
message: messages.expected,
messageArgs: [selector, primary],
word: selector,
messageArgs: [selectorStr, primary],
index,
endIndex: index + selectorStr.length,
});
}
}
Expand All @@ -66,13 +68,13 @@ const rule = (primary) => {
return;
}

for (const selector of ruleNode.selectors) {
for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
parseSelector(resolvedSelector, result, ruleNode, (container) =>
checkSelector(container, ruleNode),
);
}
}
if (!isStandardSyntaxSelector(ruleNode.selector)) return;

flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
resolvedSelectors.forEach((resolvedSelector) => {
checkSelector(resolvedSelector, selector, ruleNode);
});
});
});
};
};
Expand Down

0 comments on commit 3edaa03

Please sign in to comment.