123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- '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}`,
- });
- const meta = {
- url: 'https://stylelint.io/user-guide/rules/list/no-duplicate-selectors',
- };
- /** @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;
- rule.meta = meta;
- module.exports = rule;
|