index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const getUnitFromValueNode = require('../../utils/getUnitFromValueNode');
  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 unit = getUnitFromValueNode(valueNode);
  65. // There is not unit or it is not configured as a problem
  66. if (!unit || (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,
  75. message: messages.rejected(unit),
  76. node,
  77. result,
  78. ruleName,
  79. });
  80. }
  81. /**
  82. * @template {import('postcss').AtRule} T
  83. * @param {T} node
  84. * @param {string} value
  85. * @param {(node: T) => number} getIndex
  86. * @returns {void}
  87. */
  88. function checkMedia(node, value, getIndex) {
  89. mediaParser(node.params).walk(/^media-feature$/i, (mediaFeatureNode) => {
  90. const mediaName = getMediaFeatureName(mediaFeatureNode);
  91. const parentValue = mediaFeatureNode.parent.value;
  92. valueParser(value).walk((valueNode) => {
  93. // Ignore all non-word valueNode and
  94. // the values not included in the parentValue string
  95. if (valueNode.type !== 'word' || !parentValue.includes(valueNode.value)) {
  96. return;
  97. }
  98. check(
  99. node,
  100. getIndex(node),
  101. valueNode,
  102. mediaName,
  103. secondaryOptions ? secondaryOptions.ignoreMediaFeatureNames : {},
  104. );
  105. });
  106. });
  107. }
  108. /**
  109. * @template {import('postcss').Declaration} T
  110. * @param {T} node
  111. * @param {string} value
  112. * @param {(node: T) => number} getIndex
  113. * @returns {void}
  114. */
  115. function checkDecl(node, value, getIndex) {
  116. // make sure multiplication operations (*) are divided - not handled
  117. // by postcss-value-parser
  118. value = value.replace(/\*/g, ',');
  119. valueParser(value).walk((valueNode) => {
  120. // Ignore wrong units within `url` function
  121. if (valueNode.type === 'function' && valueNode.value.toLowerCase() === 'url') {
  122. return false;
  123. }
  124. check(
  125. node,
  126. getIndex(node),
  127. valueNode,
  128. node.prop,
  129. secondaryOptions ? secondaryOptions.ignoreProperties : {},
  130. );
  131. });
  132. }
  133. root.walkAtRules(/^media$/i, (atRule) => checkMedia(atRule, atRule.params, atRuleParamIndex));
  134. root.walkDecls((decl) => checkDecl(decl, decl.value, declarationValueIndex));
  135. };
  136. };
  137. rule.primaryOptionArray = true;
  138. rule.ruleName = ruleName;
  139. rule.messages = messages;
  140. rule.meta = meta;
  141. module.exports = rule;