Skip to content

Commit

Permalink
Fix no-descending-specificity end positions (#7701)
Browse files Browse the repository at this point in the history
* Fix `no-descending-specificity` end positions

* Create nervous-zebras-bow.md

* use `getSelectorSourceIndex` helper
  • Loading branch information
romainmenke committed May 23, 2024
1 parent 024fa7e commit c76a5d3
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-zebras-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": patch
---

Fixed: `no-descending-specificity` end positions
2 changes: 2 additions & 0 deletions lib/rules/no-descending-specificity/__tests__/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ testRule({
message: messages.rejected('a', 'b a'),
line: 1,
column: 26,
endLine: 1,
endColumn: 27,
},
],
});
67 changes: 28 additions & 39 deletions lib/rules/no-descending-specificity/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
'use strict';

const selectorSpecificity = require('@csstools/selector-specificity');
const resolvedNestedSelector = require('postcss-resolve-nested-selector');
const findAtRuleContext = require('../../utils/findAtRuleContext.cjs');
const flattenNestedSelectorsForRule = require('../../utils/flattenNestedSelectorsForRule.cjs');
const getSelectorSourceIndex = require('../../utils/getSelectorSourceIndex.cjs');
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule.cjs');
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector.cjs');
const nodeContextLookup = require('../../utils/nodeContextLookup.cjs');
const optionsMatches = require('../../utils/optionsMatches.cjs');
const parseSelector = require('../../utils/parseSelector.cjs');
const selectors = require('../../reference/selectors.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
Expand All @@ -18,7 +17,9 @@ const validateOptions = require('../../utils/validateOptions.cjs');
const ruleName = 'no-descending-specificity';

const messages = ruleMessages(ruleName, {
rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`,
rejected: (b, a) => {
return `Expected selector "${b}" to come before selector "${a}"`;
},
});

const meta = {
Expand Down Expand Up @@ -63,10 +64,8 @@ const rule = (primary, secondaryOptions) => {
return;
}

const selectors = ruleNode.selectors;

// Ignores selectors within list of selectors
if (ignoreSelectorsWithinList && selectors.length > 1) {
if (ignoreSelectorsWithinList && ruleNode.selectors.length > 1) {
return;
}

Expand All @@ -76,40 +75,30 @@ const rule = (primary, secondaryOptions) => {
findAtRuleContext(ruleNode),
);

for (const selector of selectors) {
// Ignore `.selector, { }`
if (selector.trim() === '') {
continue;
}

// Resolve any nested selectors before checking
for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
if (!isStandardSyntaxSelector(resolvedSelector)) {
continue;
}

const selectorRoot = parseSelector(resolvedSelector, result, ruleNode);

if (selectorRoot) {
checkSelector(resolvedSelector, selectorRoot, ruleNode, comparisonContext);
}
}
}
// Resolve any nested selectors before checking
flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
resolvedSelectors.forEach((resolvedSelector) => {
checkSelector(resolvedSelector, selector, ruleNode, comparisonContext);
});
});
});

/**
* @param {string} selector
* @param {import('postcss-selector-parser').Root} selectorNode
* @param {import('postcss-selector-parser').Selector} resolvedSelectorNode
* @param {import('postcss-selector-parser').Selector} selectorNode
* @param {import('postcss').Rule} ruleNode
* @param {Map<string, Entry[]>} comparisonContext
*/
function checkSelector(selector, selectorNode, ruleNode, comparisonContext) {
function checkSelector(resolvedSelectorNode, selectorNode, ruleNode, comparisonContext) {
const referenceSelector = lastCompoundSelectorWithoutPseudoClasses(selectorNode);

if (!referenceSelector) return;

const specificity = selectorSpecificity.selectorSpecificity(selectorNode);
const entry = { selector, specificity };
const specificity = selectorSpecificity.selectorSpecificity(resolvedSelectorNode);
const entry = {
selector: resolvedSelectorNode.toString().trim(),
specificity,
};
const priorComparableSelectors = comparisonContext.get(referenceSelector);

if (!priorComparableSelectors) {
Expand All @@ -120,13 +109,17 @@ const rule = (primary, secondaryOptions) => {

for (const priorEntry of priorComparableSelectors) {
if (selectorSpecificity.compare(specificity, priorEntry.specificity) < 0) {
const index = getSelectorSourceIndex(selectorNode);
const selectorStr = selectorNode.toString().trim();

report({
ruleName,
result,
node: ruleNode,
message: messages.rejected,
messageArgs: [selector, priorEntry.selector],
word: selector,
messageArgs: [selectorStr, priorEntry.selector],
index,
endIndex: index + selectorStr.length,
});

break;
Expand All @@ -139,15 +132,11 @@ const rule = (primary, secondaryOptions) => {
};

/**
* @param {import('postcss-selector-parser').Root} selectorNode
* @param {import('postcss-selector-parser').Selector} selectorNode
* @returns {string | undefined}
*/
function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
const firstChild = selectorNode.nodes[0];

if (!firstChild) return undefined;

const nodesByCombinator = firstChild.split((node) => node.type === 'combinator');
const nodesByCombinator = selectorNode.split((node) => node.type === 'combinator');
const nodesAfterLastCombinator = nodesByCombinator[nodesByCombinator.length - 1];

if (!nodesAfterLastCombinator) return undefined;
Expand Down
67 changes: 28 additions & 39 deletions lib/rules/no-descending-specificity/index.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { compare, selectorSpecificity } from '@csstools/selector-specificity';
import resolvedNestedSelector from 'postcss-resolve-nested-selector';

import findAtRuleContext from '../../utils/findAtRuleContext.mjs';
import flattenNestedSelectorsForRule from '../../utils/flattenNestedSelectorsForRule.mjs';
import getSelectorSourceIndex from '../../utils/getSelectorSourceIndex.mjs';
import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
import isStandardSyntaxSelector from '../../utils/isStandardSyntaxSelector.mjs';
import nodeContextLookup from '../../utils/nodeContextLookup.mjs';
import optionsMatches from '../../utils/optionsMatches.mjs';
import parseSelector from '../../utils/parseSelector.mjs';
import { pseudoElements } from '../../reference/selectors.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
Expand All @@ -15,7 +14,9 @@ import validateOptions from '../../utils/validateOptions.mjs';
const ruleName = 'no-descending-specificity';

const messages = ruleMessages(ruleName, {
rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`,
rejected: (b, a) => {
return `Expected selector "${b}" to come before selector "${a}"`;
},
});

const meta = {
Expand Down Expand Up @@ -60,10 +61,8 @@ const rule = (primary, secondaryOptions) => {
return;
}

const selectors = ruleNode.selectors;

// Ignores selectors within list of selectors
if (ignoreSelectorsWithinList && selectors.length > 1) {
if (ignoreSelectorsWithinList && ruleNode.selectors.length > 1) {
return;
}

Expand All @@ -73,40 +72,30 @@ const rule = (primary, secondaryOptions) => {
findAtRuleContext(ruleNode),
);

for (const selector of selectors) {
// Ignore `.selector, { }`
if (selector.trim() === '') {
continue;
}

// Resolve any nested selectors before checking
for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
if (!isStandardSyntaxSelector(resolvedSelector)) {
continue;
}

const selectorRoot = parseSelector(resolvedSelector, result, ruleNode);

if (selectorRoot) {
checkSelector(resolvedSelector, selectorRoot, ruleNode, comparisonContext);
}
}
}
// Resolve any nested selectors before checking
flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
resolvedSelectors.forEach((resolvedSelector) => {
checkSelector(resolvedSelector, selector, ruleNode, comparisonContext);
});
});
});

/**
* @param {string} selector
* @param {import('postcss-selector-parser').Root} selectorNode
* @param {import('postcss-selector-parser').Selector} resolvedSelectorNode
* @param {import('postcss-selector-parser').Selector} selectorNode
* @param {import('postcss').Rule} ruleNode
* @param {Map<string, Entry[]>} comparisonContext
*/
function checkSelector(selector, selectorNode, ruleNode, comparisonContext) {
function checkSelector(resolvedSelectorNode, selectorNode, ruleNode, comparisonContext) {
const referenceSelector = lastCompoundSelectorWithoutPseudoClasses(selectorNode);

if (!referenceSelector) return;

const specificity = selectorSpecificity(selectorNode);
const entry = { selector, specificity };
const specificity = selectorSpecificity(resolvedSelectorNode);
const entry = {
selector: resolvedSelectorNode.toString().trim(),
specificity,
};
const priorComparableSelectors = comparisonContext.get(referenceSelector);

if (!priorComparableSelectors) {
Expand All @@ -117,13 +106,17 @@ const rule = (primary, secondaryOptions) => {

for (const priorEntry of priorComparableSelectors) {
if (compare(specificity, priorEntry.specificity) < 0) {
const index = getSelectorSourceIndex(selectorNode);
const selectorStr = selectorNode.toString().trim();

report({
ruleName,
result,
node: ruleNode,
message: messages.rejected,
messageArgs: [selector, priorEntry.selector],
word: selector,
messageArgs: [selectorStr, priorEntry.selector],
index,
endIndex: index + selectorStr.length,
});

break;
Expand All @@ -136,15 +129,11 @@ const rule = (primary, secondaryOptions) => {
};

/**
* @param {import('postcss-selector-parser').Root} selectorNode
* @param {import('postcss-selector-parser').Selector} selectorNode
* @returns {string | undefined}
*/
function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
const firstChild = selectorNode.nodes[0];

if (!firstChild) return undefined;

const nodesByCombinator = firstChild.split((node) => node.type === 'combinator');
const nodesByCombinator = selectorNode.split((node) => node.type === 'combinator');
const nodesAfterLastCombinator = nodesByCombinator[nodesByCombinator.length - 1];

if (!nodesAfterLastCombinator) return undefined;
Expand Down

0 comments on commit c76a5d3

Please sign in to comment.