index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. 'use strict';
  2. const arrayEqual = require('../../utils/arrayEqual');
  3. const { basicKeywords } = require('../../reference/keywords');
  4. const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
  5. const optionsMatches = require('../../utils/optionsMatches');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const { longhandSubPropertiesOfShorthandProperties } = require('../../reference/properties');
  9. const validateOptions = require('../../utils/validateOptions');
  10. const vendor = require('../../utils/vendor');
  11. const { isRegExp, isString } = require('../../utils/validateTypes');
  12. const ruleName = 'declaration-block-no-redundant-longhand-properties';
  13. const messages = ruleMessages(ruleName, {
  14. expected: (props) => `Expected shorthand property "${props}"`,
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/declaration-block-no-redundant-longhand-properties',
  18. fixable: true,
  19. };
  20. /** @typedef {import('postcss').Declaration} Declaration */
  21. /** @type {import('stylelint').Rule} */
  22. const rule = (primary, secondaryOptions, context) => {
  23. return (root, result) => {
  24. const validOptions = validateOptions(
  25. result,
  26. ruleName,
  27. { actual: primary },
  28. {
  29. actual: secondaryOptions,
  30. possible: {
  31. ignoreShorthands: [isString, isRegExp],
  32. },
  33. optional: true,
  34. },
  35. );
  36. if (!validOptions) {
  37. return;
  38. }
  39. /** @type {Map<string, import('stylelint').ShorthandProperties[]>} */
  40. const longhandToShorthands = new Map();
  41. for (const [shorthand, longhandProps] of longhandSubPropertiesOfShorthandProperties.entries()) {
  42. if (optionsMatches(secondaryOptions, 'ignoreShorthands', shorthand)) {
  43. continue;
  44. }
  45. for (const longhand of longhandProps) {
  46. const shorthands = longhandToShorthands.get(longhand) || [];
  47. shorthands.push(shorthand);
  48. longhandToShorthands.set(longhand, shorthands);
  49. }
  50. }
  51. eachDeclarationBlock(root, (eachDecl) => {
  52. /** @type {Map<string, string[]>} */
  53. const longhandDeclarations = new Map();
  54. /** @type {Map<string, Declaration[]>} */
  55. const longhandDeclarationNodes = new Map();
  56. eachDecl((decl) => {
  57. // basic keywords are not allowed in shorthand properties
  58. if (basicKeywords.has(decl.value)) {
  59. return;
  60. }
  61. const prop = decl.prop.toLowerCase();
  62. const unprefixedProp = vendor.unprefixed(prop);
  63. const prefix = vendor.prefix(prop);
  64. const shorthandProperties = longhandToShorthands.get(unprefixedProp);
  65. if (!shorthandProperties) {
  66. return;
  67. }
  68. for (const shorthandProperty of shorthandProperties) {
  69. const prefixedShorthandProperty = prefix + shorthandProperty;
  70. const longhandDeclaration = longhandDeclarations.get(prefixedShorthandProperty) || [];
  71. const longhandDeclarationNode =
  72. longhandDeclarationNodes.get(prefixedShorthandProperty) || [];
  73. longhandDeclaration.push(prop);
  74. longhandDeclarations.set(prefixedShorthandProperty, longhandDeclaration);
  75. longhandDeclarationNode.push(decl);
  76. longhandDeclarationNodes.set(prefixedShorthandProperty, longhandDeclarationNode);
  77. const shorthandProps = longhandSubPropertiesOfShorthandProperties.get(shorthandProperty);
  78. const prefixedShorthandData = Array.from(shorthandProps || []).map(
  79. (item) => prefix + item,
  80. );
  81. const copiedPrefixedShorthandData = [...prefixedShorthandData];
  82. if (!arrayEqual(copiedPrefixedShorthandData.sort(), longhandDeclaration.sort())) {
  83. continue;
  84. }
  85. if (context.fix) {
  86. const declNodes = longhandDeclarationNodes.get(prefixedShorthandProperty) || [];
  87. const [firstDeclNode] = declNodes;
  88. if (firstDeclNode) {
  89. const transformedDeclarationNodes = new Map(
  90. declNodes.map((d) => [d.prop.toLowerCase(), d]),
  91. );
  92. const resolvedShorthandValue = prefixedShorthandData
  93. .map((p) => transformedDeclarationNodes.get(p)?.value.trim())
  94. .filter(Boolean)
  95. .join(' ');
  96. const newShorthandDeclarationNode = firstDeclNode.clone({
  97. prop: prefixedShorthandProperty,
  98. value: resolvedShorthandValue,
  99. });
  100. firstDeclNode.replaceWith(newShorthandDeclarationNode);
  101. declNodes.forEach((node) => node.remove());
  102. return;
  103. }
  104. }
  105. report({
  106. ruleName,
  107. result,
  108. node: decl,
  109. word: decl.prop,
  110. message: messages.expected,
  111. messageArgs: [prefixedShorthandProperty],
  112. });
  113. }
  114. });
  115. });
  116. };
  117. };
  118. rule.ruleName = ruleName;
  119. rule.messages = messages;
  120. rule.meta = meta;
  121. module.exports = rule;