index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. 'use strict';
  2. const findAtRuleContext = require('../../utils/findAtRuleContext');
  3. const isKeyframeRule = require('../../utils/isKeyframeRule');
  4. const nodeContextLookup = require('../../utils/nodeContextLookup');
  5. const normalizeSelector = require('normalize-selector');
  6. const parseSelector = require('../../utils/parseSelector');
  7. const report = require('../../utils/report');
  8. const resolvedNestedSelector = require('postcss-resolve-nested-selector');
  9. const ruleMessages = require('../../utils/ruleMessages');
  10. const validateOptions = require('../../utils/validateOptions');
  11. const { isBoolean } = require('../../utils/validateTypes');
  12. const ruleName = 'no-duplicate-selectors';
  13. const messages = ruleMessages(ruleName, {
  14. rejected: (selector, firstDuplicateLine) =>
  15. `Unexpected duplicate selector "${selector}", first used at line ${firstDuplicateLine}`,
  16. });
  17. const meta = {
  18. url: 'https://stylelint.io/user-guide/rules/list/no-duplicate-selectors',
  19. };
  20. /** @type {import('stylelint').Rule} */
  21. const rule = (primary, secondaryOptions) => {
  22. return (root, result) => {
  23. const validOptions = validateOptions(
  24. result,
  25. ruleName,
  26. { actual: primary },
  27. {
  28. actual: secondaryOptions,
  29. possible: {
  30. disallowInList: [isBoolean],
  31. },
  32. optional: true,
  33. },
  34. );
  35. if (!validOptions) {
  36. return;
  37. }
  38. const shouldDisallowDuplicateInList = secondaryOptions && secondaryOptions.disallowInList;
  39. // The top level of this map will be rule sources.
  40. // Each source maps to another map, which maps rule parents to a set of selectors.
  41. // This ensures that selectors are only checked against selectors
  42. // from other rules that share the same parent and the same source.
  43. const selectorContextLookup = nodeContextLookup();
  44. root.walkRules((ruleNode) => {
  45. if (isKeyframeRule(ruleNode)) {
  46. return;
  47. }
  48. const contextSelectorSet = selectorContextLookup.getContext(
  49. ruleNode,
  50. findAtRuleContext(ruleNode),
  51. );
  52. const resolvedSelectorList = [
  53. ...new Set(
  54. ruleNode.selectors.flatMap((selector) => resolvedNestedSelector(selector, ruleNode)),
  55. ),
  56. ];
  57. const normalizedSelectorList = resolvedSelectorList.map((selector) =>
  58. normalizeSelector(selector),
  59. );
  60. // Sort the selectors list so that the order of the constituents
  61. // doesn't matter
  62. const sortedSelectorList = [...normalizedSelectorList].sort().join(',');
  63. if (!ruleNode.source) throw new Error('The rule node must have a source');
  64. if (!ruleNode.source.start) throw new Error('The rule source must have a start position');
  65. const selectorLine = ruleNode.source.start.line;
  66. // Complain if the same selector list occurs twice
  67. let previousDuplicatePosition;
  68. // When `disallowInList` is true, we must parse `sortedSelectorList` into
  69. // list items.
  70. /** @type {string[]} */
  71. const selectorListParsed = [];
  72. if (shouldDisallowDuplicateInList) {
  73. parseSelector(sortedSelectorList, result, ruleNode, (selectors) => {
  74. selectors.each((s) => {
  75. const selector = String(s);
  76. selectorListParsed.push(selector);
  77. if (contextSelectorSet.get(selector)) {
  78. previousDuplicatePosition = contextSelectorSet.get(selector);
  79. }
  80. });
  81. });
  82. } else {
  83. previousDuplicatePosition = contextSelectorSet.get(sortedSelectorList);
  84. }
  85. if (previousDuplicatePosition) {
  86. // If the selector isn't nested we can use its raw value; otherwise,
  87. // we have to approximate something for the message -- which is close enough
  88. const isNestedSelector = resolvedSelectorList.join(',') !== ruleNode.selectors.join(',');
  89. const selectorForMessage = isNestedSelector
  90. ? resolvedSelectorList.join(', ')
  91. : ruleNode.selector;
  92. return report({
  93. result,
  94. ruleName,
  95. node: ruleNode,
  96. message: messages.rejected(selectorForMessage, previousDuplicatePosition),
  97. });
  98. }
  99. const presentedSelectors = new Set();
  100. const reportedSelectors = new Set();
  101. // Or complain if one selector list contains the same selector more than once
  102. for (const selector of ruleNode.selectors) {
  103. const normalized = normalizeSelector(selector);
  104. if (presentedSelectors.has(normalized)) {
  105. if (reportedSelectors.has(normalized)) {
  106. continue;
  107. }
  108. report({
  109. result,
  110. ruleName,
  111. node: ruleNode,
  112. message: messages.rejected(selector, selectorLine),
  113. });
  114. reportedSelectors.add(normalized);
  115. } else {
  116. presentedSelectors.add(normalized);
  117. }
  118. }
  119. if (shouldDisallowDuplicateInList) {
  120. for (const selector of selectorListParsed) {
  121. // [selectorLine] will not really be accurate for multi-line
  122. // selectors, such as "bar" in "foo,\nbar {}".
  123. contextSelectorSet.set(selector, selectorLine);
  124. }
  125. } else {
  126. contextSelectorSet.set(sortedSelectorList, selectorLine);
  127. }
  128. });
  129. };
  130. };
  131. rule.ruleName = ruleName;
  132. rule.messages = messages;
  133. rule.meta = meta;
  134. module.exports = rule;