index.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. 'use strict';
  2. const isNonNegativeInteger = require('../../utils/isNonNegativeInteger');
  3. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  4. const parseSelector = require('../../utils/parseSelector');
  5. const report = require('../../utils/report');
  6. const resolvedNestedSelector = require('postcss-resolve-nested-selector');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const selectorParser = require('postcss-selector-parser');
  9. const validateOptions = require('../../utils/validateOptions');
  10. const optionsMatches = require('../../utils/optionsMatches');
  11. const { isString } = require('../../utils/validateTypes');
  12. const ruleName = 'selector-max-universal';
  13. const messages = ruleMessages(ruleName, {
  14. expected: (selector, max) =>
  15. `Expected "${selector}" to have no more than ${max} universal ${
  16. max === 1 ? 'selector' : 'selectors'
  17. }`,
  18. });
  19. const meta = {
  20. url: 'https://stylelint.io/user-guide/rules/list/selector-max-universal',
  21. };
  22. /** @type {import('stylelint').Rule} */
  23. const rule = (primary, secondaryOptions) => {
  24. return (root, result) => {
  25. const validOptions = validateOptions(
  26. result,
  27. ruleName,
  28. {
  29. actual: primary,
  30. possible: isNonNegativeInteger,
  31. },
  32. {
  33. actual: secondaryOptions,
  34. possible: {
  35. ignoreAfterCombinators: [isString],
  36. },
  37. optional: true,
  38. },
  39. );
  40. if (!validOptions) {
  41. return;
  42. }
  43. /**
  44. * @param {import('postcss-selector-parser').Container<unknown>} selectorNode
  45. * @param {import('postcss').Rule} ruleNode
  46. */
  47. function checkSelector(selectorNode, ruleNode) {
  48. const count = selectorNode.reduce((total, childNode) => {
  49. // Only traverse inside actual selectors
  50. // All logical combinations will be resolved as nested selector in `postcss-resolve-nested-selector`
  51. if (childNode.type === 'selector') {
  52. checkSelector(childNode, ruleNode);
  53. }
  54. const prevChildNode = childNode.prev();
  55. const prevChildNodeValue = prevChildNode && prevChildNode.value;
  56. if (childNode.type === 'universal') {
  57. if (!optionsMatches(secondaryOptions, 'ignoreAfterCombinators', prevChildNodeValue)) {
  58. total += 1;
  59. }
  60. }
  61. return total;
  62. }, 0);
  63. if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > primary) {
  64. const selector = selectorNode.toString();
  65. report({
  66. ruleName,
  67. result,
  68. node: ruleNode,
  69. message: messages.expected(selector, primary),
  70. word: selector,
  71. });
  72. }
  73. }
  74. root.walkRules((ruleNode) => {
  75. if (!isStandardSyntaxRule(ruleNode)) {
  76. return;
  77. }
  78. /** @type {string[]} */
  79. const selectors = [];
  80. selectorParser()
  81. .astSync(ruleNode.selector)
  82. .walk((node) => {
  83. if (node.type === 'selector') {
  84. selectors.push(String(node).trim());
  85. }
  86. });
  87. for (const selector of selectors) {
  88. for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
  89. parseSelector(resolvedSelector, result, ruleNode, (container) =>
  90. checkSelector(container, ruleNode),
  91. );
  92. }
  93. }
  94. });
  95. };
  96. };
  97. rule.ruleName = ruleName;
  98. rule.messages = messages;
  99. rule.meta = meta;
  100. module.exports = rule;