index.cjs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // NOTICE: This file is generated by Rollup. To modify it,
  2. // please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
  3. 'use strict';
  4. const selectorSpecificity = require('@csstools/selector-specificity');
  5. const validateTypes = require('../../utils/validateTypes.cjs');
  6. const flattenNestedSelectorsForRule = require('../../utils/flattenNestedSelectorsForRule.cjs');
  7. const getStrippedSelectorSource = require('../../utils/getStrippedSelectorSource.cjs');
  8. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule.cjs');
  9. const optionsMatches = require('../../utils/optionsMatches.cjs');
  10. const report = require('../../utils/report.cjs');
  11. const ruleMessages = require('../../utils/ruleMessages.cjs');
  12. const validateOptions = require('../../utils/validateOptions.cjs');
  13. const ruleName = 'selector-max-specificity';
  14. const messages = ruleMessages(ruleName, {
  15. expected: (selector, max) => `Expected "${selector}" to have a specificity no more than "${max}"`,
  16. });
  17. const meta = {
  18. url: 'https://stylelint.io/user-guide/rules/selector-max-specificity',
  19. };
  20. /** @typedef {import('@csstools/selector-specificity').Specificity} Specificity */
  21. /**
  22. * Return a zero specificity. We need a new instance each time so that it can mutated.
  23. *
  24. * @returns {Specificity}
  25. */
  26. const zeroSpecificity = () => ({ a: 0, b: 0, c: 0 });
  27. /** @type {import('stylelint').CoreRules[ruleName]} */
  28. const rule = (primary, secondaryOptions) => {
  29. return (root, result) => {
  30. const validOptions = validateOptions(
  31. result,
  32. ruleName,
  33. {
  34. actual: primary,
  35. possible: [
  36. // Check that the max specificity is in the form "a,b,c"
  37. (spec) => validateTypes.isString(spec) && /^\d+,\d+,\d+$/.test(spec),
  38. ],
  39. },
  40. {
  41. actual: secondaryOptions,
  42. possible: {
  43. ignoreSelectors: [validateTypes.isString, validateTypes.isRegExp],
  44. },
  45. optional: true,
  46. },
  47. );
  48. if (!validOptions) {
  49. return;
  50. }
  51. /** @type {(selector: string) => boolean} */
  52. const isSelectorIgnored = (selector) =>
  53. optionsMatches(secondaryOptions, 'ignoreSelectors', selector);
  54. /** @type {import('@csstools/selector-specificity').CustomSpecificityCallback | undefined} */
  55. const customSpecificity = secondaryOptions?.ignoreSelectors
  56. ? (node) => {
  57. switch (node.type) {
  58. case 'attribute':
  59. case 'class':
  60. case 'id':
  61. case 'tag':
  62. if (!isSelectorIgnored(node.toString())) {
  63. return;
  64. }
  65. return zeroSpecificity();
  66. case 'pseudo': {
  67. if (!isSelectorIgnored(node.value.toLowerCase())) {
  68. return;
  69. }
  70. if (!node.nodes?.length) {
  71. return zeroSpecificity();
  72. }
  73. // We only ignore the current pseudo-class, not the specificity of the child nodes.
  74. // Calculate the diff between specificity with and without child nodes.
  75. const entireSpecificity = selectorSpecificity.selectorSpecificity(node);
  76. const emptySpecificity = selectorSpecificity.selectorSpecificity(node.clone({ nodes: [] }));
  77. return {
  78. a: entireSpecificity.a - emptySpecificity.a,
  79. b: entireSpecificity.b - emptySpecificity.b,
  80. c: entireSpecificity.c - emptySpecificity.c,
  81. };
  82. }
  83. default:
  84. // Other node types are not ignorable.
  85. }
  86. }
  87. : undefined;
  88. const [a, b, c] = primary.split(',').map((s) => Number.parseFloat(s));
  89. validateTypes.assertNumber(a);
  90. validateTypes.assertNumber(b);
  91. validateTypes.assertNumber(c);
  92. const maxSpecificity = { a, b, c };
  93. root.walkRules((ruleNode) => {
  94. if (!isStandardSyntaxRule(ruleNode)) return;
  95. flattenNestedSelectorsForRule(ruleNode, result).forEach(({ selector, resolvedSelectors }) => {
  96. resolvedSelectors.each((resolvedSelector) => {
  97. const specificity = selectorSpecificity.selectorSpecificity(resolvedSelector, { customSpecificity });
  98. // Check if the selector specificity exceeds the allowed maximum
  99. if (selectorSpecificity.compare(specificity, maxSpecificity) > 0) {
  100. const { index, endIndex, selector: selectorStr } = getStrippedSelectorSource(selector);
  101. report({
  102. ruleName,
  103. result,
  104. node: ruleNode,
  105. message: messages.expected,
  106. messageArgs: [selectorStr, primary],
  107. index,
  108. endIndex,
  109. });
  110. }
  111. });
  112. });
  113. });
  114. };
  115. };
  116. rule.ruleName = ruleName;
  117. rule.messages = messages;
  118. rule.meta = meta;
  119. module.exports = rule;