index.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. // @ts-nocheck
  2. 'use strict';
  3. const _ = require('lodash');
  4. const findAtRuleContext = require('../../utils/findAtRuleContext');
  5. const isKeyframeRule = require('../../utils/isKeyframeRule');
  6. const nodeContextLookup = require('../../utils/nodeContextLookup');
  7. const normalizeSelector = require('normalize-selector');
  8. const parseSelector = require('../../utils/parseSelector');
  9. const report = require('../../utils/report');
  10. const resolvedNestedSelector = require('postcss-resolve-nested-selector');
  11. const ruleMessages = require('../../utils/ruleMessages');
  12. const validateOptions = require('../../utils/validateOptions');
  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. function rule(actual, options) {
  19. return (root, result) => {
  20. const validOptions = validateOptions(
  21. result,
  22. ruleName,
  23. { actual },
  24. {
  25. actual: options,
  26. possible: {
  27. disallowInList: _.isBoolean,
  28. },
  29. optional: true,
  30. },
  31. );
  32. if (!validOptions) {
  33. return;
  34. }
  35. const shouldDisallowDuplicateInList = _.get(options, 'disallowInList');
  36. // The top level of this map will be rule sources.
  37. // Each source maps to another map, which maps rule parents to a set of selectors.
  38. // This ensures that selectors are only checked against selectors
  39. // from other rules that share the same parent and the same source.
  40. const selectorContextLookup = nodeContextLookup();
  41. root.walkRules((rule) => {
  42. if (isKeyframeRule(rule)) {
  43. return;
  44. }
  45. const contextSelectorSet = selectorContextLookup.getContext(rule, findAtRuleContext(rule));
  46. const resolvedSelectors = rule.selectors.reduce((result, selector) => {
  47. return _.union(result, resolvedNestedSelector(selector, rule));
  48. }, []);
  49. const normalizedSelectorList = resolvedSelectors.map(normalizeSelector);
  50. // Sort the selectors list so that the order of the constituents
  51. // doesn't matter
  52. const sortedSelectorList = normalizedSelectorList.slice().sort().join(',');
  53. const selectorLine = rule.source.start.line;
  54. // Complain if the same selector list occurs twice
  55. let previousDuplicatePosition;
  56. // When `disallowInList` is true, we must parse `sortedSelectorList` into
  57. // list items.
  58. let selectorListParsed = [];
  59. if (shouldDisallowDuplicateInList) {
  60. parseSelector(sortedSelectorList, result, rule, (selectors) => {
  61. selectors.each((s) => {
  62. const selector = String(s);
  63. selectorListParsed.push(selector);
  64. if (contextSelectorSet.get(selector)) {
  65. previousDuplicatePosition = contextSelectorSet.get(selector);
  66. }
  67. });
  68. });
  69. } else {
  70. previousDuplicatePosition = contextSelectorSet.get(sortedSelectorList);
  71. }
  72. if (previousDuplicatePosition) {
  73. // If the selector isn't nested we can use its raw value; otherwise,
  74. // we have to approximate something for the message -- which is close enough
  75. const isNestedSelector = resolvedSelectors.join(',') !== rule.selectors.join(',');
  76. const selectorForMessage = isNestedSelector ? resolvedSelectors.join(', ') : rule.selector;
  77. return report({
  78. result,
  79. ruleName,
  80. node: rule,
  81. message: messages.rejected(selectorForMessage, previousDuplicatePosition),
  82. });
  83. }
  84. const presentedSelectors = new Set();
  85. const reportedSelectors = new Set();
  86. // Or complain if one selector list contains the same selector more than once
  87. rule.selectors.forEach((selector) => {
  88. const normalized = normalizeSelector(selector);
  89. if (presentedSelectors.has(normalized)) {
  90. if (reportedSelectors.has(normalized)) {
  91. return;
  92. }
  93. report({
  94. result,
  95. ruleName,
  96. node: rule,
  97. message: messages.rejected(selector, selectorLine),
  98. });
  99. reportedSelectors.add(normalized);
  100. } else {
  101. presentedSelectors.add(normalized);
  102. }
  103. });
  104. if (shouldDisallowDuplicateInList) {
  105. for (let selector of selectorListParsed) {
  106. // [selectorLine] will not really be accurate for multi-line
  107. // selectors, such as "bar" in "foo,\nbar {}".
  108. contextSelectorSet.set(selector, selectorLine);
  109. }
  110. } else {
  111. contextSelectorSet.set(sortedSelectorList, selectorLine);
  112. }
  113. });
  114. };
  115. }
  116. rule.ruleName = ruleName;
  117. rule.messages = messages;
  118. module.exports = rule;