blob: 78a117e0909d5a61c668db793a6e8910a90ff57a [file] [log] [blame]
'use strict';
const findAtRuleContext = require('../../utils/findAtRuleContext');
const isKeyframeRule = require('../../utils/isKeyframeRule');
const nodeContextLookup = require('../../utils/nodeContextLookup');
const normalizeSelector = require('normalize-selector');
const parseSelector = require('../../utils/parseSelector');
const report = require('../../utils/report');
const resolvedNestedSelector = require('postcss-resolve-nested-selector');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isBoolean } = require('../../utils/validateTypes');
const ruleName = 'no-duplicate-selectors';
const messages = ruleMessages(ruleName, {
rejected: (selector, firstDuplicateLine) =>
`Unexpected duplicate selector "${selector}", first used at line ${firstDuplicateLine}`,
});
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
disallowInList: [isBoolean],
},
optional: true,
},
);
if (!validOptions) {
return;
}
const shouldDisallowDuplicateInList = secondaryOptions && secondaryOptions.disallowInList;
// The top level of this map will be rule sources.
// Each source maps to another map, which maps rule parents to a set of selectors.
// This ensures that selectors are only checked against selectors
// from other rules that share the same parent and the same source.
const selectorContextLookup = nodeContextLookup();
root.walkRules((ruleNode) => {
if (isKeyframeRule(ruleNode)) {
return;
}
const contextSelectorSet = selectorContextLookup.getContext(
ruleNode,
findAtRuleContext(ruleNode),
);
const resolvedSelectorList = [
...new Set(
ruleNode.selectors.flatMap((selector) => resolvedNestedSelector(selector, ruleNode)),
),
];
const normalizedSelectorList = resolvedSelectorList.map((selector) =>
normalizeSelector(selector),
);
// Sort the selectors list so that the order of the constituents
// doesn't matter
const sortedSelectorList = [...normalizedSelectorList].sort().join(',');
if (!ruleNode.source) throw new Error('The rule node must have a source');
if (!ruleNode.source.start) throw new Error('The rule source must have a start position');
const selectorLine = ruleNode.source.start.line;
// Complain if the same selector list occurs twice
let previousDuplicatePosition;
// When `disallowInList` is true, we must parse `sortedSelectorList` into
// list items.
/** @type {string[]} */
const selectorListParsed = [];
if (shouldDisallowDuplicateInList) {
parseSelector(sortedSelectorList, result, ruleNode, (selectors) => {
selectors.each((s) => {
const selector = String(s);
selectorListParsed.push(selector);
if (contextSelectorSet.get(selector)) {
previousDuplicatePosition = contextSelectorSet.get(selector);
}
});
});
} else {
previousDuplicatePosition = contextSelectorSet.get(sortedSelectorList);
}
if (previousDuplicatePosition) {
// If the selector isn't nested we can use its raw value; otherwise,
// we have to approximate something for the message -- which is close enough
const isNestedSelector = resolvedSelectorList.join(',') !== ruleNode.selectors.join(',');
const selectorForMessage = isNestedSelector
? resolvedSelectorList.join(', ')
: ruleNode.selector;
return report({
result,
ruleName,
node: ruleNode,
message: messages.rejected(selectorForMessage, previousDuplicatePosition),
});
}
const presentedSelectors = new Set();
const reportedSelectors = new Set();
// Or complain if one selector list contains the same selector more than once
for (const selector of ruleNode.selectors) {
const normalized = normalizeSelector(selector);
if (presentedSelectors.has(normalized)) {
if (reportedSelectors.has(normalized)) {
continue;
}
report({
result,
ruleName,
node: ruleNode,
message: messages.rejected(selector, selectorLine),
});
reportedSelectors.add(normalized);
} else {
presentedSelectors.add(normalized);
}
}
if (shouldDisallowDuplicateInList) {
for (const selector of selectorListParsed) {
// [selectorLine] will not really be accurate for multi-line
// selectors, such as "bar" in "foo,\nbar {}".
contextSelectorSet.set(selector, selectorLine);
}
} else {
contextSelectorSet.set(sortedSelectorList, selectorLine);
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;