index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // @ts-nocheck
  2. 'use strict';
  3. const _ = require('lodash');
  4. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  5. const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
  6. const keywordSets = require('../../reference/keywordSets');
  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 = 'selector-max-specificity';
  15. const messages = ruleMessages(ruleName, {
  16. expected: (selector, specificity) =>
  17. `Expected "${selector}" to have a specificity no more than "${specificity}"`,
  18. });
  19. // Return an array representation of zero specificity. We need a new array each time so that it can mutated
  20. const zeroSpecificity = () => [0, 0, 0, 0];
  21. // Calculate the sum of given array of specificity arrays
  22. const specificitySum = (specificities) => {
  23. const sum = zeroSpecificity();
  24. specificities.forEach((specificityArray) => {
  25. specificityArray.forEach((value, i) => {
  26. sum[i] += value;
  27. });
  28. });
  29. return sum;
  30. };
  31. function rule(max, options) {
  32. return (root, result) => {
  33. const validOptions = validateOptions(
  34. result,
  35. ruleName,
  36. {
  37. actual: max,
  38. possible: [
  39. function (max) {
  40. // Check that the max specificity is in the form "a,b,c"
  41. return /^\d+,\d+,\d+$/.test(max);
  42. },
  43. ],
  44. },
  45. {
  46. actual: options,
  47. possible: {
  48. ignoreSelectors: [_.isString, _.isRegExp],
  49. },
  50. optional: true,
  51. },
  52. );
  53. if (!validOptions) {
  54. return;
  55. }
  56. // Calculate the specificity of a simple selector (type, attribute, class, ID, or pseudos's own value)
  57. const simpleSpecificity = (selector) => {
  58. if (optionsMatches(options, 'ignoreSelectors', selector)) {
  59. return zeroSpecificity();
  60. }
  61. return specificity.calculate(selector)[0].specificityArray;
  62. };
  63. // Calculate the the specificity of the most specific direct child
  64. const maxChildSpecificity = (node) =>
  65. node.reduce((max, child) => {
  66. const childSpecificity = nodeSpecificity(child); // eslint-disable-line no-use-before-define
  67. return specificity.compare(childSpecificity, max) === 1 ? childSpecificity : max;
  68. }, zeroSpecificity());
  69. // Calculate the specificity of a pseudo selector including own value and children
  70. const pseudoSpecificity = (node) => {
  71. // `node.toString()` includes children which should be processed separately,
  72. // so use `node.value` instead
  73. const ownValue = node.value;
  74. const ownSpecificity =
  75. ownValue === ':not' || ownValue === ':matches'
  76. ? // :not and :matches don't add specificity themselves, but their children do
  77. zeroSpecificity()
  78. : simpleSpecificity(ownValue);
  79. return specificitySum([ownSpecificity, maxChildSpecificity(node)]);
  80. };
  81. const shouldSkipPseudoClassArgument = (node) => {
  82. // postcss-selector-parser includes the arguments to nth-child() functions
  83. // as "tags", so we need to ignore them ourselves.
  84. // The fake-tag's "parent" is actually a selector node, whose parent
  85. // should be the :nth-child pseudo node.
  86. const parentNode = node.parent.parent;
  87. if (parentNode && parentNode.value) {
  88. const parentNodeValue = parentNode.value;
  89. const normalisedParentNode = parentNodeValue.toLowerCase().replace(/:+/, '');
  90. return (
  91. parentNode.type === 'pseudo' &&
  92. (keywordSets.aNPlusBNotationPseudoClasses.has(normalisedParentNode) ||
  93. keywordSets.linguisticPseudoClasses.has(normalisedParentNode))
  94. );
  95. }
  96. return false;
  97. };
  98. // Calculate the specificity of a node parsed by `postcss-selector-parser`
  99. const nodeSpecificity = (node) => {
  100. if (shouldSkipPseudoClassArgument(node)) {
  101. return zeroSpecificity();
  102. }
  103. switch (node.type) {
  104. case 'attribute':
  105. case 'class':
  106. case 'id':
  107. case 'tag':
  108. return simpleSpecificity(node.toString());
  109. case 'pseudo':
  110. return pseudoSpecificity(node);
  111. case 'selector':
  112. // Calculate the sum of all the direct children
  113. return specificitySum(node.map(nodeSpecificity));
  114. default:
  115. return zeroSpecificity();
  116. }
  117. };
  118. const maxSpecificityArray = `0,${max}`.split(',').map(parseFloat);
  119. root.walkRules((rule) => {
  120. if (!isStandardSyntaxRule(rule)) {
  121. return;
  122. }
  123. // Using rule.selectors gets us each selector in the eventuality we have a comma separated set
  124. rule.selectors.forEach((selector) => {
  125. resolvedNestedSelector(selector, rule).forEach((resolvedSelector) => {
  126. try {
  127. // Skip non-standard syntax selectors
  128. if (!isStandardSyntaxSelector(resolvedSelector)) {
  129. return;
  130. }
  131. parseSelector(resolvedSelector, result, rule, (selectorTree) => {
  132. // Check if the selector specificity exceeds the allowed maximum
  133. if (
  134. specificity.compare(maxChildSpecificity(selectorTree), maxSpecificityArray) === 1
  135. ) {
  136. report({
  137. ruleName,
  138. result,
  139. node: rule,
  140. message: messages.expected(resolvedSelector, max),
  141. word: selector,
  142. });
  143. }
  144. });
  145. } catch (e) {
  146. result.warn('Cannot parse selector', {
  147. node: rule,
  148. stylelintType: 'parseError',
  149. });
  150. }
  151. });
  152. });
  153. });
  154. };
  155. }
  156. rule.ruleName = ruleName;
  157. rule.messages = messages;
  158. module.exports = rule;