123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- 'use strict';
- const resolvedNestedSelector = require('postcss-resolve-nested-selector');
- const { selectorSpecificity: calculate, compare } = require('@csstools/selector-specificity');
- const findAtRuleContext = require('../../utils/findAtRuleContext');
- const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
- const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
- const { pseudoElements } = require('../../reference/selectors');
- const nodeContextLookup = require('../../utils/nodeContextLookup');
- const optionsMatches = require('../../utils/optionsMatches');
- const parseSelector = require('../../utils/parseSelector');
- const report = require('../../utils/report');
- const ruleMessages = require('../../utils/ruleMessages');
- const validateOptions = require('../../utils/validateOptions');
- const { assert } = require('../../utils/validateTypes');
- const ruleName = 'no-descending-specificity';
- const messages = ruleMessages(ruleName, {
- rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`,
- });
- const meta = {
- url: 'https://stylelint.io/user-guide/rules/no-descending-specificity',
- };
- /** @typedef {{ selector: string, specificity: import('@csstools/selector-specificity').Specificity }} Entry */
- /** @type {import('stylelint').Rule} */
- const rule = (primary, secondaryOptions) => {
- return (root, result) => {
- const validOptions = validateOptions(
- result,
- ruleName,
- {
- actual: primary,
- },
- {
- optional: true,
- actual: secondaryOptions,
- possible: {
- ignore: ['selectors-within-list'],
- },
- },
- );
- if (!validOptions) {
- return;
- }
- const ignoreSelectorsWithinList = optionsMatches(
- secondaryOptions,
- 'ignore',
- 'selectors-within-list',
- );
- const selectorContextLookup = nodeContextLookup();
- root.walkRules((ruleNode) => {
- // Ignore nested property `foo: {};`
- if (!isStandardSyntaxRule(ruleNode)) {
- return;
- }
- // Ignores selectors within list of selectors
- if (ignoreSelectorsWithinList && ruleNode.selectors.length > 1) {
- return;
- }
- /** @type {Map<string, Entry[]>} */
- const comparisonContext = selectorContextLookup.getContext(
- ruleNode,
- findAtRuleContext(ruleNode),
- );
- for (const selector of ruleNode.selectors) {
- const trimSelector = selector.trim();
- // Ignore `.selector, { }`
- if (trimSelector === '') {
- continue;
- }
- // Resolve any nested selectors before checking
- for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
- parseSelector(resolvedSelector, result, ruleNode, (s) => {
- if (!isStandardSyntaxSelector(resolvedSelector)) {
- return;
- }
- checkSelector(s, ruleNode, comparisonContext);
- });
- }
- }
- });
- /**
- * @param {import('postcss-selector-parser').Root} selectorNode
- * @param {import('postcss').Rule} ruleNode
- * @param {Map<string, Entry[]>} comparisonContext
- */
- function checkSelector(selectorNode, ruleNode, comparisonContext) {
- const selector = selectorNode.toString();
- const referenceSelector = lastCompoundSelectorWithoutPseudoClasses(selectorNode);
- if (referenceSelector === undefined) return;
- const selectorSpecificity = calculate(selectorNode);
- const entry = { selector, specificity: selectorSpecificity };
- const priorComparableSelectors = comparisonContext.get(referenceSelector);
- if (priorComparableSelectors === undefined) {
- comparisonContext.set(referenceSelector, [entry]);
- return;
- }
- for (const priorEntry of priorComparableSelectors) {
- if (compare(selectorSpecificity, priorEntry.specificity) < 0) {
- report({
- ruleName,
- result,
- node: ruleNode,
- message: messages.rejected(selector, priorEntry.selector),
- word: selector,
- });
- }
- }
- priorComparableSelectors.push(entry);
- }
- };
- };
- /**
- * @param {import('postcss-selector-parser').Root} selectorNode
- * @returns {string | undefined}
- */
- function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
- const firstChild = selectorNode.nodes[0];
- assert(firstChild);
- const nodesByCombinator = firstChild.split((node) => node.type === 'combinator');
- const nodesAfterLastCombinator = nodesByCombinator[nodesByCombinator.length - 1];
- assert(nodesAfterLastCombinator);
- const nodesWithoutPseudoClasses = nodesAfterLastCombinator.filter((node) => {
- return (
- node.type !== 'pseudo' ||
- node.value.startsWith('::') ||
- pseudoElements.has(node.value.replace(/:/g, ''))
- );
- });
- if (nodesWithoutPseudoClasses.length === 0) return undefined;
- return nodesWithoutPseudoClasses.join('');
- }
- rule.ruleName = ruleName;
- rule.messages = messages;
- rule.meta = meta;
- module.exports = rule;
|