index.js 4.4 KB

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