UNPKG

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