blob: 945e9b8583f7874eac20f54c7e2dc307528b7690 [file] [log] [blame]
"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;