index.js 4.3 KB

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