index.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const getDeclarationValue = require('../../utils/getDeclarationValue');
  5. const isWhitespace = require('../../utils/isWhitespace');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const setDeclarationValue = require('../../utils/setDeclarationValue');
  9. const styleSearch = require('style-search');
  10. const validateOptions = require('../../utils/validateOptions');
  11. const ruleName = 'function-whitespace-after';
  12. const messages = ruleMessages(ruleName, {
  13. expected: 'Expected whitespace after ")"',
  14. rejected: 'Unexpected whitespace after ")"',
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/function-whitespace-after',
  18. fixable: true,
  19. };
  20. const ACCEPTABLE_AFTER_CLOSING_PAREN = new Set([')', ',', '}', ':', '/', undefined]);
  21. /** @type {import('stylelint').Rule} */
  22. const rule = (primary, _secondaryOptions, context) => {
  23. return (root, result) => {
  24. const validOptions = validateOptions(result, ruleName, {
  25. actual: primary,
  26. possible: ['always', 'never'],
  27. });
  28. if (!validOptions) {
  29. return;
  30. }
  31. /**
  32. * @param {import('postcss').Node} node
  33. * @param {string} value
  34. * @param {number} nodeIndex
  35. * @param {((index: number) => void) | undefined} fix
  36. */
  37. function check(node, value, nodeIndex, fix) {
  38. styleSearch(
  39. {
  40. source: value,
  41. target: ')',
  42. functionArguments: 'only',
  43. },
  44. (match) => {
  45. checkClosingParen(value, match.startIndex + 1, node, nodeIndex, fix);
  46. },
  47. );
  48. }
  49. /**
  50. * @param {string} source
  51. * @param {number} index
  52. * @param {import('postcss').Node} node
  53. * @param {number} nodeIndex
  54. * @param {((index: number) => void) | undefined} fix
  55. */
  56. function checkClosingParen(source, index, node, nodeIndex, fix) {
  57. const nextChar = source.charAt(index);
  58. if (!nextChar) return;
  59. if (primary === 'always') {
  60. // Allow for the next character to be a single empty space,
  61. // another closing parenthesis, a comma, or the end of the value
  62. if (nextChar === ' ') {
  63. return;
  64. }
  65. if (nextChar === '\n') {
  66. return;
  67. }
  68. if (source.slice(index, index + 2) === '\r\n') {
  69. return;
  70. }
  71. if (ACCEPTABLE_AFTER_CLOSING_PAREN.has(nextChar)) {
  72. return;
  73. }
  74. if (fix) {
  75. fix(index);
  76. return;
  77. }
  78. report({
  79. message: messages.expected,
  80. node,
  81. index: nodeIndex + index,
  82. result,
  83. ruleName,
  84. });
  85. } else if (primary === 'never' && isWhitespace(nextChar)) {
  86. if (fix) {
  87. fix(index);
  88. return;
  89. }
  90. report({
  91. message: messages.rejected,
  92. node,
  93. index: nodeIndex + index,
  94. result,
  95. ruleName,
  96. });
  97. }
  98. }
  99. /**
  100. * @param {string} value
  101. */
  102. function createFixer(value) {
  103. let fixed = '';
  104. let lastIndex = 0;
  105. /** @type {(index: number) => void} */
  106. let applyFix;
  107. if (primary === 'always') {
  108. applyFix = (index) => {
  109. // eslint-disable-next-line prefer-template
  110. fixed += value.slice(lastIndex, index) + ' ';
  111. lastIndex = index;
  112. };
  113. } else if (primary === 'never') {
  114. applyFix = (index) => {
  115. let whitespaceEndIndex = index + 1;
  116. while (
  117. whitespaceEndIndex < value.length &&
  118. isWhitespace(value.charAt(whitespaceEndIndex))
  119. ) {
  120. whitespaceEndIndex++;
  121. }
  122. fixed += value.slice(lastIndex, index);
  123. lastIndex = whitespaceEndIndex;
  124. };
  125. } else {
  126. throw new Error(`Unexpected option: "${primary}"`);
  127. }
  128. return {
  129. applyFix,
  130. get hasFixed() {
  131. return Boolean(lastIndex);
  132. },
  133. get fixed() {
  134. return fixed + value.slice(lastIndex);
  135. },
  136. };
  137. }
  138. root.walkAtRules(/^import$/i, (atRule) => {
  139. const param = (atRule.raws.params && atRule.raws.params.raw) || atRule.params;
  140. const fixer = context.fix && createFixer(param);
  141. check(atRule, param, atRuleParamIndex(atRule), fixer ? fixer.applyFix : undefined);
  142. if (fixer && fixer.hasFixed) {
  143. if (atRule.raws.params) {
  144. atRule.raws.params.raw = fixer.fixed;
  145. } else {
  146. atRule.params = fixer.fixed;
  147. }
  148. }
  149. });
  150. root.walkDecls((decl) => {
  151. const value = getDeclarationValue(decl);
  152. const fixer = context.fix && createFixer(value);
  153. check(decl, value, declarationValueIndex(decl), fixer ? fixer.applyFix : undefined);
  154. if (fixer && fixer.hasFixed) {
  155. setDeclarationValue(decl, fixer.fixed);
  156. }
  157. });
  158. };
  159. };
  160. rule.ruleName = ruleName;
  161. rule.messages = messages;
  162. rule.meta = meta;
  163. module.exports = rule;