index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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/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. ignoreFunctions: [isString, isRegExp],
  47. ignoreProperties: [validateObjectWithArrayProps(isString, isRegExp)],
  48. ignoreMediaFeatureNames: [validateObjectWithArrayProps(isString, isRegExp)],
  49. },
  50. },
  51. );
  52. if (!validOptions) {
  53. return;
  54. }
  55. const primaryValues = [primary].flat();
  56. /**
  57. * @param {import('postcss').Node} node
  58. * @param {number} nodeIndex
  59. * @param {import('postcss-value-parser').Node} valueNode
  60. * @param {string | undefined} input
  61. * @param {Record<string, unknown>} options
  62. * @returns {void}
  63. */
  64. function check(node, nodeIndex, valueNode, input, options) {
  65. const { number, unit } = getDimension(valueNode);
  66. // There is not unit or it is not configured as a problem
  67. if (!unit || !number || (unit && !primaryValues.includes(unit.toLowerCase()))) {
  68. return;
  69. }
  70. // The unit has an ignore option for the specific input
  71. if (optionsMatches(options, unit.toLowerCase(), input)) {
  72. return;
  73. }
  74. report({
  75. index: nodeIndex + valueNode.sourceIndex + number.length,
  76. endIndex: nodeIndex + valueNode.sourceEndIndex,
  77. message: messages.rejected,
  78. messageArgs: [unit],
  79. node,
  80. result,
  81. ruleName,
  82. });
  83. }
  84. /**
  85. * @template {import('postcss').AtRule} T
  86. * @param {T} node
  87. * @param {string} value
  88. * @param {(node: T) => number} getIndex
  89. * @returns {void}
  90. */
  91. function checkMedia(node, value, getIndex) {
  92. mediaParser(node.params).walk(/^media-feature$/i, (mediaFeatureNode) => {
  93. const mediaName = getMediaFeatureName(mediaFeatureNode);
  94. const parentValue = mediaFeatureNode.parent.value;
  95. valueParser(value).walk((valueNode) => {
  96. // Ignore all non-word valueNode and
  97. // the values not included in the parentValue string
  98. if (valueNode.type !== 'word' || !parentValue.includes(valueNode.value)) {
  99. return;
  100. }
  101. check(
  102. node,
  103. getIndex(node),
  104. valueNode,
  105. mediaName,
  106. secondaryOptions ? secondaryOptions.ignoreMediaFeatureNames : {},
  107. );
  108. });
  109. });
  110. }
  111. /**
  112. * @template {import('postcss').Declaration} T
  113. * @param {T} node
  114. * @param {string} value
  115. * @param {(node: T) => number} getIndex
  116. * @returns {void}
  117. */
  118. function checkDecl(node, value, getIndex) {
  119. // make sure multiplication operations (*) are divided - not handled
  120. // by postcss-value-parser
  121. value = value.replace(/\*/g, ',');
  122. valueParser(value).walk((valueNode) => {
  123. const valueLowerCase = valueNode.value.toLowerCase();
  124. // Ignore wrong units within `url` function
  125. if (
  126. valueNode.type === 'function' &&
  127. (valueLowerCase === 'url' ||
  128. optionsMatches(secondaryOptions, 'ignoreFunctions', valueLowerCase))
  129. ) {
  130. return false;
  131. }
  132. check(
  133. node,
  134. getIndex(node),
  135. valueNode,
  136. node.prop,
  137. secondaryOptions ? secondaryOptions.ignoreProperties : {},
  138. );
  139. });
  140. }
  141. root.walkAtRules(/^media$/i, (atRule) => checkMedia(atRule, atRule.params, atRuleParamIndex));
  142. root.walkDecls((decl) => checkDecl(decl, decl.value, declarationValueIndex));
  143. };
  144. };
  145. rule.primaryOptionArray = true;
  146. rule.ruleName = ruleName;
  147. rule.messages = messages;
  148. rule.meta = meta;
  149. module.exports = rule;