UNPKG

4.15 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 // TODO: Issue #4985
50 // eslint-disable-next-line no-shadow
51 root.walkRules((rule) => {
52 // Ignore custom property set `--foo: {};`
53 if (isCustomPropertySet(rule)) {
54 return;
55 }
56
57 // Ignore nested property `foo: {};`
58 if (!isStandardSyntaxRule(rule)) {
59 return;
60 }
61
62 // Ignores selectors within list of selectors
63 if (optionsMatches(options, 'ignore', 'selectors-within-list') && rule.selectors.length > 1) {
64 return;
65 }
66
67 const comparisonContext = selectorContextLookup.getContext(rule, findAtRuleContext(rule));
68
69 rule.selectors.forEach((selector) => {
70 const trimSelector = selector.trim();
71
72 // Ignore `.selector, { }`
73 if (trimSelector === '') {
74 return;
75 }
76
77 // The edge-case of duplicate selectors will act acceptably
78 const index = rule.selector.indexOf(trimSelector);
79
80 // Resolve any nested selectors before checking
81 resolvedNestedSelector(selector, rule).forEach((resolvedSelector) => {
82 parseSelector(resolvedSelector, result, rule, (s) => {
83 if (!isStandardSyntaxSelector(resolvedSelector)) {
84 return;
85 }
86
87 checkSelector(s, rule, index, comparisonContext);
88 });
89 });
90 });
91 });
92
93 // TODO: Issue #4985
94 // eslint-disable-next-line no-shadow
95 function checkSelector(selectorNode, rule, sourceIndex, comparisonContext) {
96 const selector = selectorNode.toString();
97 const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses(selectorNode);
98 const selectorSpecificity = specificity.calculate(selector)[0].specificityArray;
99 const entry = { selector, specificity: selectorSpecificity };
100
101 if (!comparisonContext.has(referenceSelectorNode)) {
102 comparisonContext.set(referenceSelectorNode, [entry]);
103
104 return;
105 }
106
107 const priorComparableSelectors = comparisonContext.get(referenceSelectorNode);
108
109 priorComparableSelectors.forEach((priorEntry) => {
110 if (specificity.compare(selectorSpecificity, priorEntry.specificity) === -1) {
111 report({
112 ruleName,
113 result,
114 node: rule,
115 message: messages.rejected(selector, priorEntry.selector),
116 index: sourceIndex,
117 });
118 }
119 });
120
121 priorComparableSelectors.push(entry);
122 }
123 };
124}
125
126function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
127 const nodesAfterLastCombinator = _.last(
128 selectorNode.nodes[0].split((node) => {
129 return node.type === 'combinator';
130 }),
131 );
132
133 const nodesWithoutPseudoClasses = nodesAfterLastCombinator
134 .filter((node) => {
135 return node.type !== 'pseudo' || keywordSets.pseudoElements.has(node.value.replace(/:/g, ''));
136 })
137 .join('');
138
139 return nodesWithoutPseudoClasses.toString();
140}
141
142rule.ruleName = ruleName;
143rule.messages = messages;
144module.exports = rule;