Skip to content

Commit

Permalink
Fix selector-max-compound-selectors end positions
Browse files Browse the repository at this point in the history
  • Loading branch information
romainmenke committed Apr 7, 2024
1 parent 4dd08ea commit bf23d78
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 68 deletions.
106 changes: 79 additions & 27 deletions lib/rules/selector-max-compound-selectors/__tests__/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@ testRule({
{
code: ':not(a b c) { top: 0; }',
description: 'Standalone :not(), number of compound selectors > max inside it',
message: messages.expected('a b c', 2),
message: messages.expected(':not(a b c)', 2),
line: 1,
column: 6,
column: 1,
endLine: 1,
endColumn: 11,
endColumn: 12,
},
{
code: 'a b > c:not( d e ) { top: 0; }',
Expand Down Expand Up @@ -204,65 +204,109 @@ testRule({
},
{
code: '.cd { .de { .fg {} } }',
message: messages.expected('.cd .de .fg', 2),
message: messages.expected('.fg', 2),
line: 1,
column: 13,
endLine: 1,
endColumn: 19,
endColumn: 16,
},
{
code: '.cd { .de { & > .fg {} } }',
message: messages.expected('.cd .de > .fg', 2),
message: messages.expected('& > .fg', 2),
line: 1,
column: 13,
endLine: 1,
endColumn: 20,
},
{
code: '.cd { .de { &:hover > .fg {} } }',
message: messages.expected('.cd .de:hover > .fg', 2),
message: messages.expected('&:hover > .fg', 2),
line: 1,
column: 13,
endLine: 1,
endColumn: 26,
},
{
code: '.cd { .de { .fg > & {} } }',
message: messages.expected('.fg > .cd .de', 2),
message: messages.expected('.fg > &', 2),
line: 1,
column: 13,
endLine: 1,
endColumn: 20,
},
{
code: 'a { b { c {} } }',
description: 'standard nesting',
message: messages.expected('a b c', 2),
message: messages.expected('c', 2),
line: 1,
column: 9,
endLine: 1,
endColumn: 10,
},
{
code: 'a b { c { top: 10px; } }',
description: 'standard nesting with declaration',
message: messages.expected('a b c', 2),
message: messages.expected('c', 2),
line: 1,
column: 7,
endLine: 1,
endColumn: 8,
},
{
code: 'a b { c { d {} } }',
description: 'standard nesting with declaration and more nested rule',
warnings: [
{
message: messages.expected('a b c', 2),
message: messages.expected('c', 2),
line: 1,
column: 7,
endLine: 1,
endColumn: 8,
},
{
message: messages.expected('a b c d', 2),
message: messages.expected('d', 2),
line: 1,
column: 11,
endLine: 1,
endColumn: 12,
},
],
},
{
code: 'a b { c { top: 10px; @media print {} } }',
description: 'standard nesting with declaration and more nested rule',
message: messages.expected('a b c', 2),
message: messages.expected('c', 2),
line: 1,
column: 7,
endLine: 1,
endColumn: 8,
},
{
code: 'a b { c { @media print { top: 10px; } } }',
description: 'standard nesting with declaration and more nested rule',
message: messages.expected('a b c', 2),
message: messages.expected('c', 2),
line: 1,
column: 7,
endLine: 1,
endColumn: 8,
},
{
code: 'a { @media print { b > c { d {} } } }',
description: 'The rule fails, but nesting even deeper with more compound selectors,',
warnings: [
{
message: messages.expected('a b > c', 2),
message: messages.expected('b > c', 2),
line: 1,
column: 20,
endLine: 1,
endColumn: 25,
},
{
message: messages.expected('a b > c d', 2),
message: messages.expected('d', 2),
line: 1,
column: 28,
endLine: 1,
endColumn: 29,
},
],
},
Expand All @@ -272,22 +316,30 @@ testRule({
'The rule fails, but nesting even deeper with more compound selectors, parent ref.,',
warnings: [
{
message: messages.expected('.a .b > .c', 2),
message: messages.expected('& .b > .c', 2),
line: 1,
column: 21,
endLine: 1,
endColumn: 30,
},
{
message: messages.expected('.a .b > .c + .d', 2),
message: messages.expected('& + .d', 2),
line: 1,
column: 33,
endLine: 1,
endColumn: 39,
},
],
},
{
code: '@media print { li { & + .ab { .cd { top: 10px; } } } }',
description:
'The rule fails, but nesting even deeper with more compound selectors, has declarations',
message: messages.expected('li + .ab .cd', 2),
message: messages.expected('.cd', 2),
line: 1,
column: 31,
endLine: 1,
endColumn: 34,
},
],
});
Expand Down Expand Up @@ -463,11 +515,11 @@ testRule({
{
code: '.foo { & ::v-deep > .bar .baz {} }',
description: 'nested ignored selector',
message: messages.expected('.foo ::v-deep > .bar .baz', 2),
message: messages.expected('& ::v-deep > .bar .baz', 2),
line: 1,
column: 8,
endLine: 1,
endColumn: 33,
endColumn: 30,
},
{
code: '.foo .bar > .baz.ignored {}',
Expand Down Expand Up @@ -508,20 +560,20 @@ testRule({
{
code: 'p a :not(.foo .bar .baz) {}',
description: 'still evaluates compound selectors inside :not',
message: messages.expected('.foo .bar .baz', 2),
message: messages.expected('p a :not(.foo .bar .baz)', 2),
line: 1,
column: 10,
column: 1,
endLine: 1,
endColumn: 24,
endColumn: 25,
},
{
code: 'p a :not(.foo .bar, .foo .bar .baz) {}',
description: 'still evaluates compound selectors inside :not with comma',
message: messages.expected(' .foo .bar .baz', 2),
message: messages.expected('p a :not(.foo .bar, .foo .bar .baz)', 2),
line: 1,
column: 20,
column: 1,
endLine: 1,
endColumn: 35,
endColumn: 36,
},
],
});
39 changes: 19 additions & 20 deletions lib/rules/selector-max-compound-selectors/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
'use strict';

const resolveNestedSelector = require('postcss-resolve-nested-selector');
const selectorParser = require('postcss-selector-parser');
const validateTypes = require('../../utils/validateTypes.cjs');
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 isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector.cjs');
const optionsMatches = require('../../utils/optionsMatches.cjs');
const parseSelector = require('../../utils/parseSelector.cjs');
const pluralize = require('../../utils/pluralize.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
Expand Down Expand Up @@ -62,19 +62,18 @@ const rule = (primary, secondaryOptions) => {
}

/**
* Finds actual selectors in selectorNode object and checks them.
*
* @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) {
function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) {
/** @type {import('postcss-selector-parser').Node[]} */
const filteredChildNodes = [];

selectorNode.each((childNode) => {
resolvedSelectorNode.each((childNode) => {
// Only traverse inside actual selectors and context functional pseudo-classes
if (isSelector(childNode) || isContextFunctionalPseudoClass(childNode)) {
checkSelector(childNode, ruleNode);
checkSelector(childNode, selectorNode, ruleNode);
}

if (!isSelectorIgnored(childNode)) {
Expand All @@ -101,31 +100,31 @@ const rule = (primary, secondaryOptions) => {
const compoundCount = combinatorCount + 1;

if (compoundCount > 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,
});
}
}

root.walkRules((ruleNode) => {
if (!isStandardSyntaxRule(ruleNode)) {
return;
}
if (!isStandardSyntaxRule(ruleNode)) return;

// Using `.selectors` gets us each selector if there is a comma separated set
for (const selector of ruleNode.selectors) {
for (const resolvedSelector of resolveNestedSelector(selector, ruleNode)) {
// Process each resolved selector with `checkSelector` via postcss-selector-parser
parseSelector(resolvedSelector, result, ruleNode, (s) => checkSelector(s, ruleNode));
}
}
if (!isStandardSyntaxSelector(ruleNode.selector)) return;

flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
resolvedSelectors.forEach((resolvedSelector) => {
checkSelector(resolvedSelector, selector, ruleNode);
});
});
});
};
};
Expand Down
40 changes: 19 additions & 21 deletions lib/rules/selector-max-compound-selectors/index.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import resolveNestedSelector from 'postcss-resolve-nested-selector';

import selectorParser from 'postcss-selector-parser';
const { isCombinator, isPseudo, isRoot, isSelector } = selectorParser;

import { isRegExp, isString } from '../../utils/validateTypes.mjs';
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 isStandardSyntaxSelector from '../../utils/isStandardSyntaxSelector.mjs';
import optionsMatches from '../../utils/optionsMatches.mjs';
import parseSelector from '../../utils/parseSelector.mjs';
import pluralize from '../../utils/pluralize.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
Expand Down Expand Up @@ -59,19 +58,18 @@ const rule = (primary, secondaryOptions) => {
}

/**
* Finds actual selectors in selectorNode object and checks them.
*
* @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) {
function checkSelector(resolvedSelectorNode, selectorNode, ruleNode) {
/** @type {import('postcss-selector-parser').Node[]} */
const filteredChildNodes = [];

selectorNode.each((childNode) => {
resolvedSelectorNode.each((childNode) => {
// Only traverse inside actual selectors and context functional pseudo-classes
if (isSelector(childNode) || isContextFunctionalPseudoClass(childNode)) {
checkSelector(childNode, ruleNode);
checkSelector(childNode, selectorNode, ruleNode);
}

if (!isSelectorIgnored(childNode)) {
Expand All @@ -98,31 +96,31 @@ const rule = (primary, secondaryOptions) => {
const compoundCount = combinatorCount + 1;

if (compoundCount > 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,
});
}
}

root.walkRules((ruleNode) => {
if (!isStandardSyntaxRule(ruleNode)) {
return;
}
if (!isStandardSyntaxRule(ruleNode)) return;

// Using `.selectors` gets us each selector if there is a comma separated set
for (const selector of ruleNode.selectors) {
for (const resolvedSelector of resolveNestedSelector(selector, ruleNode)) {
// Process each resolved selector with `checkSelector` via postcss-selector-parser
parseSelector(resolvedSelector, result, ruleNode, (s) => checkSelector(s, 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 bf23d78

Please sign in to comment.