123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- 'use strict';
- const findAtRuleContext = require('../../utils/findAtRuleContext');
- const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
- const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
- const keywordSets = require('../../reference/keywordSets');
- const nodeContextLookup = require('../../utils/nodeContextLookup');
- const optionsMatches = require('../../utils/optionsMatches');
- const parseSelector = require('../../utils/parseSelector');
- const report = require('../../utils/report');
- const resolvedNestedSelector = require('postcss-resolve-nested-selector');
- const ruleMessages = require('../../utils/ruleMessages');
- const specificity = require('specificity');
- const validateOptions = require('../../utils/validateOptions');
- 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/list/no-descending-specificity',
- };
- /** @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 selectorContextLookup = nodeContextLookup();
- root.walkRules((ruleNode) => {
- // Ignore nested property `foo: {};`
- if (!isStandardSyntaxRule(ruleNode)) {
- return;
- }
- // Ignores selectors within list of selectors
- if (
- optionsMatches(secondaryOptions, 'ignore', 'selectors-within-list') &&
- ruleNode.selectors.length > 1
- ) {
- return;
- }
- const comparisonContext = selectorContextLookup.getContext(
- ruleNode,
- findAtRuleContext(ruleNode),
- );
- for (const selector of ruleNode.selectors) {
- const trimSelector = selector.trim();
- // Ignore `.selector, { }`
- if (trimSelector === '') {
- continue;
- }
- // The edge-case of duplicate selectors will act acceptably
- const index = ruleNode.selector.indexOf(trimSelector);
- // 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, index, comparisonContext);
- });
- }
- }
- });
- /**
- * @param {import('postcss-selector-parser').Root} selectorNode
- * @param {import('postcss').Rule} ruleNode
- * @param {number} sourceIndex
- * @param {Map<any, any>} comparisonContext
- */
- function checkSelector(selectorNode, ruleNode, sourceIndex, comparisonContext) {
- const selector = selectorNode.toString();
- const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses(selectorNode);
- const selectorSpecificity = specificity.calculate(selector)[0].specificityArray;
- const entry = { selector, specificity: selectorSpecificity };
- if (!comparisonContext.has(referenceSelectorNode)) {
- comparisonContext.set(referenceSelectorNode, [entry]);
- return;
- }
- /** @type {Array<{ selector: string, specificity: import('specificity').SpecificityArray }>} */
- const priorComparableSelectors = comparisonContext.get(referenceSelectorNode);
- for (const priorEntry of priorComparableSelectors) {
- if (specificity.compare(selectorSpecificity, priorEntry.specificity) === -1) {
- report({
- ruleName,
- result,
- node: ruleNode,
- message: messages.rejected(selector, priorEntry.selector),
- index: sourceIndex,
- });
- }
- }
- priorComparableSelectors.push(entry);
- }
- };
- };
- /**
- * @param {import('postcss-selector-parser').Root} selectorNode
- */
- function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
- const nodesByCombinator = selectorNode.nodes[0].split((node) => node.type === 'combinator');
- const nodesAfterLastCombinator = nodesByCombinator[nodesByCombinator.length - 1];
- const nodesWithoutPseudoClasses = nodesAfterLastCombinator
- .filter((node) => {
- return (
- node.type !== 'pseudo' ||
- node.value.startsWith('::') ||
- keywordSets.pseudoElements.has(node.value.replace(/:/g, ''))
- );
- })
- .join('');
- return nodesWithoutPseudoClasses.toString();
- }
- rule.ruleName = ruleName;
- rule.messages = messages;
- rule.meta = meta;
- module.exports = rule;
|