index.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { atRuleParamIndex, declarationValueIndex } from '../../utils/nodeFieldIndices.mjs';
  2. import functionArgumentsSearch from '../../utils/functionArgumentsSearch.mjs';
  3. import getAtRuleParams from '../../utils/getAtRuleParams.mjs';
  4. import getDeclarationValue from '../../utils/getDeclarationValue.mjs';
  5. import isStandardSyntaxDeclaration from '../../utils/isStandardSyntaxDeclaration.mjs';
  6. import isStandardSyntaxUrl from '../../utils/isStandardSyntaxUrl.mjs';
  7. import optionsMatches from '../../utils/optionsMatches.mjs';
  8. import report from '../../utils/report.mjs';
  9. import ruleMessages from '../../utils/ruleMessages.mjs';
  10. import validateOptions from '../../utils/validateOptions.mjs';
  11. const ruleName = 'function-url-quotes';
  12. const messages = ruleMessages(ruleName, {
  13. expected: (functionName) => `Expected quotes around "${functionName}" function argument`,
  14. rejected: (functionName) => `Unexpected quotes around "${functionName}" function argument`,
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/function-url-quotes',
  18. fixable: true,
  19. };
  20. const URL_FUNC_REGEX = /url\(/i;
  21. /** @import { Problem, CoreRules } from 'stylelint' */
  22. /** @import { FunctionNode, ParsedValue } from 'postcss-value-parser' */
  23. /** @import { AtRule, Declaration } from 'postcss' */
  24. /** @type {CoreRules[ruleName]} */
  25. const rule = (primary, secondaryOptions) => {
  26. return (root, result) => {
  27. const validOptions = validateOptions(
  28. result,
  29. ruleName,
  30. {
  31. actual: primary,
  32. possible: ['always', 'never'],
  33. },
  34. {
  35. actual: secondaryOptions,
  36. possible: {
  37. except: ['empty'],
  38. },
  39. optional: true,
  40. },
  41. );
  42. if (!validOptions) {
  43. return;
  44. }
  45. const exceptEmpty = optionsMatches(secondaryOptions, 'except', 'empty');
  46. const emptyArgumentPatterns = new Set(['', "''", '""']);
  47. root.walkAtRules(checkAtRuleParams);
  48. root.walkDecls(checkDeclParams);
  49. /**
  50. * @param {Declaration|AtRule} node
  51. * @param {FunctionNode} fn
  52. * @param {keyof messages} messageType
  53. * @param {ParsedValue} parsedValue
  54. */
  55. function getFix(node, fn, messageType, parsedValue) {
  56. return () => {
  57. switch (messageType) {
  58. case 'expected':
  59. addQuotes(fn);
  60. break;
  61. case 'rejected':
  62. removeQuotes(fn);
  63. break;
  64. }
  65. switch (node.type) {
  66. case 'decl':
  67. node.value = parsedValue.toString();
  68. break;
  69. case 'atrule':
  70. node.params = parsedValue.toString();
  71. break;
  72. }
  73. };
  74. }
  75. /**
  76. * @param {AtRule|Declaration} node
  77. * @param {string} source
  78. * @param {number} startIndex
  79. */
  80. function complain(node, source, startIndex) {
  81. functionArgumentsSearch(source, /^url$/i, (args, index, funcNode, parsedValue) => {
  82. const object = checkArgs(args, startIndex + index);
  83. if (object) {
  84. const { messageType, ...rest } = object;
  85. const message = messages[messageType];
  86. const messageArgs = [funcNode.value.toLowerCase()];
  87. const fix = getFix(node, funcNode, messageType, parsedValue);
  88. report({
  89. ...rest,
  90. node,
  91. fix: { apply: fix, node },
  92. message,
  93. messageArgs,
  94. result,
  95. ruleName,
  96. });
  97. }
  98. });
  99. }
  100. /** @param {Declaration} decl */
  101. function checkDeclParams(decl) {
  102. if (!URL_FUNC_REGEX.test(decl.value)) return;
  103. if (!isStandardSyntaxDeclaration(decl)) return;
  104. const value = getDeclarationValue(decl);
  105. const startIndex = declarationValueIndex(decl);
  106. complain(decl, value, startIndex);
  107. }
  108. /** @param {AtRule} atRule */
  109. function checkAtRuleParams(atRule) {
  110. const params = getAtRuleParams(atRule);
  111. const startIndex = atRuleParamIndex(atRule);
  112. complain(atRule, params, startIndex);
  113. }
  114. /** @param {FunctionNode} funcNode */
  115. function addQuotes(funcNode) {
  116. for (const argNode of funcNode.nodes) {
  117. if (argNode.type === 'word') {
  118. argNode.value = `"${argNode.value}"`;
  119. }
  120. }
  121. }
  122. /** @param {FunctionNode} funcNode */
  123. function removeQuotes(funcNode) {
  124. for (const argNode of funcNode.nodes) {
  125. if (argNode.type === 'string') {
  126. // NOTE: We can ignore this error because the test passes.
  127. // @ts-expect-error -- TS2322: Type '"word"' is not assignable to type '"string"'.
  128. argNode.type = 'word';
  129. }
  130. }
  131. }
  132. /**
  133. * @param {string} args
  134. * @param {number} index
  135. * @returns {Pick<Problem, 'index' | 'endIndex'> & { messageType: keyof messages } | undefined}
  136. */
  137. function checkArgs(args, index) {
  138. const leftTrimmedArgs = args.trimStart();
  139. if (!isStandardSyntaxUrl(leftTrimmedArgs)) return;
  140. let expectQuotes = primary === 'always';
  141. if (exceptEmpty && emptyArgumentPatterns.has(args.trim())) {
  142. expectQuotes = !expectQuotes;
  143. }
  144. const hasQuotes = leftTrimmedArgs.startsWith("'") || leftTrimmedArgs.startsWith('"');
  145. if (expectQuotes && hasQuotes) return;
  146. if (!expectQuotes && !hasQuotes) return;
  147. const messageType = expectQuotes ? 'expected' : 'rejected';
  148. const reportIndex = index + args.length - leftTrimmedArgs.length;
  149. const reportEndIndex = index + args.length;
  150. return {
  151. messageType,
  152. index: reportIndex,
  153. endIndex: reportEndIndex,
  154. };
  155. }
  156. };
  157. };
  158. rule.ruleName = ruleName;
  159. rule.messages = messages;
  160. rule.meta = meta;
  161. export default rule;