jsx-wrap-multilines.js 6.9 KB

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