index.js 6.2 KB

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