3 const _ = require("lodash");
4 const findAtRuleContext = require("../../utils/findAtRuleContext");
5 const isCustomPropertySet = require("../../utils/isCustomPropertySet");
6 const keywordSets = require("../../reference/keywordSets");
7 const nodeContextLookup = require("../../utils/nodeContextLookup");
8 const parseSelector = require("../../utils/parseSelector");
9 const report = require("../../utils/report");
10 const resolvedNestedSelector = require("postcss-resolve-nested-selector");
11 const ruleMessages = require("../../utils/ruleMessages");
12 const specificity = require("specificity");
13 const validateOptions = require("../../utils/validateOptions");
15 const ruleName = "no-descending-specificity";
17 const messages = ruleMessages(ruleName, {
18 rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`
21 const rule = function(actual) {
22 return (root, result) => {
23 const validOptions = validateOptions(result, ruleName, { actual });
28 const selectorContextLookup = nodeContextLookup();
30 root.walkRules(rule => {
31 // Ignore custom property set `--foo: {};`
32 if (isCustomPropertySet(rule)) {
36 const comparisonContext = selectorContextLookup.getContext(
38 findAtRuleContext(rule)
41 rule.selectors.forEach(selector => {
42 const trimSelector = selector.trim();
43 // Ignore `.selector, { }`
44 if (trimSelector === "") {
48 // The edge-case of duplicate selectors will act acceptably
49 const index = rule.selector.indexOf(trimSelector);
50 // Resolve any nested selectors before checking
51 resolvedNestedSelector(selector, rule).forEach(resolvedSelector => {
52 parseSelector(resolvedSelector, result, rule, s =>
53 checkSelector(s, rule, index, comparisonContext)
59 function checkSelector(selectorNode, rule, sourceIndex, comparisonContext) {
60 const selector = selectorNode.toString();
61 const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses(
64 const selectorSpecificity = specificity.calculate(selector)[0]
66 const entry = { selector, specificity: selectorSpecificity };
68 if (!comparisonContext.has(referenceSelectorNode)) {
69 comparisonContext.set(referenceSelectorNode, [entry]);
73 const priorComparableSelectors = comparisonContext.get(
77 priorComparableSelectors.forEach(priorEntry => {
79 specificity.compare(selectorSpecificity, priorEntry.specificity) ===
86 message: messages.rejected(selector, priorEntry.selector),
92 priorComparableSelectors.push(entry);
97 function lastCompoundSelectorWithoutPseudoClasses(selectorNode) {
98 const nodesAfterLastCombinator = _.last(
99 selectorNode.nodes[0].split(node => {
100 return node.type === "combinator";
104 const nodesWithoutPseudoClasses = nodesAfterLastCombinator
107 node.type !== "pseudo" ||
108 keywordSets.pseudoElements.has(node.value.replace(/:/g, ""))
113 return nodesWithoutPseudoClasses.toString();
116 rule.ruleName = ruleName;
117 rule.messages = messages;
118 module.exports = rule;