jsx-wrap-multilines.js 7.8 KB

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