UNPKG

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