index.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. 'use strict';
  2. const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration');
  3. const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
  4. const report = require('../../utils/report');
  5. const ruleMessages = require('../../utils/ruleMessages');
  6. const validateOptions = require('../../utils/validateOptions');
  7. const valueParser = require('postcss-value-parser');
  8. const vendor = require('../../utils/vendor');
  9. const ruleName = 'shorthand-property-no-redundant-values';
  10. const messages = ruleMessages(ruleName, {
  11. rejected: (unexpected, expected) =>
  12. `Unexpected longhand value '${unexpected}' instead of '${expected}'`,
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/list/shorthand-property-no-redundant-values',
  16. };
  17. const propertiesWithShorthandNotation = new Set([
  18. 'margin',
  19. 'padding',
  20. 'border-color',
  21. 'border-radius',
  22. 'border-style',
  23. 'border-width',
  24. 'grid-gap',
  25. ]);
  26. const ignoredCharacters = ['+', '*', '/', '(', ')', '$', '@', '--', 'var('];
  27. /**
  28. * @param {string} value
  29. * @returns {boolean}
  30. */
  31. function hasIgnoredCharacters(value) {
  32. return ignoredCharacters.some((char) => value.includes(char));
  33. }
  34. /**
  35. * @param {string} property
  36. * @returns {boolean}
  37. */
  38. function isShorthandProperty(property) {
  39. return propertiesWithShorthandNotation.has(property);
  40. }
  41. /**
  42. * @param {string} top
  43. * @param {string} right
  44. * @param {string} bottom
  45. * @param {string} left
  46. * @returns {string[]}
  47. */
  48. function canCondense(top, right, bottom, left) {
  49. const lowerTop = top.toLowerCase();
  50. const lowerRight = right.toLowerCase();
  51. const lowerBottom = bottom && bottom.toLowerCase();
  52. const lowerLeft = left && left.toLowerCase();
  53. if (canCondenseToOneValue(lowerTop, lowerRight, lowerBottom, lowerLeft)) {
  54. return [top];
  55. }
  56. if (canCondenseToTwoValues(lowerTop, lowerRight, lowerBottom, lowerLeft)) {
  57. return [top, right];
  58. }
  59. if (canCondenseToThreeValues(lowerTop, lowerRight, lowerBottom, lowerLeft)) {
  60. return [top, right, bottom];
  61. }
  62. return [top, right, bottom, left];
  63. }
  64. /**
  65. * @param {string} top
  66. * @param {string} right
  67. * @param {string} bottom
  68. * @param {string} left
  69. * @returns {boolean}
  70. */
  71. function canCondenseToOneValue(top, right, bottom, left) {
  72. if (top !== right) {
  73. return false;
  74. }
  75. return (top === bottom && (bottom === left || !left)) || (!bottom && !left);
  76. }
  77. /**
  78. * @param {string} top
  79. * @param {string} right
  80. * @param {string} bottom
  81. * @param {string} left
  82. * @returns {boolean}
  83. */
  84. function canCondenseToTwoValues(top, right, bottom, left) {
  85. return (top === bottom && right === left) || (top === bottom && !left && top !== right);
  86. }
  87. /**
  88. * @param {string} _top
  89. * @param {string} right
  90. * @param {string} _bottom
  91. * @param {string} left
  92. * @returns {boolean}
  93. */
  94. function canCondenseToThreeValues(_top, right, _bottom, left) {
  95. return right === left;
  96. }
  97. /** @type {import('stylelint').Rule} */
  98. const rule = (primary, _secondaryOptions, context) => {
  99. return (root, result) => {
  100. const validOptions = validateOptions(result, ruleName, { actual: primary });
  101. if (!validOptions) {
  102. return;
  103. }
  104. root.walkDecls((decl) => {
  105. if (!isStandardSyntaxDeclaration(decl) || !isStandardSyntaxProperty(decl.prop)) {
  106. return;
  107. }
  108. const prop = decl.prop;
  109. const value = decl.value;
  110. const normalizedProp = vendor.unprefixed(prop.toLowerCase());
  111. if (hasIgnoredCharacters(value) || !isShorthandProperty(normalizedProp)) {
  112. return;
  113. }
  114. /** @type {string[]} */
  115. const valuesToShorthand = [];
  116. valueParser(value).walk((valueNode) => {
  117. if (valueNode.type !== 'word') {
  118. return;
  119. }
  120. valuesToShorthand.push(valueParser.stringify(valueNode));
  121. });
  122. if (valuesToShorthand.length <= 1 || valuesToShorthand.length > 4) {
  123. return;
  124. }
  125. const shortestForm = canCondense(
  126. valuesToShorthand[0],
  127. valuesToShorthand[1],
  128. valuesToShorthand[2],
  129. valuesToShorthand[3],
  130. );
  131. const shortestFormString = shortestForm.filter(Boolean).join(' ');
  132. const valuesFormString = valuesToShorthand.join(' ');
  133. if (shortestFormString.toLowerCase() === valuesFormString.toLowerCase()) {
  134. return;
  135. }
  136. if (context.fix) {
  137. decl.value = decl.value.replace(value, shortestFormString);
  138. } else {
  139. report({
  140. message: messages.rejected(value, shortestFormString),
  141. node: decl,
  142. result,
  143. ruleName,
  144. });
  145. }
  146. });
  147. };
  148. };
  149. rule.ruleName = ruleName;
  150. rule.messages = messages;
  151. rule.meta = meta;
  152. module.exports = rule;