index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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(primary),
  66. });
  67. }
  68. }
  69. };
  70. /**
  71. * @param {import('postcss').Node} node
  72. * @param {number} level
  73. * @returns {number}
  74. */
  75. function nestingDepth(node, level) {
  76. const parent = node.parent;
  77. if (parent == null) {
  78. throw new Error('The parent node must exist');
  79. }
  80. if (isIgnoreAtRule(parent)) {
  81. return 0;
  82. }
  83. // The nesting depth level's computation has finished
  84. // when this function, recursively called, receives
  85. // a node that is not nested -- a direct child of the
  86. // root node
  87. if (isRoot(parent) || (isAtRule(parent) && parent.parent && isRoot(parent.parent))) {
  88. return level;
  89. }
  90. /**
  91. * @param {string} selector
  92. */
  93. function containsPseudoClassesOnly(selector) {
  94. const normalized = parser().processSync(selector, { lossless: false });
  95. const selectors = normalized.split(',');
  96. return selectors.every((sel) => extractPseudoRule(sel));
  97. }
  98. /**
  99. * @param {string[]} selectors
  100. * @returns {boolean}
  101. */
  102. function containsIgnoredPseudoClassesOnly(selectors) {
  103. if (!(secondaryOptions && secondaryOptions.ignorePseudoClasses)) return false;
  104. return selectors.every((selector) => {
  105. const pseudoRule = extractPseudoRule(selector);
  106. if (!pseudoRule) return false;
  107. return optionsMatches(secondaryOptions, 'ignorePseudoClasses', pseudoRule);
  108. });
  109. }
  110. if (
  111. (optionsMatches(secondaryOptions, 'ignore', 'blockless-at-rules') &&
  112. isAtRule(node) &&
  113. node.every((child) => !isDeclaration(child))) ||
  114. (optionsMatches(secondaryOptions, 'ignore', 'pseudo-classes') &&
  115. isRule(node) &&
  116. containsPseudoClassesOnly(node.selector)) ||
  117. (isRule(node) && containsIgnoredPseudoClassesOnly(node.selectors))
  118. ) {
  119. return nestingDepth(parent, level);
  120. }
  121. // Unless any of the conditions above apply, we want to
  122. // add 1 to the nesting depth level and then check the parent,
  123. // continuing to add and move up the hierarchy
  124. // until we hit the root node
  125. return nestingDepth(parent, level + 1);
  126. }
  127. };
  128. /**
  129. * @param {string} selector
  130. * @returns {string | undefined}
  131. */
  132. function extractPseudoRule(selector) {
  133. return selector.startsWith('&:') && selector[2] !== ':' ? selector.slice(2) : undefined;
  134. }
  135. rule.ruleName = ruleName;
  136. rule.messages = messages;
  137. rule.meta = meta;
  138. module.exports = rule;