1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const elementType = require('jsx-ast-utils/elementType');
|
9 | const pragmaUtil = require('../util/pragma');
|
10 | const variableUtil = require('../util/variable');
|
11 | const versionUtil = require('../util/version');
|
12 | const docsUrl = require('../util/docsUrl');
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | function replaceNode(source, node, text) {
|
19 | return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
|
20 | }
|
21 |
|
22 | module.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 |
|
100 | if (variableInit.type === 'Identifier' && variableInit.name === reactPragma) {
|
101 | return true;
|
102 | }
|
103 |
|
104 |
|
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 |
|
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 |
|
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 | };
|