UNPKG

4.55 kBJavaScriptView Raw
1'use strict';
2
3const findAtRuleContext = require('../../utils/findAtRuleContext');
4const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
5const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
6const keywordSets = require('../../reference/keywordSets');
7const nodeContextLookup = require('../../utils/nodeContextLookup');
8const optionsMatches = require('../../utils/optionsMatches');
9const parseSelector = require('../../utils/parseSelector');
10const report = require('../../utils/report');
11const resolvedNestedSelector = require('postcss-resolve-nested-selector');
12const ruleMessages = require('../../utils/ruleMessages');
13const specificity = require('specificity');
14const validateOptions = require('../../utils/validateOptions');
15
16const ruleName = 'no-descending-specificity';
17
18const messages = ruleMessages(ruleName, {
19 rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`,
20});
21
22const meta = {
23 url: 'https://stylelint.io/user-guide/rules/list/no-descending-specificity',
24};
25
26/** @type {import('stylelint').Rule} */
27const rule = (primary, secondaryOptions) => {
28 return (root, result) => {
29 const validOptions = validateOptions(
30 result,
31 ruleName,
32 {
33 actual: primary,
34 },
35 {
36 optional: true,
37 actual: secondaryOptions,
38 possible: {
39 ignore: ['selectors-within-list'],
40 },
41 },
42 );
43
44 if (!validOptions) {
45 return;
46 }
47
48 const selectorContextLookup = nodeContextLookup();
49
50 root.walkRules((ruleNode) => {
51 // Ignore nested property `foo: {};`
52 if (!isStandardSyntaxRule(ruleNode)) {
53 return;
54 }
55
56 // Ignores selectors within list of selectors
57 if (
58 optionsMatches(secondaryOptions, 'ignore', 'selectors-within-list') &&
59 ruleNode.selectors.length > 1
60 ) {
61 return;
62 }
63
64 const comparisonContext = selectorContextLookup.getContext(
65 ruleNode,
66 findAtRuleContext(ruleNode),
67 );
68
69 for (const selector of ruleNode.selectors) {
70 const trimSelector = selector.trim();
71
72 // Ignore `.selector, { }`
73 if (trimSelector === '') {
74 continue;
75 }
76
77 // The edge-case of duplicate selectors will act acceptably
78 const index = ruleNode.selector.indexOf(trimSelector);
79
80 // Resolve any nested selectors before checking
81 for (const resolvedSelector of resolvedNestedSelector(selector, ruleNode)) {
82 parseSelector(resolvedSelector, result, ruleNode, (s) => {
83 if (!isStandardSyntaxSelector(resolvedSelector)) {
84 return;
85 }
86
87 checkSelector(s, ruleNode, index, comparisonContext);
88 });
89 }
90 }
91 });
92
93 /**
94 * @param {import('postcss-selector-parser').Root} selectorNode
95 * @param {import('postcss').Rule} ruleNode
96 * @param {number} sourceIndex
97 * @param {Map<any, any>} comparisonContext
98 */
99 function checkSelector(selectorNode, ruleNode, sourceIndex, comparisonContext) {
100 const selector = selectorNode.toString();
101 const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses(selectorNode);
102 const selectorSpecificity = specificity.calculate(selector)[0].specificityArray;
103 const entry = { selector, specificity: selectorSpecificity };
104
105 if (!comparisonContext.has(referenceSelectorNode)) {
106 comparisonContext.set(referenceSelectorNode, [entry]);
107
108 return;
109 }
110
111 /** @type {Array<{ selector: string, specificity: import('specificity').SpecificityArray }>} */
112 const priorComparableSelectors = comparisonContext.get(referenceSelectorNode);
113
114 for (const priorEntry of priorComparableSelectors) {
115 if (specificity.compare(selectorSpecificity, priorEntry.specificity) === -1) {
116 report({
117 ruleName,
118 result,
119 node: ruleNode,
120 message: messages.rejected(selector, priorEntry.selector),
121 index: sourceIndex,
122 });
123 }
124 }
125
126 priorComparableSelectors.push(entry);
127 }
128 };
129};
130
131/**
132 * @param {import('postcss-selector-parser').Root} selectorNode
133 */
134function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
135 const nodesByCombinator = selectorNode.nodes[0].split((node) => node.type === 'combinator');
136 const nodesAfterLastCombinator = nodesByCombinator[nodesByCombinator.length - 1];
137
138 const nodesWithoutPseudoClasses = nodesAfterLastCombinator
139 .filter((node) => {
140 return (
141 node.type !== 'pseudo' ||
142 node.value.startsWith('::') ||
143 keywordSets.pseudoElements.has(node.value.replace(/:/g, ''))
144 );
145 })
146 .join('');
147
148 return nodesWithoutPseudoClasses.toString();
149}
150
151rule.ruleName = ruleName;
152rule.messages = messages;
153rule.meta = meta;
154module.exports = rule;