eslint-plugin-react-jsx 5.16.1 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +202 -185
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
|
|
2
|
-
import { Check, Extract } from "@eslint-react/ast";
|
|
3
|
-
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
4
2
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
import { Check, Extract } from "@eslint-react/ast";
|
|
5
4
|
import * as core from "@eslint-react/core";
|
|
5
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
6
6
|
import "@eslint-react/eslint";
|
|
7
|
-
import { collapseMultilineText, findAttribute, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
7
|
+
import { collapseMultilineText, findAttribute, getAttributeName, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
8
8
|
import ts from "typescript";
|
|
9
9
|
|
|
10
10
|
//#region \0rolldown/runtime.js
|
|
@@ -26,45 +26,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region package.json
|
|
28
28
|
var name$2 = "eslint-plugin-react-jsx";
|
|
29
|
-
var version = "5.
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/utils/common.ts
|
|
33
|
-
function findChildrenProperty(node) {
|
|
34
|
-
for (const prop of node.properties) if (prop.type === AST_NODE_TYPES.Property && Extract.getStaticPropertyName(prop) === "children") return prop;
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
function getChildrenContentRange(node) {
|
|
38
|
-
const first = node.children.at(0);
|
|
39
|
-
const last = node.children.at(-1);
|
|
40
|
-
if (first == null || last == null) return null;
|
|
41
|
-
return [first.range[0], last.range[1]];
|
|
42
|
-
}
|
|
43
|
-
function getChildrenPropText(context, prop) {
|
|
44
|
-
const { sourceCode } = context;
|
|
45
|
-
const { value } = prop;
|
|
46
|
-
if (value == null) return null;
|
|
47
|
-
if (value.type === AST_NODE_TYPES.Literal) return escapeJsxText(String(value.value));
|
|
48
|
-
if (value.type !== AST_NODE_TYPES.JSXExpressionContainer) return null;
|
|
49
|
-
const { expression } = value;
|
|
50
|
-
if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return null;
|
|
51
|
-
const exprText = sourceCode.getText(expression);
|
|
52
|
-
if (Check.isJSXElementOrFragment(expression)) return exprText;
|
|
53
|
-
return `{${exprText}}`;
|
|
54
|
-
}
|
|
55
|
-
function escapeJsxText(text) {
|
|
56
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("{", "{").replaceAll("}", "}");
|
|
57
|
-
}
|
|
58
|
-
function getPropRemovalRange(context, prop) {
|
|
59
|
-
const { sourceCode } = context;
|
|
60
|
-
let start = prop.range[0];
|
|
61
|
-
const end = prop.range[1];
|
|
62
|
-
while (start > 0 && /\s/.test(sourceCode.text[start - 1] ?? "")) start--;
|
|
63
|
-
return [start, end];
|
|
64
|
-
}
|
|
65
|
-
function isDirectJsxChild(node) {
|
|
66
|
-
return Check.isJSXElementOrFragment(node.parent);
|
|
67
|
-
}
|
|
29
|
+
var version = "5.17.0";
|
|
68
30
|
|
|
69
31
|
//#endregion
|
|
70
32
|
//#region src/utils/create-rule.ts
|
|
@@ -73,6 +35,39 @@ function getDocsUrl(ruleName) {
|
|
|
73
35
|
}
|
|
74
36
|
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
75
37
|
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/utils/find-create-element-children-prop.ts
|
|
40
|
+
/**
|
|
41
|
+
* Finds the statically named `children` property in the props object of a `createElement` call.
|
|
42
|
+
* @param context The rule context
|
|
43
|
+
* @param node The `CallExpression` node to inspect
|
|
44
|
+
* @returns The `children` property node, or `null` when the call has no static `children` prop
|
|
45
|
+
*/
|
|
46
|
+
function findCreateElementChildrenProp(context, node) {
|
|
47
|
+
if (!core.isCreateElementCall(context, node)) return null;
|
|
48
|
+
const propsArg = node.arguments[1];
|
|
49
|
+
if (propsArg == null) return null;
|
|
50
|
+
const propsObject = Extract.unwrap(propsArg);
|
|
51
|
+
if (propsObject.type !== AST_NODE_TYPES.ObjectExpression) return null;
|
|
52
|
+
for (const prop of propsObject.properties) if (prop.type === AST_NODE_TYPES.Property && Extract.getStaticPropertyName(prop) === "children") return prop;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/utils/remove-jsx-attribute.ts
|
|
58
|
+
/**
|
|
59
|
+
* Builds a fix that removes a JSX attribute along with the whitespace before it.
|
|
60
|
+
* @param context The rule context
|
|
61
|
+
* @param fixer The rule fixer
|
|
62
|
+
* @param attribute The `JSXAttribute` node to remove
|
|
63
|
+
* @returns A fix that removes the attribute and its preceding whitespace
|
|
64
|
+
*/
|
|
65
|
+
function removeJsxAttribute(context, fixer, attribute) {
|
|
66
|
+
let start = attribute.range[0];
|
|
67
|
+
while (start > 0 && /\s/.test(context.sourceCode.text[start - 1] ?? "")) start--;
|
|
68
|
+
return fixer.removeRange([start, attribute.range[1]]);
|
|
69
|
+
}
|
|
70
|
+
|
|
76
71
|
//#endregion
|
|
77
72
|
//#region src/rules/no-children-prop-with-children/no-children-prop-with-children.ts
|
|
78
73
|
const RULE_NAME$7 = "no-children-prop-with-children";
|
|
@@ -96,14 +91,9 @@ var no_children_prop_with_children_default = createRule({
|
|
|
96
91
|
function create$7(context) {
|
|
97
92
|
return {
|
|
98
93
|
CallExpression(node) {
|
|
99
|
-
|
|
100
|
-
const [, propsArg, firstExtra] = node.arguments;
|
|
101
|
-
if (propsArg == null) return;
|
|
102
|
-
const propsObject = Extract.unwrap(propsArg);
|
|
103
|
-
if (propsObject.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
104
|
-
const childrenProp = findChildrenProperty(propsObject);
|
|
94
|
+
const childrenProp = findCreateElementChildrenProp(context, node);
|
|
105
95
|
if (childrenProp == null) return;
|
|
106
|
-
if (
|
|
96
|
+
if (node.arguments[2] == null) return;
|
|
107
97
|
context.report({
|
|
108
98
|
messageId: "default",
|
|
109
99
|
node: childrenProp
|
|
@@ -111,8 +101,7 @@ function create$7(context) {
|
|
|
111
101
|
},
|
|
112
102
|
JSXElement(node) {
|
|
113
103
|
const childrenProp = findAttribute(context, node, "children");
|
|
114
|
-
if (childrenProp == null) return;
|
|
115
|
-
if (!hasChildren(node)) return;
|
|
104
|
+
if (childrenProp == null || !hasChildren(node)) return;
|
|
116
105
|
if (childrenProp.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
117
106
|
context.report({
|
|
118
107
|
messageId: "default",
|
|
@@ -124,16 +113,14 @@ function create$7(context) {
|
|
|
124
113
|
messageId: "default",
|
|
125
114
|
node: childrenProp,
|
|
126
115
|
suggest: [{
|
|
127
|
-
fix(fixer)
|
|
128
|
-
const [start, end] = getPropRemovalRange(context, childrenProp);
|
|
129
|
-
return fixer.removeRange([start, end]);
|
|
130
|
-
},
|
|
116
|
+
fix: (fixer) => removeJsxAttribute(context, fixer, childrenProp),
|
|
131
117
|
messageId: "removeChildrenProp"
|
|
132
118
|
}, {
|
|
133
119
|
fix(fixer) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
120
|
+
const first = node.children.at(0);
|
|
121
|
+
const last = node.children.at(-1);
|
|
122
|
+
if (first == null || last == null) return [];
|
|
123
|
+
return fixer.removeRange([first.range[0], last.range[1]]);
|
|
137
124
|
},
|
|
138
125
|
messageId: "removeChildrenContent"
|
|
139
126
|
}]
|
|
@@ -164,12 +151,7 @@ var no_children_prop_default = createRule({
|
|
|
164
151
|
function create$6(context) {
|
|
165
152
|
return {
|
|
166
153
|
CallExpression(node) {
|
|
167
|
-
|
|
168
|
-
const [, propsArg] = node.arguments;
|
|
169
|
-
if (propsArg == null) return;
|
|
170
|
-
const propsObject = Extract.unwrap(propsArg);
|
|
171
|
-
if (propsObject.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
172
|
-
const childrenProp = findChildrenProperty(propsObject);
|
|
154
|
+
const childrenProp = findCreateElementChildrenProp(context, node);
|
|
173
155
|
if (childrenProp == null) return;
|
|
174
156
|
context.report({
|
|
175
157
|
messageId: "default",
|
|
@@ -186,7 +168,7 @@ function create$6(context) {
|
|
|
186
168
|
});
|
|
187
169
|
return;
|
|
188
170
|
}
|
|
189
|
-
const childrenText =
|
|
171
|
+
const childrenText = getChildrenText(context, childrenProp);
|
|
190
172
|
if (childrenText == null) {
|
|
191
173
|
context.report({
|
|
192
174
|
messageId: "default",
|
|
@@ -198,27 +180,60 @@ function create$6(context) {
|
|
|
198
180
|
messageId: "default",
|
|
199
181
|
node: childrenProp,
|
|
200
182
|
suggest: [{
|
|
201
|
-
fix(
|
|
202
|
-
const sourceCode = context.sourceCode;
|
|
203
|
-
const { openingElement } = node;
|
|
204
|
-
const [removeStart, removeEnd] = getPropRemovalRange(context, childrenProp);
|
|
205
|
-
if (openingElement.selfClosing) {
|
|
206
|
-
const tagName = sourceCode.getText(openingElement.name);
|
|
207
|
-
const selfCloseOffset = sourceCode.getText(openingElement).lastIndexOf("/>");
|
|
208
|
-
let wsStart = openingElement.range[0] + selfCloseOffset;
|
|
209
|
-
while (wsStart > removeEnd && /\s/.test(sourceCode.text[wsStart - 1])) wsStart--;
|
|
210
|
-
return [fixer.removeRange([removeStart, removeEnd]), fixer.replaceTextRange([wsStart, openingElement.range[1]], `>${childrenText}</${tagName}>`)];
|
|
211
|
-
}
|
|
212
|
-
const fixes = [fixer.removeRange([removeStart, removeEnd])];
|
|
213
|
-
if (node.closingElement != null) fixes.push(fixer.insertTextBefore(node.closingElement, childrenText));
|
|
214
|
-
return fixes;
|
|
215
|
-
},
|
|
183
|
+
fix: buildFix$1(context, node, childrenProp, childrenText),
|
|
216
184
|
messageId: "moveChildrenToContent"
|
|
217
185
|
}]
|
|
218
186
|
});
|
|
219
187
|
}
|
|
220
188
|
};
|
|
221
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Turns the value of a 'children' JSXAttribute into text usable as element content
|
|
192
|
+
* @param context The rule context
|
|
193
|
+
* @param attribute The 'children' JSXAttribute node
|
|
194
|
+
* @returns The text to insert as element content, or `null` when no usable value exists
|
|
195
|
+
*/
|
|
196
|
+
function getChildrenText(context, attribute) {
|
|
197
|
+
const { value } = attribute;
|
|
198
|
+
if (value?.type === AST_NODE_TYPES.Literal) return escapeJsxText(String(value.value));
|
|
199
|
+
if (value?.type === AST_NODE_TYPES.JSXExpressionContainer && value.expression.type !== AST_NODE_TYPES.JSXEmptyExpression) {
|
|
200
|
+
const exprText = context.sourceCode.getText(value.expression);
|
|
201
|
+
return Check.isJSXElementOrFragment(value.expression) ? exprText : `{${exprText}}`;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Escapes characters that would be parsed as markup in JSX text
|
|
207
|
+
* @param text The raw string value
|
|
208
|
+
* @returns The escaped text safe for insertion as JSX content
|
|
209
|
+
*/
|
|
210
|
+
function escapeJsxText(text) {
|
|
211
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("{", "{").replaceAll("}", "}");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Builds the fix that moves the 'children' prop value into the element's content
|
|
215
|
+
* @param context The rule context object
|
|
216
|
+
* @param node The JSXElement node being reported
|
|
217
|
+
* @param prop The 'children' JSXAttribute to remove
|
|
218
|
+
* @param childrenText The text to insert as element content
|
|
219
|
+
* @returns A fixer function that applies the changes
|
|
220
|
+
*/
|
|
221
|
+
function buildFix$1(context, node, prop, childrenText) {
|
|
222
|
+
return (fixer) => {
|
|
223
|
+
const sourceCode = context.sourceCode;
|
|
224
|
+
const { openingElement } = node;
|
|
225
|
+
const removePropFix = removeJsxAttribute(context, fixer, prop);
|
|
226
|
+
if (openingElement.selfClosing) {
|
|
227
|
+
const tagName = sourceCode.getText(openingElement.name);
|
|
228
|
+
const selfCloseOffset = sourceCode.getText(openingElement).lastIndexOf("/>");
|
|
229
|
+
let wsStart = openingElement.range[0] + selfCloseOffset;
|
|
230
|
+
while (wsStart > prop.range[1] && /\s/.test(sourceCode.text[wsStart - 1])) wsStart--;
|
|
231
|
+
return [removePropFix, fixer.replaceTextRange([wsStart, openingElement.range[1]], `>${childrenText}</${tagName}>`)];
|
|
232
|
+
}
|
|
233
|
+
if (node.closingElement == null) return [removePropFix];
|
|
234
|
+
return [removePropFix, fixer.insertTextBefore(node.closingElement, childrenText)];
|
|
235
|
+
};
|
|
236
|
+
}
|
|
222
237
|
|
|
223
238
|
//#endregion
|
|
224
239
|
//#region src/rules/no-comment-textnodes/no-comment-textnodes.ts
|
|
@@ -236,17 +251,14 @@ var no_comment_textnodes_default = createRule({
|
|
|
236
251
|
});
|
|
237
252
|
function create$5(context) {
|
|
238
253
|
function visit(node) {
|
|
239
|
-
if (!
|
|
254
|
+
if (!Check.isJSXElementOrFragment(node.parent)) return;
|
|
240
255
|
if (!/^\s*\/(?:\/|\*)/mu.test(context.sourceCode.getText(node))) return;
|
|
241
256
|
context.report({
|
|
242
257
|
messageId: "default",
|
|
243
258
|
node
|
|
244
259
|
});
|
|
245
260
|
}
|
|
246
|
-
return {
|
|
247
|
-
JSXText: visit,
|
|
248
|
-
Literal: visit
|
|
249
|
-
};
|
|
261
|
+
return { JSXText: visit };
|
|
250
262
|
}
|
|
251
263
|
|
|
252
264
|
//#endregion
|
|
@@ -256,7 +268,7 @@ var no_key_after_spread_default = createRule({
|
|
|
256
268
|
meta: {
|
|
257
269
|
type: "problem",
|
|
258
270
|
docs: { description: "Prevent patterns that cause deoptimization when using the automatic JSX runtime." },
|
|
259
|
-
messages: {
|
|
271
|
+
messages: { default: "Placing 'key' after spread props causes deoptimization when using the automatic JSX runtime. Put 'key' before any spread props." },
|
|
260
272
|
schema: []
|
|
261
273
|
},
|
|
262
274
|
name: RULE_NAME$4,
|
|
@@ -265,17 +277,16 @@ var no_key_after_spread_default = createRule({
|
|
|
265
277
|
});
|
|
266
278
|
function create$4(context) {
|
|
267
279
|
const { jsx } = core.getJsxConfig(context);
|
|
268
|
-
if (
|
|
280
|
+
if (jsx !== ts.JsxEmit.ReactJSX && jsx !== ts.JsxEmit.ReactJSXDev) return {};
|
|
269
281
|
return { JSXOpeningElement(node) {
|
|
270
|
-
let
|
|
271
|
-
for (const
|
|
282
|
+
let hasSpreadBefore = false;
|
|
283
|
+
for (const prop of node.attributes) {
|
|
272
284
|
if (prop.type === AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
273
|
-
|
|
285
|
+
hasSpreadBefore = true;
|
|
274
286
|
continue;
|
|
275
287
|
}
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
messageId: "noKeyAfterSpread",
|
|
288
|
+
if (hasSpreadBefore && getAttributeName(prop) === "key") context.report({
|
|
289
|
+
messageId: "default",
|
|
279
290
|
node: prop
|
|
280
291
|
});
|
|
281
292
|
}
|
|
@@ -302,15 +313,12 @@ var no_leaked_dollar_default = createRule({
|
|
|
302
313
|
defaultOptions: []
|
|
303
314
|
});
|
|
304
315
|
function create$3(context) {
|
|
305
|
-
|
|
306
|
-
* Visitor function for JSXElement and JSXFragment nodes
|
|
307
|
-
* @param node The JSXElement or JSXFragment node to be checked
|
|
308
|
-
*/
|
|
316
|
+
if (!context.sourceCode.text.includes("$")) return {};
|
|
309
317
|
function visit(node) {
|
|
310
318
|
for (const [index, child] of node.children.entries()) {
|
|
311
319
|
if (child.type !== AST_NODE_TYPES.JSXText || !child.value.endsWith("$")) continue;
|
|
312
320
|
if (node.children[index + 1]?.type !== AST_NODE_TYPES.JSXExpressionContainer) continue;
|
|
313
|
-
if (child.value === "$" && node.children.
|
|
321
|
+
if (child.value.trim() === "$" && node.children.every((sibling, siblingIndex) => siblingIndex === index || siblingIndex === index + 1 || isNonSubstantiveChild(sibling))) continue;
|
|
314
322
|
if (!context.sourceCode.getText(child).endsWith("$")) continue;
|
|
315
323
|
const dollarStart = child.range[1] - 1;
|
|
316
324
|
const dollarEnd = child.range[1];
|
|
@@ -335,6 +343,15 @@ function create$3(context) {
|
|
|
335
343
|
JSXFragment: visit
|
|
336
344
|
};
|
|
337
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Whether a JSX child carries no renderable content: padding whitespace, an
|
|
348
|
+
* empty expression (ex: `{}` or a comment), or an empty string expression.
|
|
349
|
+
* @param child The JSX child node to check.
|
|
350
|
+
*/
|
|
351
|
+
function isNonSubstantiveChild(child) {
|
|
352
|
+
if (isWhitespaceText(child) || isEmptyStringExpression(child)) return true;
|
|
353
|
+
return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
|
|
354
|
+
}
|
|
338
355
|
|
|
339
356
|
//#endregion
|
|
340
357
|
//#region src/rules/no-leaked-semicolon/no-leaked-semicolon.ts
|
|
@@ -343,6 +360,7 @@ var no_leaked_semicolon_default = createRule({
|
|
|
343
360
|
meta: {
|
|
344
361
|
type: "problem",
|
|
345
362
|
docs: { description: "Catches `;` at the start of JSX text nodes — typically from accidentally placing a statement-ending `;` inside JSX. The `;` \"leaks\" into the rendered output." },
|
|
363
|
+
fixable: "code",
|
|
346
364
|
hasSuggestions: true,
|
|
347
365
|
messages: {
|
|
348
366
|
default: "Leaked ';' in JSX. This ';' will be rendered as text nodes.",
|
|
@@ -354,30 +372,22 @@ var no_leaked_semicolon_default = createRule({
|
|
|
354
372
|
create: create$2,
|
|
355
373
|
defaultOptions: []
|
|
356
374
|
});
|
|
357
|
-
function hasLeakedSemicolon(text) {
|
|
358
|
-
return /^;[ \t]*(?:\r\n|\r|\n)/u.test(text);
|
|
359
|
-
}
|
|
360
375
|
function create$2(context) {
|
|
361
376
|
function visit(node) {
|
|
362
|
-
if (!
|
|
363
|
-
if (
|
|
364
|
-
const semicolonStart = node.range[0];
|
|
365
|
-
const semicolonEnd = node.range[0] + 1;
|
|
377
|
+
if (!Check.isJSXElementOrFragment(node.parent)) return;
|
|
378
|
+
if (!/^;+[ \t]*(?:\r\n|\r|\n)/u.test(context.sourceCode.getText(node))) return;
|
|
366
379
|
context.report({
|
|
367
380
|
messageId: "default",
|
|
368
381
|
node,
|
|
369
382
|
suggest: [{
|
|
370
383
|
fix(fixer) {
|
|
371
|
-
return fixer.removeRange([
|
|
384
|
+
return fixer.removeRange([node.range[0], node.range[0] + 1]);
|
|
372
385
|
},
|
|
373
386
|
messageId: "removeSemicolon"
|
|
374
387
|
}]
|
|
375
388
|
});
|
|
376
389
|
}
|
|
377
|
-
return {
|
|
378
|
-
JSXText: visit,
|
|
379
|
-
Literal: visit
|
|
380
|
-
};
|
|
390
|
+
return { JSXText: visit };
|
|
381
391
|
}
|
|
382
392
|
|
|
383
393
|
//#endregion
|
|
@@ -430,7 +440,6 @@ const schema = [{
|
|
|
430
440
|
var no_useless_fragment_default = createRule({
|
|
431
441
|
meta: {
|
|
432
442
|
type: "suggestion",
|
|
433
|
-
defaultOptions: [...defaultOptions],
|
|
434
443
|
docs: { description: "Disallows useless fragment elements." },
|
|
435
444
|
fixable: "code",
|
|
436
445
|
messages: { default: "A fragment {{reason}} is useless." },
|
|
@@ -441,88 +450,96 @@ var no_useless_fragment_default = createRule({
|
|
|
441
450
|
defaultOptions
|
|
442
451
|
});
|
|
443
452
|
function create(context, [option]) {
|
|
444
|
-
const
|
|
453
|
+
const options = {
|
|
454
|
+
allowEmptyFragment: option.allowEmptyFragment ?? false,
|
|
455
|
+
allowExpressions: option.allowExpressions ?? true
|
|
456
|
+
};
|
|
445
457
|
const jsxConfig = core.getJsxConfig(context);
|
|
446
|
-
/**
|
|
447
|
-
* Whether the fragment has too few meaningful children to justify its
|
|
448
|
-
* existence (the "contains less than two children" reason).
|
|
449
|
-
* @param node The fragment node to check.
|
|
450
|
-
*/
|
|
451
|
-
function isContentUseless(node) {
|
|
452
|
-
if (node.children.length === 0) return !allowEmptyFragment;
|
|
453
|
-
const insideJsx = Check.isJSXElementOrFragment(node.parent);
|
|
454
|
-
if (allowExpressions && !insideJsx && node.children.length === 1 && node.children[0]?.type === AST_NODE_TYPES.JSXText) return false;
|
|
455
|
-
const meaningful = getChildren(node);
|
|
456
|
-
if (meaningful.length === 0) return true;
|
|
457
|
-
if (meaningful.length > 1) return false;
|
|
458
|
-
if (meaningful[0]?.type === AST_NODE_TYPES.JSXExpressionContainer) return !allowExpressions;
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
463
|
-
* @param node The fragment node to check.
|
|
464
|
-
*/
|
|
465
|
-
function isSafeToFix(node) {
|
|
466
|
-
if (node.type === AST_NODE_TYPES.JSXElement && node.openingElement.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute)) return false;
|
|
467
|
-
if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
|
|
468
|
-
if (node.children.length === 0) return false;
|
|
469
|
-
return node.children.every((child) => {
|
|
470
|
-
if (Check.isJSXElementOrFragment(child)) return true;
|
|
471
|
-
return child.type === AST_NODE_TYPES.JSXText && isWhitespaceText(child);
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Build an autofix that unwraps the fragment, replacing it with its
|
|
476
|
-
* meaningful children content. Returns `null` when the fix is unsafe.
|
|
477
|
-
* @param node The fragment node to fix.
|
|
478
|
-
*/
|
|
479
|
-
function buildFix(node) {
|
|
480
|
-
if (!isSafeToFix(node)) return null;
|
|
481
|
-
return (fixer) => {
|
|
482
|
-
let text = "";
|
|
483
|
-
for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
|
|
484
|
-
const cleaned = collapseMultilineText(child.value);
|
|
485
|
-
if (cleaned != null) text += cleaned;
|
|
486
|
-
} else text += context.sourceCode.getText(child);
|
|
487
|
-
let start = node.range[0];
|
|
488
|
-
if (text === "" && node.children.length > 0) {
|
|
489
|
-
const lineStart = Math.max(context.sourceCode.text.lastIndexOf("\n", start - 1), context.sourceCode.text.lastIndexOf("\r", start - 1)) + 1;
|
|
490
|
-
if (/^[ \t]*$/u.test(context.sourceCode.text.slice(lineStart, start))) start = lineStart;
|
|
491
|
-
}
|
|
492
|
-
return fixer.replaceTextRange([start, node.range[1]], text);
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Inspect a fragment node and report if it is useless.
|
|
497
|
-
*
|
|
498
|
-
* A fragment may be reported for **two independent reasons** on the same
|
|
499
|
-
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
500
|
-
* and* "contains less than two children").
|
|
501
|
-
* @param node The fragment node to inspect.
|
|
502
|
-
*/
|
|
503
|
-
function visit(node) {
|
|
504
|
-
if (isHostElement(node.parent)) context.report({
|
|
505
|
-
data: { reason: "placed inside a host component" },
|
|
506
|
-
fix: buildFix(node),
|
|
507
|
-
messageId: "default",
|
|
508
|
-
node
|
|
509
|
-
});
|
|
510
|
-
if (isContentUseless(node)) context.report({
|
|
511
|
-
data: { reason: "contains less than two children" },
|
|
512
|
-
fix: buildFix(node),
|
|
513
|
-
messageId: "default",
|
|
514
|
-
node
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
458
|
return {
|
|
518
459
|
JSXElement(node) {
|
|
519
460
|
if (!isFragmentElement(node, jsxConfig.jsxFragmentFactory)) return;
|
|
520
461
|
if (hasAnyAttribute(context, node, ["key", "ref"])) return;
|
|
521
|
-
|
|
462
|
+
visitFragment(context, node, options);
|
|
522
463
|
},
|
|
523
464
|
JSXFragment(node) {
|
|
524
|
-
|
|
465
|
+
visitFragment(context, node, options);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Inspect a fragment node and report if it is useless.
|
|
471
|
+
*
|
|
472
|
+
* A fragment may be reported for **two independent reasons** on the same
|
|
473
|
+
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
474
|
+
* and "contains less than two children").
|
|
475
|
+
* @param context The rule context.
|
|
476
|
+
* @param node The fragment node to inspect.
|
|
477
|
+
* @param options The resolved rule options.
|
|
478
|
+
*/
|
|
479
|
+
function visitFragment(context, node, options) {
|
|
480
|
+
const reasons = [];
|
|
481
|
+
if (isHostElement(node.parent)) reasons.push("placed inside a host component");
|
|
482
|
+
if (isContentUseless(node, options)) reasons.push("contains less than two children");
|
|
483
|
+
if (reasons.length === 0) return;
|
|
484
|
+
const fix = buildFix(context, node);
|
|
485
|
+
for (const reason of reasons) context.report({
|
|
486
|
+
data: { reason },
|
|
487
|
+
fix,
|
|
488
|
+
messageId: "default",
|
|
489
|
+
node
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Whether the fragment has too few meaningful children to justify its
|
|
494
|
+
* existence (the "contains less than two children" reason).
|
|
495
|
+
* @param node The fragment node to check.
|
|
496
|
+
* @param options The resolved rule options.
|
|
497
|
+
*/
|
|
498
|
+
function isContentUseless(node, options) {
|
|
499
|
+
const { allowEmptyFragment, allowExpressions } = options;
|
|
500
|
+
if (node.children.length === 0) return !allowEmptyFragment;
|
|
501
|
+
const insideJsx = Check.isJSXElementOrFragment(node.parent);
|
|
502
|
+
if (allowExpressions && !insideJsx && node.children.length === 1 && node.children[0]?.type === AST_NODE_TYPES.JSXText) return false;
|
|
503
|
+
const meaningful = getChildren(node);
|
|
504
|
+
if (meaningful.length === 0) return true;
|
|
505
|
+
if (meaningful.length > 1) return false;
|
|
506
|
+
if (meaningful[0]?.type === AST_NODE_TYPES.JSXExpressionContainer) return !allowExpressions;
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
511
|
+
* @param node The fragment node to check.
|
|
512
|
+
*/
|
|
513
|
+
function isSafeToFix(node) {
|
|
514
|
+
if (node.type === AST_NODE_TYPES.JSXElement && node.openingElement.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute)) return false;
|
|
515
|
+
if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
|
|
516
|
+
if (node.children.length === 0) return false;
|
|
517
|
+
return node.children.every((child) => {
|
|
518
|
+
if (Check.isJSXElementOrFragment(child)) return true;
|
|
519
|
+
return child.type === AST_NODE_TYPES.JSXText && isWhitespaceText(child);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Build an autofix that unwraps the fragment, replacing it with its
|
|
524
|
+
* meaningful children content. Returns `null` when the fix is unsafe.
|
|
525
|
+
* @param context The rule context.
|
|
526
|
+
* @param node The fragment node to fix.
|
|
527
|
+
*/
|
|
528
|
+
function buildFix(context, node) {
|
|
529
|
+
if (!isSafeToFix(node)) return null;
|
|
530
|
+
return (fixer) => {
|
|
531
|
+
const sourceCode = context.sourceCode;
|
|
532
|
+
let text = "";
|
|
533
|
+
for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
|
|
534
|
+
const cleaned = collapseMultilineText(child.value);
|
|
535
|
+
if (cleaned != null) text += cleaned;
|
|
536
|
+
} else text += sourceCode.getText(child);
|
|
537
|
+
let start = node.range[0];
|
|
538
|
+
if (text === "" && node.children.length > 0) {
|
|
539
|
+
const lineStart = Math.max(sourceCode.text.lastIndexOf("\n", start - 1), sourceCode.text.lastIndexOf("\r", start - 1)) + 1;
|
|
540
|
+
if (/^[ \t]*$/u.test(sourceCode.text.slice(lineStart, start))) start = lineStart;
|
|
525
541
|
}
|
|
542
|
+
return fixer.replaceTextRange([start, node.range[1]], text);
|
|
526
543
|
};
|
|
527
544
|
}
|
|
528
545
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-jsx",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for React Flavored JSX rules.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@typescript-eslint/types": "^8.64.0",
|
|
41
41
|
"@typescript-eslint/utils": "^8.64.0",
|
|
42
|
-
"@eslint-react/ast": "5.
|
|
43
|
-
"@eslint-react/
|
|
44
|
-
"@eslint-react/
|
|
45
|
-
"@eslint-react/
|
|
46
|
-
"@eslint-react/
|
|
42
|
+
"@eslint-react/ast": "5.17.0",
|
|
43
|
+
"@eslint-react/core": "5.17.0",
|
|
44
|
+
"@eslint-react/eslint": "5.17.0",
|
|
45
|
+
"@eslint-react/jsx": "5.17.0",
|
|
46
|
+
"@eslint-react/shared": "5.17.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.2.17",
|