UNPKG

4.54 kBJavaScriptView Raw
1// @ts-nocheck
2
3'use strict';
4
5const _ = require('lodash');
6const isKeyframeSelector = require('../../utils/isKeyframeSelector');
7const isLogicalCombination = require('../../utils/isLogicalCombination');
8const isOnlyWhitespace = require('../../utils/isOnlyWhitespace');
9const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
10const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
11const optionsMatches = require('../../utils/optionsMatches');
12const parseSelector = require('../../utils/parseSelector');
13const report = require('../../utils/report');
14const resolvedNestedSelector = require('postcss-resolve-nested-selector');
15const ruleMessages = require('../../utils/ruleMessages');
16const validateOptions = require('../../utils/validateOptions');
17
18const ruleName = 'selector-max-type';
19
20const messages = ruleMessages(ruleName, {
21 expected: (selector, max) =>
22 `Expected "${selector}" to have no more than ${max} type ${
23 max === 1 ? 'selector' : 'selectors'
24 }`,
25});
26
27function rule(max, options) {
28 return (root, result) => {
29 const validOptions = validateOptions(
30 result,
31 ruleName,
32 {
33 actual: max,
34 possible(max) {
35 return typeof max === 'number' && max >= 0;
36 },
37 },
38 {
39 actual: options,
40 possible: {
41 ignore: ['descendant', 'child', 'compounded', 'next-sibling'],
42 ignoreTypes: [_.isString, _.isRegExp],
43 },
44 optional: true,
45 },
46 );
47
48 if (!validOptions) {
49 return;
50 }
51
52 const ignoreDescendant = optionsMatches(options, 'ignore', 'descendant');
53 const ignoreChild = optionsMatches(options, 'ignore', 'child');
54 const ignoreCompounded = optionsMatches(options, 'ignore', 'compounded');
55 const ignoreNextSibling = optionsMatches(options, 'ignore', 'next-sibling');
56
57 function checkSelector(selectorNode, ruleNode) {
58 const count = selectorNode.reduce((total, childNode) => {
59 // Only traverse inside actual selectors and logical combinations
60 if (childNode.type === 'selector' || isLogicalCombination(childNode)) {
61 checkSelector(childNode, ruleNode);
62 }
63
64 if (optionsMatches(options, 'ignoreTypes', childNode.value)) {
65 return total;
66 }
67
68 if (ignoreDescendant && hasDescendantCombinatorBefore(childNode)) {
69 return total;
70 }
71
72 if (ignoreChild && hasChildCombinatorBefore(childNode)) {
73 return total;
74 }
75
76 if (ignoreCompounded && hasCompoundSelector(childNode)) {
77 return total;
78 }
79
80 if (ignoreNextSibling && hasNextSiblingCombinator(childNode)) {
81 return total;
82 }
83
84 return total + (childNode.type === 'tag');
85 }, 0);
86
87 if (selectorNode.type !== 'root' && selectorNode.type !== 'pseudo' && count > max) {
88 report({
89 ruleName,
90 result,
91 node: ruleNode,
92 message: messages.expected(selectorNode, max),
93 word: selectorNode,
94 });
95 }
96 }
97
98 root.walkRules((ruleNode) => {
99 const selectors = ruleNode.selectors;
100
101 if (!isStandardSyntaxRule(ruleNode)) {
102 return;
103 }
104
105 if (selectors.some((s) => isKeyframeSelector(s))) {
106 return;
107 }
108
109 ruleNode.selectors.forEach((selector) => {
110 resolvedNestedSelector(selector, ruleNode).forEach((resolvedSelector) => {
111 if (!isStandardSyntaxSelector(resolvedSelector)) {
112 return;
113 }
114
115 parseSelector(resolvedSelector, result, ruleNode, (container) =>
116 checkSelector(container, ruleNode),
117 );
118 });
119 });
120 });
121 };
122}
123
124function hasDescendantCombinatorBefore(node) {
125 const nodeIndex = node.parent.nodes.indexOf(node);
126
127 return node.parent.nodes.slice(0, nodeIndex).some(isDescendantCombinator);
128}
129
130function hasChildCombinatorBefore(node) {
131 const nodeIndex = node.parent.nodes.indexOf(node);
132
133 return node.parent.nodes.slice(0, nodeIndex).some(isChildCombinator);
134}
135
136function hasCompoundSelector(node) {
137 if (node.prev() && !isCombinator(node.prev())) {
138 return true;
139 }
140
141 return node.next() && !isCombinator(node.next());
142}
143
144function hasNextSiblingCombinator(node) {
145 return node.prev() && isNextSiblingCombinator(node.prev());
146}
147
148function isCombinator(node) {
149 if (!node) return false;
150
151 return _.get(node, 'type') === 'combinator';
152}
153
154function isDescendantCombinator(node) {
155 if (!node) return false;
156
157 return isCombinator(node) && isOnlyWhitespace(node.value);
158}
159
160function isChildCombinator(node) {
161 if (!node) return false;
162
163 return isCombinator(node) && node.value === '>';
164}
165
166function isNextSiblingCombinator(node) {
167 return isCombinator(node) && node.value === '+';
168}
169
170rule.ruleName = ruleName;
171rule.messages = messages;
172module.exports = rule;