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.
Files changed (2) hide show
  1. package/dist/index.js +202 -185
  2. 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.16.1";
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("{", "&#123;").replaceAll("}", "&#125;");
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
- if (!core.isCreateElementCall(context, node)) return;
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 (firstExtra == null) return;
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 range = getChildrenContentRange(node);
135
- if (range == null) return [];
136
- return fixer.removeRange(range);
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
- if (!core.isCreateElementCall(context, node)) return;
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 = getChildrenPropText(context, childrenProp);
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(fixer) {
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("{", "&#123;").replaceAll("}", "&#125;");
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 (!isDirectJsxChild(node)) return;
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: { noKeyAfterSpread: "Placing 'key' after spread props causes deoptimization when using the automatic JSX runtime. Put 'key' before any spread props." },
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 (!(jsx === ts.JsxEmit.ReactJSX || jsx === ts.JsxEmit.ReactJSXDev)) return {};
280
+ if (jsx !== ts.JsxEmit.ReactJSX && jsx !== ts.JsxEmit.ReactJSXDev) return {};
269
281
  return { JSXOpeningElement(node) {
270
- let firstSpreadPropIndex = null;
271
- for (const [index, prop] of node.attributes.entries()) {
282
+ let hasSpreadBefore = false;
283
+ for (const prop of node.attributes) {
272
284
  if (prop.type === AST_NODE_TYPES.JSXSpreadAttribute) {
273
- firstSpreadPropIndex ??= index;
285
+ hasSpreadBefore = true;
274
286
  continue;
275
287
  }
276
- if (firstSpreadPropIndex == null) continue;
277
- if (prop.name.type === AST_NODE_TYPES.JSXIdentifier && prop.name.name === "key" && index > firstSpreadPropIndex) context.report({
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.length === 2) continue;
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 (!isDirectJsxChild(node)) return;
363
- if (!hasLeakedSemicolon(context.sourceCode.getText(node))) return;
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([semicolonStart, semicolonEnd]);
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 { allowEmptyFragment = false, allowExpressions = true } = option;
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
- visit(node);
462
+ visitFragment(context, node, options);
522
463
  },
523
464
  JSXFragment(node) {
524
- visit(node);
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.16.1",
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.16.1",
43
- "@eslint-react/eslint": "5.16.1",
44
- "@eslint-react/jsx": "5.16.1",
45
- "@eslint-react/shared": "5.16.1",
46
- "@eslint-react/core": "5.16.1"
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",