jsx-fragments.js 6.5 KB

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