index.js 6.3 KB

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