index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. 'use strict';
  2. const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
  3. const isCustomProperty = require('../../utils/isCustomProperty');
  4. const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
  5. const optionsMatches = require('../../utils/optionsMatches');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const { isString } = require('../../utils/validateTypes');
  10. const vendor = require('../../utils/vendor');
  11. const ruleName = 'declaration-block-no-duplicate-properties';
  12. const messages = ruleMessages(ruleName, {
  13. rejected: (property) => `Unexpected duplicate "${property}"`,
  14. });
  15. const meta = {
  16. url: 'https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties',
  17. fixable: true,
  18. };
  19. /** @type {import('stylelint').Rule} */
  20. const rule = (primary, secondaryOptions, context) => {
  21. return (root, result) => {
  22. const validOptions = validateOptions(
  23. result,
  24. ruleName,
  25. { actual: primary },
  26. {
  27. actual: secondaryOptions,
  28. possible: {
  29. ignore: [
  30. 'consecutive-duplicates',
  31. 'consecutive-duplicates-with-different-values',
  32. 'consecutive-duplicates-with-same-prefixless-values',
  33. ],
  34. ignoreProperties: [isString],
  35. },
  36. optional: true,
  37. },
  38. );
  39. if (!validOptions) {
  40. return;
  41. }
  42. const ignoreDuplicates = optionsMatches(secondaryOptions, 'ignore', 'consecutive-duplicates');
  43. const ignoreDiffValues = optionsMatches(
  44. secondaryOptions,
  45. 'ignore',
  46. 'consecutive-duplicates-with-different-values',
  47. );
  48. const ignorePrefixlessSameValues = optionsMatches(
  49. secondaryOptions,
  50. 'ignore',
  51. 'consecutive-duplicates-with-same-prefixless-values',
  52. );
  53. eachDeclarationBlock(root, (eachDecl) => {
  54. /** @type {import('postcss').Declaration[]} */
  55. const decls = [];
  56. eachDecl((decl) => {
  57. const prop = decl.prop;
  58. const lowerProp = decl.prop.toLowerCase();
  59. const value = decl.value;
  60. if (!isStandardSyntaxProperty(prop)) {
  61. return;
  62. }
  63. if (isCustomProperty(prop)) {
  64. return;
  65. }
  66. // Return early if the property is to be ignored
  67. if (optionsMatches(secondaryOptions, 'ignoreProperties', prop)) {
  68. return;
  69. }
  70. // Ignore the src property as commonly duplicated in at-fontface
  71. if (lowerProp === 'src') {
  72. return;
  73. }
  74. const indexDuplicate = decls.findIndex((d) => d.prop.toLowerCase() === lowerProp);
  75. if (indexDuplicate !== -1) {
  76. if (ignoreDiffValues || ignorePrefixlessSameValues) {
  77. // fails if duplicates are not consecutive
  78. if (indexDuplicate !== decls.length - 1) {
  79. if (context.fix) {
  80. removePreviousDuplicate(decls, lowerProp);
  81. return;
  82. }
  83. report({
  84. message: messages.rejected(prop),
  85. node: decl,
  86. result,
  87. ruleName,
  88. word: prop,
  89. });
  90. return;
  91. }
  92. const duplicateDecl = decls[indexDuplicate];
  93. const duplicateValue = duplicateDecl ? duplicateDecl.value : '';
  94. if (ignorePrefixlessSameValues) {
  95. // fails if values of consecutive, unprefixed duplicates are equal
  96. if (vendor.unprefixed(value) !== vendor.unprefixed(duplicateValue)) {
  97. if (context.fix) {
  98. removePreviousDuplicate(decls, lowerProp);
  99. return;
  100. }
  101. report({
  102. message: messages.rejected(prop),
  103. node: decl,
  104. result,
  105. ruleName,
  106. word: prop,
  107. });
  108. return;
  109. }
  110. }
  111. // fails if values of consecutive duplicates are equal
  112. if (value === duplicateValue) {
  113. if (context.fix) {
  114. removePreviousDuplicate(decls, lowerProp);
  115. return;
  116. }
  117. report({
  118. message: messages.rejected(prop),
  119. node: decl,
  120. result,
  121. ruleName,
  122. word: prop,
  123. });
  124. return;
  125. }
  126. return;
  127. }
  128. if (ignoreDuplicates && indexDuplicate === decls.length - 1) {
  129. return;
  130. }
  131. if (context.fix) {
  132. removePreviousDuplicate(decls, lowerProp);
  133. return;
  134. }
  135. report({
  136. message: messages.rejected(prop),
  137. node: decl,
  138. result,
  139. ruleName,
  140. word: prop,
  141. });
  142. }
  143. decls.push(decl);
  144. });
  145. });
  146. };
  147. };
  148. /**
  149. * @param {import('postcss').Declaration[]} declarations
  150. * @param {string} lowerProperty
  151. * @returns {void}
  152. * */
  153. function removePreviousDuplicate(declarations, lowerProperty) {
  154. const declToRemove = declarations.find((d) => d.prop.toLowerCase() === lowerProperty);
  155. if (declToRemove) declToRemove.remove();
  156. }
  157. rule.ruleName = ruleName;
  158. rule.messages = messages;
  159. rule.meta = meta;
  160. module.exports = rule;