141 lines
3.9 KiB
JavaScript
141 lines
3.9 KiB
JavaScript
// @ts-nocheck
|
|
|
|
'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 optionsMatches = require('../../utils/optionsMatches');
|
|
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}"`,
|
|
});
|
|
|
|
function rule(on, options) {
|
|
return (root, result) => {
|
|
const validOptions = validateOptions(
|
|
result,
|
|
ruleName,
|
|
{
|
|
actual: on,
|
|
},
|
|
{
|
|
optional: true,
|
|
actual: options,
|
|
possible: {
|
|
ignore: ['selectors-within-list'],
|
|
},
|
|
},
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
// Ignores selectors within list of selectors
|
|
if (optionsMatches(options, 'ignore', 'selectors-within-list') && rule.selectors.length > 1) {
|
|
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;
|