"use strict"; const _ = require("lodash"); const findAtRuleContext = require("../../utils/findAtRuleContext"); const isCustomPropertySet = require("../../utils/isCustomPropertySet"); const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule"); const isStandardSyntaxSelector = require("../../utils/isStandardSyntaxSelector"); const keywordSets = require("../../reference/keywordSets"); const nodeContextLookup = require("../../utils/nodeContextLookup"); const parseSelector = require("../../utils/parseSelector"); const report = require("../../utils/report"); const resolvedNestedSelector = require("postcss-resolve-nested-selector"); const ruleMessages = require("../../utils/ruleMessages"); const specificity = require("specificity"); const validateOptions = require("../../utils/validateOptions"); const ruleName = "no-descending-specificity"; const messages = ruleMessages(ruleName, { rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"` }); const rule = function(actual) { return (root, result) => { const validOptions = validateOptions(result, ruleName, { actual }); if (!validOptions) { return; } const selectorContextLookup = nodeContextLookup(); root.walkRules(rule => { // Ignore custom property set `--foo: {};` if (isCustomPropertySet(rule)) { return; } // Ignore nested property `foo: {};` if (!isStandardSyntaxRule(rule)) { return; } const comparisonContext = selectorContextLookup.getContext( rule, findAtRuleContext(rule) ); rule.selectors.forEach(selector => { const trimSelector = selector.trim(); // Ignore `.selector, { }` if (trimSelector === "") { return; } // The edge-case of duplicate selectors will act acceptably const index = rule.selector.indexOf(trimSelector); // Resolve any nested selectors before checking resolvedNestedSelector(selector, rule).forEach(resolvedSelector => { parseSelector(resolvedSelector, result, rule, s => { if (!isStandardSyntaxSelector(resolvedSelector)) { return; } checkSelector(s, rule, index, comparisonContext); }); }); }); }); function checkSelector(selectorNode, rule, sourceIndex, comparisonContext) { const selector = selectorNode.toString(); const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses( selectorNode ); const selectorSpecificity = specificity.calculate(selector)[0] .specificityArray; const entry = { selector, specificity: selectorSpecificity }; if (!comparisonContext.has(referenceSelectorNode)) { comparisonContext.set(referenceSelectorNode, [entry]); return; } const priorComparableSelectors = comparisonContext.get( referenceSelectorNode ); priorComparableSelectors.forEach(priorEntry => { if ( specificity.compare(selectorSpecificity, priorEntry.specificity) === -1 ) { report({ ruleName, result, node: rule, message: messages.rejected(selector, priorEntry.selector), index: sourceIndex }); } }); priorComparableSelectors.push(entry); } }; }; function lastCompoundSelectorWithoutPseudoClasses(selectorNode) { const nodesAfterLastCombinator = _.last( selectorNode.nodes[0].split(node => { return node.type === "combinator"; }) ); const nodesWithoutPseudoClasses = nodesAfterLastCombinator .filter(node => { return ( node.type !== "pseudo" || keywordSets.pseudoElements.has(node.value.replace(/:/g, "")) ); }) .join(""); return nodesWithoutPseudoClasses.toString(); } rule.ruleName = ruleName; rule.messages = messages; module.exports = rule;