jsx-fragments.js 6.9 KB

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