index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const getDimension = require('../../utils/getDimension');
  5. const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
  6. const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration');
  7. const { units } = require('../../reference/units');
  8. const mediaParser = require('postcss-media-query-parser').default;
  9. const optionsMatches = require('../../utils/optionsMatches');
  10. const report = require('../../utils/report');
  11. const ruleMessages = require('../../utils/ruleMessages');
  12. const validateOptions = require('../../utils/validateOptions');
  13. const valueParser = require('postcss-value-parser');
  14. const vendor = require('../../utils/vendor');
  15. const { isRegExp, isString, assert } = require('../../utils/validateTypes');
  16. const { isAtRule } = require('../../utils/typeGuards');
  17. const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
  18. const ruleName = 'unit-no-unknown';
  19. const messages = ruleMessages(ruleName, {
  20. rejected: (unit) => `Unexpected unknown unit "${unit}"`,
  21. });
  22. const meta = {
  23. url: 'https://stylelint.io/user-guide/rules/unit-no-unknown',
  24. };
  25. /** @type {import('stylelint').Rule} */
  26. const rule = (primary, secondaryOptions) => {
  27. return (root, result) => {
  28. const validOptions = validateOptions(
  29. result,
  30. ruleName,
  31. { actual: primary },
  32. {
  33. actual: secondaryOptions,
  34. possible: {
  35. ignoreUnits: [isString, isRegExp],
  36. ignoreFunctions: [isString, isRegExp],
  37. },
  38. optional: true,
  39. },
  40. );
  41. if (!validOptions) {
  42. return;
  43. }
  44. /**
  45. * @template {import('postcss').AtRule | import('postcss').Declaration} T
  46. * @param {T} node
  47. * @param {string} value
  48. * @param {(node: T) => number} getIndex
  49. * @returns {void}
  50. */
  51. function check(node, value, getIndex) {
  52. // make sure multiplication operations (*) are divided - not handled
  53. // by postcss-value-parser
  54. value = value.replace(/\*/g, ',');
  55. const parsedValue = valueParser(value);
  56. parsedValue.walk((valueNode) => {
  57. // Ignore wrong units within `url` function
  58. // and within functions listed in the `ignoreFunctions` option
  59. if (
  60. valueNode.type === 'function' &&
  61. (valueNode.value.toLowerCase() === 'url' ||
  62. optionsMatches(secondaryOptions, 'ignoreFunctions', valueNode.value))
  63. ) {
  64. return false;
  65. }
  66. const { number, unit } = getDimension(valueNode);
  67. if (!number || !unit) {
  68. return;
  69. }
  70. if (optionsMatches(secondaryOptions, 'ignoreUnits', unit)) {
  71. return;
  72. }
  73. if (units.has(unit.toLowerCase()) && unit.toLowerCase() !== 'x') {
  74. return;
  75. }
  76. if (unit.toLowerCase() === 'x') {
  77. if (
  78. isAtRule(node) &&
  79. node.name === 'media' &&
  80. node.params.toLowerCase().includes('resolution')
  81. ) {
  82. let ignoreUnit = false;
  83. mediaParser(node.params).walk((mediaNode, _i, mediaNodes) => {
  84. const lastMediaNode = mediaNodes[mediaNodes.length - 1];
  85. if (
  86. mediaNode.value.toLowerCase().includes('resolution') &&
  87. lastMediaNode &&
  88. lastMediaNode.sourceIndex === valueNode.sourceIndex
  89. ) {
  90. ignoreUnit = true;
  91. return false;
  92. }
  93. });
  94. if (ignoreUnit) {
  95. return;
  96. }
  97. }
  98. if (node.type === 'decl') {
  99. if (node.prop.toLowerCase() === 'image-resolution') {
  100. return;
  101. }
  102. if (/^(?:-webkit-)?image-set[\s(]/i.test(value)) {
  103. const imageSet = parsedValue.nodes.find(
  104. (n) => vendor.unprefixed(n.value) === 'image-set',
  105. );
  106. assert(imageSet);
  107. assert('nodes' in imageSet);
  108. const imageSetLastNode = imageSet.nodes[imageSet.nodes.length - 1];
  109. assert(imageSetLastNode);
  110. const imageSetValueLastIndex = imageSetLastNode.sourceIndex;
  111. if (imageSetValueLastIndex >= valueNode.sourceIndex) {
  112. return;
  113. }
  114. }
  115. }
  116. }
  117. const index = getIndex(node);
  118. report({
  119. index: index + valueNode.sourceIndex + number.length,
  120. endIndex: index + valueNode.sourceEndIndex,
  121. message: messages.rejected,
  122. messageArgs: [unit],
  123. node,
  124. result,
  125. ruleName,
  126. });
  127. });
  128. }
  129. root.walkAtRules(/^media$/i, (atRule) => {
  130. if (!isStandardSyntaxAtRule(atRule)) {
  131. return;
  132. }
  133. check(atRule, atRule.params, atRuleParamIndex);
  134. });
  135. root.walkDecls((decl) => {
  136. if (!isStandardSyntaxDeclaration(decl)) return;
  137. if (!isStandardSyntaxValue(decl.value)) return;
  138. check(decl, decl.value, declarationValueIndex);
  139. });
  140. };
  141. };
  142. rule.ruleName = ruleName;
  143. rule.messages = messages;
  144. rule.meta = meta;
  145. module.exports = rule;