index.js 4.5 KB

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