jsx-curly-brace-presence.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /**
  2. * @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
  3. * @author Jacky Ho
  4. * @author Simon Lydell
  5. */
  6. 'use strict';
  7. const docsUrl = require('../util/docsUrl');
  8. // ------------------------------------------------------------------------------
  9. // Constants
  10. // ------------------------------------------------------------------------------
  11. const OPTION_ALWAYS = 'always';
  12. const OPTION_NEVER = 'never';
  13. const OPTION_IGNORE = 'ignore';
  14. const OPTION_VALUES = [
  15. OPTION_ALWAYS,
  16. OPTION_NEVER,
  17. OPTION_IGNORE
  18. ];
  19. const DEFAULT_CONFIG = {props: OPTION_NEVER, children: OPTION_NEVER};
  20. // ------------------------------------------------------------------------------
  21. // Rule Definition
  22. // ------------------------------------------------------------------------------
  23. module.exports = {
  24. meta: {
  25. docs: {
  26. description:
  27. 'Disallow unnecessary JSX expressions when literals alone are sufficient ' +
  28. 'or enfore JSX expressions on literals in JSX children or attributes',
  29. category: 'Stylistic Issues',
  30. recommended: false,
  31. url: docsUrl('jsx-curly-brace-presence')
  32. },
  33. fixable: 'code',
  34. schema: [
  35. {
  36. oneOf: [
  37. {
  38. type: 'object',
  39. properties: {
  40. props: {enum: OPTION_VALUES, default: DEFAULT_CONFIG.props},
  41. children: {enum: OPTION_VALUES, default: DEFAULT_CONFIG.children}
  42. },
  43. additionalProperties: false
  44. },
  45. {
  46. enum: OPTION_VALUES
  47. }
  48. ]
  49. }
  50. ]
  51. },
  52. create: function(context) {
  53. const ruleOptions = context.options[0];
  54. const userConfig = typeof ruleOptions === 'string' ?
  55. {props: ruleOptions, children: ruleOptions} :
  56. Object.assign({}, DEFAULT_CONFIG, ruleOptions);
  57. function containsLineTerminators(rawStringValue) {
  58. return /[\n\r\u2028\u2029]/.test(rawStringValue);
  59. }
  60. function containsBackslash(rawStringValue) {
  61. return rawStringValue.includes('\\');
  62. }
  63. function containsHTMLEntity(rawStringValue) {
  64. return /&[A-Za-z\d#]+;/.test(rawStringValue);
  65. }
  66. function containsDisallowedJSXTextChars(rawStringValue) {
  67. return /[{<>}]/.test(rawStringValue);
  68. }
  69. function containsQuoteCharacters(value) {
  70. return /['"]/.test(value);
  71. }
  72. function escapeDoubleQuotes(rawStringValue) {
  73. return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
  74. }
  75. function escapeBackslashes(rawStringValue) {
  76. return rawStringValue.replace(/\\/g, '\\\\');
  77. }
  78. function needToEscapeCharacterForJSX(raw) {
  79. return (
  80. containsBackslash(raw) ||
  81. containsHTMLEntity(raw) ||
  82. containsDisallowedJSXTextChars(raw)
  83. );
  84. }
  85. function containsWhitespaceExpression(child) {
  86. if (child.type === 'JSXExpressionContainer') {
  87. const value = child.expression.value;
  88. return value ? !(/\S/.test(value)) : false;
  89. }
  90. return false;
  91. }
  92. /**
  93. * Report and fix an unnecessary curly brace violation on a node
  94. * @param {ASTNode} node - The AST node with an unnecessary JSX expression
  95. */
  96. function reportUnnecessaryCurly(JSXExpressionNode) {
  97. context.report({
  98. node: JSXExpressionNode,
  99. message: 'Curly braces are unnecessary here.',
  100. fix: function(fixer) {
  101. const expression = JSXExpressionNode.expression;
  102. const expressionType = expression.type;
  103. const parentType = JSXExpressionNode.parent.type;
  104. let textToReplace;
  105. if (parentType === 'JSXAttribute') {
  106. textToReplace = `"${expressionType === 'TemplateLiteral' ?
  107. expression.quasis[0].value.raw :
  108. expression.raw.substring(1, expression.raw.length - 1)
  109. }"`;
  110. } else {
  111. textToReplace = expressionType === 'TemplateLiteral' ?
  112. expression.quasis[0].value.cooked : expression.value;
  113. }
  114. return fixer.replaceText(JSXExpressionNode, textToReplace);
  115. }
  116. });
  117. }
  118. function reportMissingCurly(literalNode) {
  119. context.report({
  120. node: literalNode,
  121. message: 'Need to wrap this literal in a JSX expression.',
  122. fix: function(fixer) {
  123. // If a HTML entity name is found, bail out because it can be fixed
  124. // by either using the real character or the unicode equivalent.
  125. // If it contains any line terminator character, bail out as well.
  126. if (
  127. containsHTMLEntity(literalNode.raw) ||
  128. containsLineTerminators(literalNode.raw)
  129. ) {
  130. return null;
  131. }
  132. const expression = literalNode.parent.type === 'JSXAttribute' ?
  133. `{"${escapeDoubleQuotes(escapeBackslashes(
  134. literalNode.raw.substring(1, literalNode.raw.length - 1)
  135. ))}"}` :
  136. `{${JSON.stringify(literalNode.value)}}`;
  137. return fixer.replaceText(literalNode, expression);
  138. }
  139. });
  140. }
  141. // Bail out if there is any character that needs to be escaped in JSX
  142. // because escaping decreases readiblity and the original code may be more
  143. // readible anyway or intentional for other specific reasons
  144. function lintUnnecessaryCurly(JSXExpressionNode) {
  145. const expression = JSXExpressionNode.expression;
  146. const expressionType = expression.type;
  147. const parentType = JSXExpressionNode.parent.type;
  148. if (
  149. expressionType === 'Literal' &&
  150. typeof expression.value === 'string' &&
  151. !needToEscapeCharacterForJSX(expression.raw) && (
  152. parentType === 'JSXElement' ||
  153. !containsQuoteCharacters(expression.value)
  154. )
  155. ) {
  156. reportUnnecessaryCurly(JSXExpressionNode);
  157. } else if (
  158. expressionType === 'TemplateLiteral' &&
  159. expression.expressions.length === 0 &&
  160. !needToEscapeCharacterForJSX(expression.quasis[0].value.raw) && (
  161. parentType === 'JSXElement' ||
  162. !containsQuoteCharacters(expression.quasis[0].value.cooked)
  163. )
  164. ) {
  165. reportUnnecessaryCurly(JSXExpressionNode);
  166. }
  167. }
  168. function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
  169. return (
  170. parentType === 'JSXAttribute' &&
  171. typeof config.props === 'string' &&
  172. config.props === ruleCondition
  173. ) || (
  174. parentType === 'JSXElement' &&
  175. typeof config.children === 'string' &&
  176. config.children === ruleCondition
  177. );
  178. }
  179. function shouldCheckForUnnecessaryCurly(parent, config) {
  180. const parentType = parent.type;
  181. // If there are more than one JSX child, there is no need to check for
  182. // unnecessary curly braces.
  183. if (parentType === 'JSXElement' && parent.children.length !== 1) {
  184. return false;
  185. }
  186. if (
  187. parent.children
  188. && parent.children.length === 1
  189. && containsWhitespaceExpression(parent.children[0])
  190. ) {
  191. return false;
  192. }
  193. return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
  194. }
  195. function shouldCheckForMissingCurly(parent, config) {
  196. if (
  197. parent.children
  198. && parent.children.length === 1
  199. && containsWhitespaceExpression(parent.children[0])
  200. ) {
  201. return false;
  202. }
  203. return areRuleConditionsSatisfied(parent.type, config, OPTION_ALWAYS);
  204. }
  205. // --------------------------------------------------------------------------
  206. // Public
  207. // --------------------------------------------------------------------------
  208. return {
  209. JSXExpressionContainer: node => {
  210. if (shouldCheckForUnnecessaryCurly(node.parent, userConfig)) {
  211. lintUnnecessaryCurly(node);
  212. }
  213. },
  214. Literal: node => {
  215. if (shouldCheckForMissingCurly(node.parent, userConfig)) {
  216. reportMissingCurly(node);
  217. }
  218. },
  219. JSXText: node => {
  220. if (shouldCheckForMissingCurly(node.parent, userConfig)) {
  221. reportMissingCurly(node);
  222. }
  223. }
  224. };
  225. }
  226. };