jsx-fragments.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * @fileoverview Enforce shorthand or standard form for React fragments.
  3. * @author Alex Zherdev
  4. */
  5. 'use strict';
  6. const elementType = require('jsx-ast-utils/elementType');
  7. const pragmaUtil = require('../util/pragma');
  8. const variableUtil = require('../util/variable');
  9. const testReactVersion = require('../util/version').testReactVersion;
  10. const docsUrl = require('../util/docsUrl');
  11. const report = require('../util/report');
  12. // ------------------------------------------------------------------------------
  13. // Rule Definition
  14. // ------------------------------------------------------------------------------
  15. function replaceNode(source, node, text) {
  16. return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
  17. }
  18. const messages = {
  19. fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. '
  20. + 'Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.',
  21. preferPragma: 'Prefer {{react}}.{{fragment}} over fragment shorthand',
  22. preferFragment: 'Prefer fragment shorthand over {{react}}.{{fragment}}',
  23. };
  24. module.exports = {
  25. meta: {
  26. docs: {
  27. description: 'Enforce shorthand or standard form for React fragments',
  28. category: 'Stylistic Issues',
  29. recommended: false,
  30. url: docsUrl('jsx-fragments'),
  31. },
  32. fixable: 'code',
  33. messages,
  34. schema: [{
  35. enum: ['syntax', 'element'],
  36. }],
  37. },
  38. create(context) {
  39. const configuration = context.options[0] || 'syntax';
  40. const reactPragma = pragmaUtil.getFromContext(context);
  41. const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
  42. const openFragShort = '<>';
  43. const closeFragShort = '</>';
  44. const openFragLong = `<${reactPragma}.${fragmentPragma}>`;
  45. const closeFragLong = `</${reactPragma}.${fragmentPragma}>`;
  46. function reportOnReactVersion(node) {
  47. if (!testReactVersion(context, '>= 16.2.0')) {
  48. report(context, messages.fragmentsNotSupported, 'fragmentsNotSupported', {
  49. node,
  50. });
  51. return true;
  52. }
  53. return false;
  54. }
  55. function getFixerToLong(jsxFragment) {
  56. const sourceCode = context.getSourceCode();
  57. if (!jsxFragment.closingFragment || !jsxFragment.openingFragment) {
  58. // the old TS parser crashes here
  59. // TODO: FIXME: can we fake these two descriptors?
  60. return null;
  61. }
  62. return function fix(fixer) {
  63. let source = sourceCode.getText();
  64. source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
  65. source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
  66. const lengthDiff = openFragLong.length - sourceCode.getText(jsxFragment.openingFragment).length
  67. + closeFragLong.length - sourceCode.getText(jsxFragment.closingFragment).length;
  68. const range = jsxFragment.range;
  69. return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
  70. };
  71. }
  72. function getFixerToShort(jsxElement) {
  73. const sourceCode = context.getSourceCode();
  74. return function fix(fixer) {
  75. let source = sourceCode.getText();
  76. let lengthDiff;
  77. if (jsxElement.closingElement) {
  78. source = replaceNode(source, jsxElement.closingElement, closeFragShort);
  79. source = replaceNode(source, jsxElement.openingElement, openFragShort);
  80. lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length
  81. + sourceCode.getText(jsxElement.closingElement).length - closeFragShort.length;
  82. } else {
  83. source = replaceNode(source, jsxElement.openingElement, `${openFragShort}${closeFragShort}`);
  84. lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length
  85. - closeFragShort.length;
  86. }
  87. const range = jsxElement.range;
  88. return fixer.replaceTextRange(range, source.slice(range[0], range[1] - lengthDiff));
  89. };
  90. }
  91. function refersToReactFragment(name) {
  92. const variableInit = variableUtil.findVariableByName(context, name);
  93. if (!variableInit) {
  94. return false;
  95. }
  96. // const { Fragment } = React;
  97. if (variableInit.type === 'Identifier' && variableInit.name === reactPragma) {
  98. return true;
  99. }
  100. // const Fragment = React.Fragment;
  101. if (
  102. variableInit.type === 'MemberExpression'
  103. && variableInit.object.type === 'Identifier'
  104. && variableInit.object.name === reactPragma
  105. && variableInit.property.type === 'Identifier'
  106. && variableInit.property.name === fragmentPragma
  107. ) {
  108. return true;
  109. }
  110. // const { Fragment } = require('react');
  111. if (
  112. variableInit.callee
  113. && variableInit.callee.name === 'require'
  114. && variableInit.arguments
  115. && variableInit.arguments[0]
  116. && variableInit.arguments[0].value === 'react'
  117. ) {
  118. return true;
  119. }
  120. return false;
  121. }
  122. const jsxElements = [];
  123. const fragmentNames = new Set([`${reactPragma}.${fragmentPragma}`]);
  124. // --------------------------------------------------------------------------
  125. // Public
  126. // --------------------------------------------------------------------------
  127. return {
  128. JSXElement(node) {
  129. jsxElements.push(node);
  130. },
  131. JSXFragment(node) {
  132. if (reportOnReactVersion(node)) {
  133. return;
  134. }
  135. if (configuration === 'element') {
  136. report(context, messages.preferPragma, 'preferPragma', {
  137. node,
  138. data: {
  139. react: reactPragma,
  140. fragment: fragmentPragma,
  141. },
  142. fix: getFixerToLong(node),
  143. });
  144. }
  145. },
  146. ImportDeclaration(node) {
  147. if (node.source && node.source.value === 'react') {
  148. node.specifiers.forEach((spec) => {
  149. if (spec.imported && spec.imported.name === fragmentPragma) {
  150. if (spec.local) {
  151. fragmentNames.add(spec.local.name);
  152. }
  153. }
  154. });
  155. }
  156. },
  157. 'Program:exit'() {
  158. jsxElements.forEach((node) => {
  159. const openingEl = node.openingElement;
  160. const elName = elementType(openingEl);
  161. if (fragmentNames.has(elName) || refersToReactFragment(elName)) {
  162. if (reportOnReactVersion(node)) {
  163. return;
  164. }
  165. const attrs = openingEl.attributes;
  166. if (configuration === 'syntax' && !(attrs && attrs.length > 0)) {
  167. report(context, messages.preferFragment, 'preferFragment', {
  168. node,
  169. data: {
  170. react: reactPragma,
  171. fragment: fragmentPragma,
  172. },
  173. fix: getFixerToShort(node),
  174. });
  175. }
  176. }
  177. });
  178. },
  179. };
  180. },
  181. };