4
0

index.mjs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import parser from 'postcss-selector-parser';
  2. import { isAtRule, isDeclaration, isRoot, isRule } from '../../utils/typeGuards.mjs';
  3. import { isNumber, isRegExp, isString } from '../../utils/validateTypes.mjs';
  4. import hasBlock from '../../utils/hasBlock.mjs';
  5. import isStandardSyntaxRule from '../../utils/isStandardSyntaxRule.mjs';
  6. import optionsMatches from '../../utils/optionsMatches.mjs';
  7. import report from '../../utils/report.mjs';
  8. import ruleMessages from '../../utils/ruleMessages.mjs';
  9. import validateOptions from '../../utils/validateOptions.mjs';
  10. const ruleName = 'max-nesting-depth';
  11. const messages = ruleMessages(ruleName, {
  12. expected: (depth) => `Expected nesting depth to be no more than ${depth}`,
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/max-nesting-depth',
  16. };
  17. /** @type {import('stylelint').CoreRules[ruleName]} */
  18. const rule = (primary, secondaryOptions) => {
  19. /**
  20. * @param {import('postcss').Node} node
  21. */
  22. const isIgnoreAtRule = (node) =>
  23. isAtRule(node) && optionsMatches(secondaryOptions, 'ignoreAtRules', node.name);
  24. /**
  25. * @param {import('postcss').Node} node
  26. */
  27. const isIgnoreRule = (node) => {
  28. return isRule(node) && optionsMatches(secondaryOptions, 'ignoreRules', node.selector);
  29. };
  30. return (root, result) => {
  31. const validOptions = validateOptions(
  32. result,
  33. ruleName,
  34. {
  35. actual: primary,
  36. possible: [isNumber],
  37. },
  38. {
  39. optional: true,
  40. actual: secondaryOptions,
  41. possible: {
  42. ignore: ['blockless-at-rules', 'pseudo-classes'],
  43. ignoreAtRules: [isString, isRegExp],
  44. ignoreRules: [isString, isRegExp],
  45. ignorePseudoClasses: [isString, isRegExp],
  46. },
  47. },
  48. );
  49. if (!validOptions) return;
  50. root.walkRules(checkStatement);
  51. root.walkAtRules(checkStatement);
  52. /**
  53. * @param {import('postcss').Rule | import('postcss').AtRule} statement
  54. */
  55. function checkStatement(statement) {
  56. if (isIgnoreAtRule(statement)) {
  57. return;
  58. }
  59. if (isIgnoreRule(statement)) {
  60. return;
  61. }
  62. if (!hasBlock(statement)) {
  63. return;
  64. }
  65. if (isRule(statement) && !isStandardSyntaxRule(statement)) {
  66. return;
  67. }
  68. const depth = nestingDepth(statement, 0);
  69. if (depth > primary) {
  70. report({
  71. ruleName,
  72. result,
  73. node: statement,
  74. message: messages.expected,
  75. messageArgs: [primary],
  76. });
  77. }
  78. }
  79. };
  80. /**
  81. * @param {import('postcss').Node} node
  82. * @param {number} level
  83. * @returns {number}
  84. */
  85. function nestingDepth(node, level) {
  86. const parent = node.parent;
  87. if (!parent) {
  88. return 0;
  89. }
  90. if (isIgnoreAtRule(parent)) {
  91. return 0;
  92. }
  93. // The nesting depth level's computation has finished
  94. // when this function, recursively called, receives
  95. // a node that is not nested -- a direct child of the
  96. // root node
  97. if (isRoot(parent) || (isAtRule(parent) && parent.parent && isRoot(parent.parent))) {
  98. return level;
  99. }
  100. /**
  101. * @param {string} selector
  102. */
  103. function containsPseudoClassesOnly(selector) {
  104. const normalized = parser().processSync(selector, { lossless: false });
  105. const selectors = normalized.split(',');
  106. const filteredSelectors = selectors.filter(
  107. (sel) => !optionsMatches(secondaryOptions, 'ignoreRules', sel),
  108. );
  109. return filteredSelectors.every((sel) => extractPseudoRule(sel));
  110. }
  111. /**
  112. * @param {string[]} selectors
  113. * @returns {boolean}
  114. */
  115. function containsIgnoredPseudoClassesOrRulesOnly(selectors) {
  116. if (
  117. !(
  118. secondaryOptions &&
  119. (secondaryOptions.ignorePseudoClasses || secondaryOptions.ignoreRules)
  120. )
  121. )
  122. return false;
  123. return selectors.every((selector) => {
  124. if (
  125. secondaryOptions.ignoreRules &&
  126. optionsMatches(secondaryOptions, 'ignoreRules', selector)
  127. )
  128. return true;
  129. if (!secondaryOptions.ignorePseudoClasses) return false;
  130. const pseudoRule = extractPseudoRule(selector);
  131. if (!pseudoRule) return false;
  132. return optionsMatches(secondaryOptions, 'ignorePseudoClasses', pseudoRule);
  133. });
  134. }
  135. if (
  136. (optionsMatches(secondaryOptions, 'ignore', 'blockless-at-rules') &&
  137. isAtRule(node) &&
  138. node.every((child) => !isDeclaration(child))) ||
  139. (optionsMatches(secondaryOptions, 'ignore', 'pseudo-classes') &&
  140. isRule(node) &&
  141. containsPseudoClassesOnly(node.selector)) ||
  142. (isRule(node) && containsIgnoredPseudoClassesOrRulesOnly(node.selectors))
  143. ) {
  144. return nestingDepth(parent, level);
  145. }
  146. // Unless any of the conditions above apply, we want to
  147. // add 1 to the nesting depth level and then check the parent,
  148. // continuing to add and move up the hierarchy
  149. // until we hit the root node
  150. return nestingDepth(parent, level + 1);
  151. }
  152. };
  153. /**
  154. * @param {string} selector
  155. * @returns {string | undefined}
  156. */
  157. function extractPseudoRule(selector) {
  158. return selector.startsWith('&:') && selector[2] !== ':' ? selector.slice(2) : undefined;
  159. }
  160. rule.ruleName = ruleName;
  161. rule.messages = messages;
  162. rule.meta = meta;
  163. export default rule;