index.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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/list/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(selectorForMessage, previousDuplicatePosition),
  96. word: selectorForMessage,
  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 = normalize(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. word: selector,
  114. });
  115. reportedSelectors.add(normalized);
  116. } else {
  117. presentedSelectors.add(normalized);
  118. }
  119. }
  120. if (shouldDisallowDuplicateInList) {
  121. for (const selector of selectorListParsed) {
  122. // [selectorLine] will not really be accurate for multi-line
  123. // selectors, such as "bar" in "foo,\nbar {}".
  124. contextSelectorSet.set(selector, selectorLine);
  125. }
  126. } else {
  127. contextSelectorSet.set(sortedSelectorList, selectorLine);
  128. }
  129. });
  130. };
  131. };
  132. /**
  133. * @param {string} selector
  134. * @returns {string}
  135. */
  136. function normalize(selector) {
  137. if (!isStandardSyntaxSelector(selector)) {
  138. return selector;
  139. }
  140. return selectorParser().processSync(selector, { lossless: false });
  141. }
  142. rule.ruleName = ruleName;
  143. rule.messages = messages;
  144. rule.meta = meta;
  145. module.exports = rule;