index.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. 'use strict';
  2. const findAtRuleContext = require('../../utils/findAtRuleContext');
  3. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  4. const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
  5. const keywordSets = require('../../reference/keywordSets');
  6. const nodeContextLookup = require('../../utils/nodeContextLookup');
  7. const optionsMatches = require('../../utils/optionsMatches');
  8. const parseSelector = require('../../utils/parseSelector');
  9. const report = require('../../utils/report');
  10. const resolvedNestedSelector = require('postcss-resolve-nested-selector');
  11. const ruleMessages = require('../../utils/ruleMessages');
  12. const specificity = require('specificity');
  13. const validateOptions = require('../../utils/validateOptions');
  14. const ruleName = 'no-descending-specificity';
  15. const messages = ruleMessages(ruleName, {
  16. rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`,
  17. });
  18. const meta = {
  19. url: 'https://stylelint.io/user-guide/rules/list/no-descending-specificity',
  20. };
  21. /** @type {import('stylelint').Rule} */
  22. const rule = (primary, secondaryOptions) => {
  23. return (root, result) => {
  24. const validOptions = validateOptions(
  25. result,
  26. ruleName,
  27. {
  28. actual: primary,
  29. },
  30. {
  31. optional: true,
  32. actual: secondaryOptions,
  33. possible: {
  34. ignore: ['selectors-within-list'],
  35. },
  36. },
  37. );
  38. if (!validOptions) {
  39. return;
  40. }
  41. const selectorContextLookup = nodeContextLookup();
  42. root.walkRules((ruleNode) => {
  43. // Ignore nested property `foo: {};`
  44. if (!isStandardSyntaxRule(ruleNode)) {
  45. return;
  46. }
  47. // Ignores selectors within list of selectors
  48. if (
  49. optionsMatches(secondaryOptions, 'ignore', 'selectors-within-list') &&
  50. ruleNode.selectors.length > 1
  51. ) {
  52. return;
  53. }
  54. const comparisonContext = selectorContextLookup.getContext(
  55. ruleNode,
  56. findAtRuleContext(ruleNode),
  57. );
  58. for (const selector of ruleNode.selectors) {
  59. const trimSelector = selector.trim();
  60. // Ignore `.selector, { }`
  61. if (trimSelector === '') {
  62. continue;
  63. }
  64. // The edge-case of duplicate selectors will act acceptably
  65. const index = ruleNode.selector.indexOf(trimSelector);
  66. // Resolve any nested selectors before checking
  67. for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
  68. parseSelector(resolvedSelector, result, ruleNode, (s) => {
  69. if (!isStandardSyntaxSelector(resolvedSelector)) {
  70. return;
  71. }
  72. checkSelector(s, ruleNode, index, comparisonContext);
  73. });
  74. }
  75. }
  76. });
  77. /**
  78. * @param {import('postcss-selector-parser').Root} selectorNode
  79. * @param {import('postcss').Rule} ruleNode
  80. * @param {number} sourceIndex
  81. * @param {Map<any, any>} comparisonContext
  82. */
  83. function checkSelector(selectorNode, ruleNode, sourceIndex, comparisonContext) {
  84. const selector = selectorNode.toString();
  85. const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses(selectorNode);
  86. const selectorSpecificity = specificity.calculate(selector)[0].specificityArray;
  87. const entry = { selector, specificity: selectorSpecificity };
  88. if (!comparisonContext.has(referenceSelectorNode)) {
  89. comparisonContext.set(referenceSelectorNode, [entry]);
  90. return;
  91. }
  92. /** @type {Array<{ selector: string, specificity: import('specificity').SpecificityArray }>} */
  93. const priorComparableSelectors = comparisonContext.get(referenceSelectorNode);
  94. for (const priorEntry of priorComparableSelectors) {
  95. if (specificity.compare(selectorSpecificity, priorEntry.specificity) === -1) {
  96. report({
  97. ruleName,
  98. result,
  99. node: ruleNode,
  100. message: messages.rejected(selector, priorEntry.selector),
  101. index: sourceIndex,
  102. });
  103. }
  104. }
  105. priorComparableSelectors.push(entry);
  106. }
  107. };
  108. };
  109. /**
  110. * @param {import('postcss-selector-parser').Root} selectorNode
  111. */
  112. function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
  113. const nodesByCombinator = selectorNode.nodes[0].split((node) => node.type === 'combinator');
  114. const nodesAfterLastCombinator = nodesByCombinator[nodesByCombinator.length - 1];
  115. const nodesWithoutPseudoClasses = nodesAfterLastCombinator
  116. .filter((node) => {
  117. return (
  118. node.type !== 'pseudo' ||
  119. node.value.startsWith('::') ||
  120. keywordSets.pseudoElements.has(node.value.replace(/:/g, ''))
  121. );
  122. })
  123. .join('');
  124. return nodesWithoutPseudoClasses.toString();
  125. }
  126. rule.ruleName = ruleName;
  127. rule.messages = messages;
  128. rule.meta = meta;
  129. module.exports = rule;