index.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. 'use strict';
  2. const declarationValueIndex = require('../../utils/declarationValueIndex');
  3. const getDeclarationValue = require('../../utils/getDeclarationValue');
  4. const isSingleLineString = require('../../utils/isSingleLineString');
  5. const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const setDeclarationValue = require('../../utils/setDeclarationValue');
  9. const validateOptions = require('../../utils/validateOptions');
  10. const valueParser = require('postcss-value-parser');
  11. const ruleName = 'function-parentheses-newline-inside';
  12. const messages = ruleMessages(ruleName, {
  13. expectedOpening: 'Expected newline after "("',
  14. expectedClosing: 'Expected newline before ")"',
  15. expectedOpeningMultiLine: 'Expected newline after "(" in a multi-line function',
  16. rejectedOpeningMultiLine: 'Unexpected whitespace after "(" in a multi-line function',
  17. expectedClosingMultiLine: 'Expected newline before ")" in a multi-line function',
  18. rejectedClosingMultiLine: 'Unexpected whitespace before ")" in a multi-line function',
  19. });
  20. const meta = {
  21. url: 'https://stylelint.io/user-guide/rules/function-parentheses-newline-inside',
  22. fixable: true,
  23. };
  24. /** @type {import('stylelint').Rule} */
  25. const rule = (primary, _secondaryOptions, context) => {
  26. return (root, result) => {
  27. const validOptions = validateOptions(result, ruleName, {
  28. actual: primary,
  29. possible: ['always', 'always-multi-line', 'never-multi-line'],
  30. });
  31. if (!validOptions) {
  32. return;
  33. }
  34. root.walkDecls((decl) => {
  35. if (!decl.value.includes('(')) {
  36. return;
  37. }
  38. let hasFixed = false;
  39. const declValue = getDeclarationValue(decl);
  40. const parsedValue = valueParser(declValue);
  41. parsedValue.walk((valueNode) => {
  42. if (valueNode.type !== 'function') {
  43. return;
  44. }
  45. if (!isStandardSyntaxFunction(valueNode)) {
  46. return;
  47. }
  48. const functionString = valueParser.stringify(valueNode);
  49. const isMultiLine = !isSingleLineString(functionString);
  50. const containsNewline = (/** @type {string} */ str) => str.includes('\n');
  51. // Check opening ...
  52. const openingIndex = valueNode.sourceIndex + valueNode.value.length + 1;
  53. const checkBefore = getCheckBefore(valueNode);
  54. if (primary === 'always' && !containsNewline(checkBefore)) {
  55. if (context.fix) {
  56. hasFixed = true;
  57. fixBeforeForAlways(valueNode, context.newline || '');
  58. } else {
  59. complain(messages.expectedOpening, openingIndex);
  60. }
  61. }
  62. if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkBefore)) {
  63. if (context.fix) {
  64. hasFixed = true;
  65. fixBeforeForAlways(valueNode, context.newline || '');
  66. } else {
  67. complain(messages.expectedOpeningMultiLine, openingIndex);
  68. }
  69. }
  70. if (isMultiLine && primary === 'never-multi-line' && checkBefore !== '') {
  71. if (context.fix) {
  72. hasFixed = true;
  73. fixBeforeForNever(valueNode);
  74. } else {
  75. complain(messages.rejectedOpeningMultiLine, openingIndex);
  76. }
  77. }
  78. // Check closing ...
  79. const closingIndex = valueNode.sourceIndex + functionString.length - 2;
  80. const checkAfter = getCheckAfter(valueNode);
  81. if (primary === 'always' && !containsNewline(checkAfter)) {
  82. if (context.fix) {
  83. hasFixed = true;
  84. fixAfterForAlways(valueNode, context.newline || '');
  85. } else {
  86. complain(messages.expectedClosing, closingIndex);
  87. }
  88. }
  89. if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkAfter)) {
  90. if (context.fix) {
  91. hasFixed = true;
  92. fixAfterForAlways(valueNode, context.newline || '');
  93. } else {
  94. complain(messages.expectedClosingMultiLine, closingIndex);
  95. }
  96. }
  97. if (isMultiLine && primary === 'never-multi-line' && checkAfter !== '') {
  98. if (context.fix) {
  99. hasFixed = true;
  100. fixAfterForNever(valueNode);
  101. } else {
  102. complain(messages.rejectedClosingMultiLine, closingIndex);
  103. }
  104. }
  105. });
  106. if (hasFixed) {
  107. setDeclarationValue(decl, parsedValue.toString());
  108. }
  109. /**
  110. * @param {string} message
  111. * @param {number} offset
  112. */
  113. function complain(message, offset) {
  114. report({
  115. ruleName,
  116. result,
  117. message,
  118. node: decl,
  119. index: declarationValueIndex(decl) + offset,
  120. });
  121. }
  122. });
  123. };
  124. };
  125. /** @typedef {import('postcss-value-parser').FunctionNode} FunctionNode */
  126. /**
  127. * @param {FunctionNode} valueNode
  128. */
  129. function getCheckBefore(valueNode) {
  130. let before = valueNode.before;
  131. for (const node of valueNode.nodes) {
  132. if (node.type === 'comment') {
  133. continue;
  134. }
  135. if (node.type === 'space') {
  136. before += node.value;
  137. continue;
  138. }
  139. break;
  140. }
  141. return before;
  142. }
  143. /**
  144. * @param {FunctionNode} valueNode
  145. */
  146. function getCheckAfter(valueNode) {
  147. let after = '';
  148. for (const node of [...valueNode.nodes].reverse()) {
  149. if (node.type === 'comment') {
  150. continue;
  151. }
  152. if (node.type === 'space') {
  153. after = node.value + after;
  154. continue;
  155. }
  156. break;
  157. }
  158. after += valueNode.after;
  159. return after;
  160. }
  161. /**
  162. * @param {FunctionNode} valueNode
  163. * @param {string} newline
  164. */
  165. function fixBeforeForAlways(valueNode, newline) {
  166. let target;
  167. for (const node of valueNode.nodes) {
  168. if (node.type === 'comment') {
  169. continue;
  170. }
  171. if (node.type === 'space') {
  172. target = node;
  173. continue;
  174. }
  175. break;
  176. }
  177. if (target) {
  178. target.value = newline + target.value;
  179. } else {
  180. valueNode.before = newline + valueNode.before;
  181. }
  182. }
  183. /**
  184. * @param {FunctionNode} valueNode
  185. */
  186. function fixBeforeForNever(valueNode) {
  187. valueNode.before = '';
  188. for (const node of valueNode.nodes) {
  189. if (node.type === 'comment') {
  190. continue;
  191. }
  192. if (node.type === 'space') {
  193. node.value = '';
  194. continue;
  195. }
  196. break;
  197. }
  198. }
  199. /**
  200. * @param {FunctionNode} valueNode
  201. * @param {string} newline
  202. */
  203. function fixAfterForAlways(valueNode, newline) {
  204. valueNode.after = newline + valueNode.after;
  205. }
  206. /**
  207. * @param {FunctionNode} valueNode
  208. */
  209. function fixAfterForNever(valueNode) {
  210. valueNode.after = '';
  211. for (const node of [...valueNode.nodes].reverse()) {
  212. if (node.type === 'comment') {
  213. continue;
  214. }
  215. if (node.type === 'space') {
  216. node.value = '';
  217. continue;
  218. }
  219. break;
  220. }
  221. }
  222. rule.ruleName = ruleName;
  223. rule.messages = messages;
  224. rule.meta = meta;
  225. module.exports = rule;