UNPKG

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