UNPKG

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