jsx-wrap-multilines.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /**
  2. * @fileoverview Prevent missing parentheses around multilines JSX
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const has = require('hasown');
  7. const docsUrl = require('../util/docsUrl');
  8. const eslintUtil = require('../util/eslint');
  9. const jsxUtil = require('../util/jsx');
  10. const reportC = require('../util/report');
  11. const isParenthesized = require('../util/ast').isParenthesized;
  12. const getSourceCode = eslintUtil.getSourceCode;
  13. const getText = eslintUtil.getText;
  14. // ------------------------------------------------------------------------------
  15. // Constants
  16. // ------------------------------------------------------------------------------
  17. const DEFAULTS = {
  18. declaration: 'parens',
  19. assignment: 'parens',
  20. return: 'parens',
  21. arrow: 'parens',
  22. condition: 'ignore',
  23. logical: 'ignore',
  24. prop: 'ignore',
  25. };
  26. // ------------------------------------------------------------------------------
  27. // Rule Definition
  28. // ------------------------------------------------------------------------------
  29. const messages = {
  30. missingParens: 'Missing parentheses around multilines JSX',
  31. extraParens: 'Expected no parentheses around multilines JSX',
  32. parensOnNewLines: 'Parentheses around JSX should be on separate lines',
  33. };
  34. /** @type {import('eslint').Rule.RuleModule} */
  35. module.exports = {
  36. meta: {
  37. docs: {
  38. description: 'Disallow missing parentheses around multiline JSX',
  39. category: 'Stylistic Issues',
  40. recommended: false,
  41. url: docsUrl('jsx-wrap-multilines'),
  42. },
  43. fixable: 'code',
  44. messages,
  45. schema: [{
  46. type: 'object',
  47. // true/false are for backwards compatibility
  48. properties: {
  49. declaration: {
  50. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  51. },
  52. assignment: {
  53. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  54. },
  55. return: {
  56. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  57. },
  58. arrow: {
  59. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  60. },
  61. condition: {
  62. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  63. },
  64. logical: {
  65. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  66. },
  67. prop: {
  68. enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
  69. },
  70. },
  71. additionalProperties: false,
  72. }],
  73. },
  74. create(context) {
  75. function getOption(type) {
  76. const userOptions = context.options[0] || {};
  77. if (has(userOptions, type)) {
  78. return userOptions[type];
  79. }
  80. return DEFAULTS[type];
  81. }
  82. function isEnabled(type) {
  83. const option = getOption(type);
  84. return option && option !== 'ignore';
  85. }
  86. function needsOpeningNewLine(node) {
  87. const previousToken = getSourceCode(context).getTokenBefore(node);
  88. if (!isParenthesized(context, node)) {
  89. return false;
  90. }
  91. if (previousToken.loc.end.line === node.loc.start.line) {
  92. return true;
  93. }
  94. return false;
  95. }
  96. function needsClosingNewLine(node) {
  97. const nextToken = getSourceCode(context).getTokenAfter(node);
  98. if (!isParenthesized(context, node)) {
  99. return false;
  100. }
  101. if (node.loc.end.line === nextToken.loc.end.line) {
  102. return true;
  103. }
  104. return false;
  105. }
  106. function isMultilines(node) {
  107. return node.loc.start.line !== node.loc.end.line;
  108. }
  109. function report(node, messageId, fix) {
  110. reportC(context, messages[messageId], messageId, {
  111. node,
  112. fix,
  113. });
  114. }
  115. function trimTokenBeforeNewline(node, tokenBefore) {
  116. // if the token before the jsx is a bracket or curly brace
  117. // we don't want a space between the opening parentheses and the multiline jsx
  118. const isBracket = tokenBefore.value === '{' || tokenBefore.value === '[';
  119. return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}`;
  120. }
  121. function check(node, type) {
  122. if (!node || !jsxUtil.isJSX(node)) {
  123. return;
  124. }
  125. const sourceCode = getSourceCode(context);
  126. const option = getOption(type);
  127. if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) {
  128. report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${getText(context, node)})`));
  129. }
  130. if (option === 'parens-new-line' && isMultilines(node)) {
  131. if (!isParenthesized(context, node)) {
  132. const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
  133. const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true });
  134. const start = node.loc.start;
  135. if (tokenBefore.loc.end.line < start.line) {
  136. // Strip newline after operator if parens newline is specified
  137. report(
  138. node,
  139. 'missingParens',
  140. (fixer) => fixer.replaceTextRange(
  141. [tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
  142. `${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${getText(context, node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
  143. )
  144. );
  145. } else {
  146. report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${getText(context, node)}\n)`));
  147. }
  148. } else {
  149. const needsOpening = needsOpeningNewLine(node);
  150. const needsClosing = needsClosingNewLine(node);
  151. if (needsOpening || needsClosing) {
  152. report(node, 'parensOnNewLines', (fixer) => {
  153. const text = getText(context, node);
  154. let fixed = text;
  155. if (needsOpening) {
  156. fixed = `\n${fixed}`;
  157. }
  158. if (needsClosing) {
  159. fixed = `${fixed}\n`;
  160. }
  161. return fixer.replaceText(node, fixed);
  162. });
  163. }
  164. }
  165. }
  166. if (option === 'never' && isParenthesized(context, node)) {
  167. const tokenBefore = sourceCode.getTokenBefore(node);
  168. const tokenAfter = sourceCode.getTokenAfter(node);
  169. report(node, 'extraParens', (fixer) => fixer.replaceTextRange(
  170. [tokenBefore.range[0], tokenAfter.range[1]],
  171. getText(context, node)
  172. ));
  173. }
  174. }
  175. // --------------------------------------------------------------------------
  176. // Public
  177. // --------------------------------------------------------------------------
  178. return {
  179. VariableDeclarator(node) {
  180. const type = 'declaration';
  181. if (!isEnabled(type)) {
  182. return;
  183. }
  184. if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') {
  185. check(node.init.consequent, type);
  186. check(node.init.alternate, type);
  187. return;
  188. }
  189. check(node.init, type);
  190. },
  191. AssignmentExpression(node) {
  192. const type = 'assignment';
  193. if (!isEnabled(type)) {
  194. return;
  195. }
  196. if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') {
  197. check(node.right.consequent, type);
  198. check(node.right.alternate, type);
  199. return;
  200. }
  201. check(node.right, type);
  202. },
  203. ReturnStatement(node) {
  204. const type = 'return';
  205. if (isEnabled(type)) {
  206. check(node.argument, type);
  207. }
  208. },
  209. 'ArrowFunctionExpression:exit': (node) => {
  210. const arrowBody = node.body;
  211. const type = 'arrow';
  212. if (isEnabled(type) && arrowBody.type !== 'BlockStatement') {
  213. check(arrowBody, type);
  214. }
  215. },
  216. ConditionalExpression(node) {
  217. const type = 'condition';
  218. if (isEnabled(type)) {
  219. check(node.consequent, type);
  220. check(node.alternate, type);
  221. }
  222. },
  223. LogicalExpression(node) {
  224. const type = 'logical';
  225. if (isEnabled(type)) {
  226. check(node.right, type);
  227. }
  228. },
  229. JSXAttribute(node) {
  230. const type = 'prop';
  231. if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') {
  232. check(node.value.expression, type);
  233. }
  234. },
  235. };
  236. },
  237. };