index.js 5.2 KB

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