source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-no-useless-fragment.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 6.8 KB
Line 
1/**
2 * @fileoverview Disallow useless fragments
3 */
4
5'use strict';
6
7const arrayIncludes = require('array-includes');
8
9const pragmaUtil = require('../util/pragma');
10const jsxUtil = require('../util/jsx');
11const docsUrl = require('../util/docsUrl');
12const report = require('../util/report');
13const getText = require('../util/eslint').getText;
14
15function isJSXText(node) {
16 return !!node && (node.type === 'JSXText' || node.type === 'Literal');
17}
18
19/**
20 * @param {string} text
21 * @returns {boolean}
22 */
23function isOnlyWhitespace(text) {
24 return text.trim().length === 0;
25}
26
27/**
28 * @param {ASTNode} node
29 * @returns {boolean}
30 */
31function isNonspaceJSXTextOrJSXCurly(node) {
32 return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer';
33}
34
35/**
36 * Somehow fragment like this is useful: <Foo content={<>ee eeee eeee ...</>} />
37 * @param {ASTNode} node
38 * @returns {boolean}
39 */
40function isFragmentWithOnlyTextAndIsNotChild(node) {
41 return node.children.length === 1
42 && isJSXText(node.children[0])
43 && !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment');
44}
45
46/**
47 * @param {string} text
48 * @returns {string}
49 */
50function trimLikeReact(text) {
51 const leadingSpaces = /^\s*/.exec(text)[0];
52 const trailingSpaces = /\s*$/.exec(text)[0];
53
54 const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0;
55 const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length;
56
57 return text.slice(start, end);
58}
59
60/**
61 * Test if node is like `<Fragment key={_}>_</Fragment>`
62 * @param {JSXElement} node
63 * @returns {boolean}
64 */
65function isKeyedElement(node) {
66 return node.type === 'JSXElement'
67 && node.openingElement.attributes
68 && node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey);
69}
70
71/**
72 * @param {ASTNode} node
73 * @returns {boolean}
74 */
75function containsCallExpression(node) {
76 return node
77 && node.type === 'JSXExpressionContainer'
78 && node.expression
79 && node.expression.type === 'CallExpression';
80}
81
82const messages = {
83 NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, there’s no need for a Fragment at all.',
84 ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.',
85};
86
87/** @type {import('eslint').Rule.RuleModule} */
88module.exports = {
89 meta: {
90 type: 'suggestion',
91 fixable: 'code',
92 docs: {
93 description: 'Disallow unnecessary fragments',
94 category: 'Possible Errors',
95 recommended: false,
96 url: docsUrl('jsx-no-useless-fragment'),
97 },
98 messages,
99 schema: [{
100 type: 'object',
101 properties: {
102 allowExpressions: {
103 type: 'boolean',
104 },
105 },
106 }],
107 },
108
109 create(context) {
110 const config = context.options[0] || {};
111 const allowExpressions = config.allowExpressions || false;
112
113 const reactPragma = pragmaUtil.getFromContext(context);
114 const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
115
116 /**
117 * Test whether a node is an padding spaces trimmed by react runtime.
118 * @param {ASTNode} node
119 * @returns {boolean}
120 */
121 function isPaddingSpaces(node) {
122 return isJSXText(node)
123 && isOnlyWhitespace(node.raw)
124 && arrayIncludes(node.raw, '\n');
125 }
126
127 function isFragmentWithSingleExpression(node) {
128 const children = node && node.children.filter((child) => !isPaddingSpaces(child));
129 return (
130 children
131 && children.length === 1
132 && children[0].type === 'JSXExpressionContainer'
133 );
134 }
135
136 /**
137 * Test whether a JSXElement has less than two children, excluding paddings spaces.
138 * @param {JSXElement|JSXFragment} node
139 * @returns {boolean}
140 */
141 function hasLessThanTwoChildren(node) {
142 if (!node || !node.children) {
143 return true;
144 }
145
146 /** @type {ASTNode[]} */
147 const nonPaddingChildren = node.children.filter(
148 (child) => !isPaddingSpaces(child)
149 );
150
151 if (nonPaddingChildren.length < 2) {
152 return !containsCallExpression(nonPaddingChildren[0]);
153 }
154 }
155
156 /**
157 * @param {JSXElement|JSXFragment} node
158 * @returns {boolean}
159 */
160 function isChildOfHtmlElement(node) {
161 return node.parent.type === 'JSXElement'
162 && node.parent.openingElement.name.type === 'JSXIdentifier'
163 && /^[a-z]+$/.test(node.parent.openingElement.name.name);
164 }
165
166 /**
167 * @param {JSXElement|JSXFragment} node
168 * @return {boolean}
169 */
170 function isChildOfComponentElement(node) {
171 return node.parent.type === 'JSXElement'
172 && !isChildOfHtmlElement(node)
173 && !jsxUtil.isFragment(node.parent, reactPragma, fragmentPragma);
174 }
175
176 /**
177 * @param {ASTNode} node
178 * @returns {boolean}
179 */
180 function canFix(node) {
181 // Not safe to fix fragments without a jsx parent.
182 if (!(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment')) {
183 // const a = <></>
184 if (node.children.length === 0) {
185 return false;
186 }
187
188 // const a = <>cat {meow}</>
189 if (node.children.some(isNonspaceJSXTextOrJSXCurly)) {
190 return false;
191 }
192 }
193
194 // Not safe to fix `<Eeee><>foo</></Eeee>` because `Eeee` might require its children be a ReactElement.
195 if (isChildOfComponentElement(node)) {
196 return false;
197 }
198
199 // old TS parser can't handle this one
200 if (node.type === 'JSXFragment' && (!node.openingFragment || !node.closingFragment)) {
201 return false;
202 }
203
204 return true;
205 }
206
207 /**
208 * @param {ASTNode} node
209 * @returns {Function | undefined}
210 */
211 function getFix(node) {
212 if (!canFix(node)) {
213 return undefined;
214 }
215
216 return function fix(fixer) {
217 const opener = node.type === 'JSXFragment' ? node.openingFragment : node.openingElement;
218 const closer = node.type === 'JSXFragment' ? node.closingFragment : node.closingElement;
219
220 const childrenText = opener.selfClosing ? '' : getText(context).slice(opener.range[1], closer.range[0]);
221
222 return fixer.replaceText(node, trimLikeReact(childrenText));
223 };
224 }
225
226 function checkNode(node) {
227 if (isKeyedElement(node)) {
228 return;
229 }
230
231 if (
232 hasLessThanTwoChildren(node)
233 && !isFragmentWithOnlyTextAndIsNotChild(node)
234 && !(allowExpressions && isFragmentWithSingleExpression(node))
235 ) {
236 report(context, messages.NeedsMoreChildren, 'NeedsMoreChildren', {
237 node,
238 fix: getFix(node),
239 });
240 }
241
242 if (isChildOfHtmlElement(node)) {
243 report(context, messages.ChildOfHtmlElement, 'ChildOfHtmlElement', {
244 node,
245 fix: getFix(node),
246 });
247 }
248 }
249
250 return {
251 JSXElement(node) {
252 if (jsxUtil.isFragment(node, reactPragma, fragmentPragma)) {
253 checkNode(node);
254 }
255 },
256 JSXFragment: checkNode,
257 };
258 },
259};
Note: See TracBrowser for help on using the repository browser.