--- /dev/null
+"use strict";
+
+const _ = require("lodash");
+const findAtRuleContext = require("../../utils/findAtRuleContext");
+const isCustomPropertySet = require("../../utils/isCustomPropertySet");
+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;
+ }
+
+ 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;