"use strict" const specificity = require("specificity") const findAtRuleContext = require("../../utils/findAtRuleContext") const isCustomPropertySet = require("../../utils/isCustomPropertySet") const nodeContextLookup = require("../../utils/nodeContextLookup") const parseSelector = require("../../utils/parseSelector") const report = require("../../utils/report") const ruleMessages = require("../../utils/ruleMessages") const validateOptions = require("../../utils/validateOptions") const _ = require("lodash") const keywordSets = require("../../reference/keywordSets") const resolvedNestedSelector = require("postcss-resolve-nested-selector") 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 } 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 => 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