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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

  • Property mode set to 100644
File size: 6.9 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 astUtil = require('../util/ast');
11const jsxUtil = require('../util/jsx');
12const docsUrl = require('../util/docsUrl');
13const report = require('../util/report');
14const getText = require('../util/eslint').getText;
15
16function isJSXText(node) {
17 return !!node && (node.type === 'JSXText' || node.type === 'Literal');
18}
19
20/**
21 * @param {string} text
22 * @returns {boolean}
23 */
24function isOnlyWhitespace(text) {
25 return text.trim().length === 0;
26}
27
28/**
29 * @param {ASTNode} node
30 * @returns {boolean}
31 */
32function isNonspaceJSXTextOrJSXCurly(node) {
33 return (isJSXText(node) && !isOnlyWhitespace(node.raw)) || node.type === 'JSXExpressionContainer';
34}
35
36/**
37 * Somehow fragment like this is useful: <Foo content={<>ee eeee eeee ...</>} />
38 * @param {ASTNode} node
39 * @returns {boolean}
40 */
41function isFragmentWithOnlyTextAndIsNotChild(node) {
42 return node.children.length === 1
43 && isJSXText(node.children[0])
44 && !(node.parent.type === 'JSXElement' || node.parent.type === 'JSXFragment');
45}
46
47/**
48 * @param {string} text
49 * @returns {string}
50 */
51function trimLikeReact(text) {
52 const leadingSpaces = /^\s*/.exec(text)[0];
53 const trailingSpaces = /\s*$/.exec(text)[0];
54
55 const start = arrayIncludes(leadingSpaces, '\n') ? leadingSpaces.length : 0;
56 const end = arrayIncludes(trailingSpaces, '\n') ? text.length - trailingSpaces.length : text.length;
57
58 return text.slice(start, end);
59}
60
61/**
62 * Test if node is like `<Fragment key={_}>_</Fragment>`
63 * @param {JSXElement} node
64 * @returns {boolean}
65 */
66function isKeyedElement(node) {
67 return node.type === 'JSXElement'
68 && node.openingElement.attributes
69 && node.openingElement.attributes.some(jsxUtil.isJSXAttributeKey);
70}
71
72/**
73 * @param {ASTNode} node
74 * @returns {boolean}
75 */
76function containsCallExpression(node) {
77 return node
78 && node.type === 'JSXExpressionContainer'
79 && astUtil.isCallExpression(node.expression);
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.