index.js 4.5 KB

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