index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const getDimension = require('../../utils/getDimension');
  5. const mediaParser = require('postcss-media-query-parser').default;
  6. const optionsMatches = require('../../utils/optionsMatches');
  7. const report = require('../../utils/report');
  8. const ruleMessages = require('../../utils/ruleMessages');
  9. const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps');
  10. const validateOptions = require('../../utils/validateOptions');
  11. const valueParser = require('postcss-value-parser');
  12. const { isRegExp, isString } = require('../../utils/validateTypes');
  13. const ruleName = 'unit-disallowed-list';
  14. const messages = ruleMessages(ruleName, {
  15. rejected: (unit) => `Unexpected unit "${unit}"`,
  16. });
  17. const meta = {
  18. url: 'https://stylelint.io/user-guide/rules/list/unit-disallowed-list',
  19. };
  20. /**
  21. * a function to retrieve only the media feature name
  22. * could be externalized in an utils function if needed in other code
  23. *
  24. * @param {import('postcss-media-query-parser').Child} mediaFeatureNode
  25. * @returns {string | undefined}
  26. */
  27. const getMediaFeatureName = (mediaFeatureNode) => {
  28. const value = mediaFeatureNode.value.toLowerCase();
  29. const match = /((?:-?\w*)*)/.exec(value);
  30. return match ? match[1] : undefined;
  31. };
  32. /** @type {import('stylelint').Rule<string | string[]>} */
  33. const rule = (primary, secondaryOptions) => {
  34. return (root, result) => {
  35. const validOptions = validateOptions(
  36. result,
  37. ruleName,
  38. {
  39. actual: primary,
  40. possible: [isString],
  41. },
  42. {
  43. optional: true,
  44. actual: secondaryOptions,
  45. possible: {
  46. ignoreProperties: [validateObjectWithArrayProps(isString, isRegExp)],
  47. ignoreMediaFeatureNames: [validateObjectWithArrayProps(isString, isRegExp)],
  48. },
  49. },
  50. );
  51. if (!validOptions) {
  52. return;
  53. }
  54. const primaryValues = [primary].flat();
  55. /**
  56. * @param {import('postcss').Node} node
  57. * @param {number} nodeIndex
  58. * @param {import('postcss-value-parser').Node} valueNode
  59. * @param {string | undefined} input
  60. * @param {Record<string, unknown>} options
  61. * @returns {void}
  62. */
  63. function check(node, nodeIndex, valueNode, input, options) {
  64. const { number, unit } = getDimension(valueNode);
  65. // There is not unit or it is not configured as a problem
  66. if (!unit || !number || (unit && !primaryValues.includes(unit.toLowerCase()))) {
  67. return;
  68. }
  69. // The unit has an ignore option for the specific input
  70. if (optionsMatches(options, unit.toLowerCase(), input)) {
  71. return;
  72. }
  73. report({
  74. index: nodeIndex + valueNode.sourceIndex + number.length,
  75. endIndex: nodeIndex + valueNode.sourceEndIndex,
  76. message: messages.rejected(unit),
  77. node,
  78. result,
  79. ruleName,
  80. });
  81. }
  82. /**
  83. * @template {import('postcss').AtRule} T
  84. * @param {T} node
  85. * @param {string} value
  86. * @param {(node: T) => number} getIndex
  87. * @returns {void}
  88. */
  89. function checkMedia(node, value, getIndex) {
  90. mediaParser(node.params).walk(/^media-feature$/i, (mediaFeatureNode) => {
  91. const mediaName = getMediaFeatureName(mediaFeatureNode);
  92. const parentValue = mediaFeatureNode.parent.value;
  93. valueParser(value).walk((valueNode) => {
  94. // Ignore all non-word valueNode and
  95. // the values not included in the parentValue string
  96. if (valueNode.type !== 'word' || !parentValue.includes(valueNode.value)) {
  97. return;
  98. }
  99. check(
  100. node,
  101. getIndex(node),
  102. valueNode,
  103. mediaName,
  104. secondaryOptions ? secondaryOptions.ignoreMediaFeatureNames : {},
  105. );
  106. });
  107. });
  108. }
  109. /**
  110. * @template {import('postcss').Declaration} T
  111. * @param {T} node
  112. * @param {string} value
  113. * @param {(node: T) => number} getIndex
  114. * @returns {void}
  115. */
  116. function checkDecl(node, value, getIndex) {
  117. // make sure multiplication operations (*) are divided - not handled
  118. // by postcss-value-parser
  119. value = value.replace(/\*/g, ',');
  120. valueParser(value).walk((valueNode) => {
  121. // Ignore wrong units within `url` function
  122. if (valueNode.type === 'function' && valueNode.value.toLowerCase() === 'url') {
  123. return false;
  124. }
  125. check(
  126. node,
  127. getIndex(node),
  128. valueNode,
  129. node.prop,
  130. secondaryOptions ? secondaryOptions.ignoreProperties : {},
  131. );
  132. });
  133. }
  134. root.walkAtRules(/^media$/i, (atRule) => checkMedia(atRule, atRule.params, atRuleParamIndex));
  135. root.walkDecls((decl) => checkDecl(decl, decl.value, declarationValueIndex));
  136. };
  137. };
  138. rule.primaryOptionArray = true;
  139. rule.ruleName = ruleName;
  140. rule.messages = messages;
  141. rule.meta = meta;
  142. module.exports = rule;