jsx-wrap-multilines.js 7.6 KB

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