eslint-plugin-react-jsx 5.16.1 → 5.17.1

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 +678 -191
  2. package/package.json +7 -7
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, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isAttribute, 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.1";
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,19 +251,493 @@ 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
 
264
+ //#endregion
265
+ //#region ../../.pkgs/eff/dist/index.js
266
+ function not(predicate) {
267
+ return (data) => !predicate(data);
268
+ }
269
+ /**
270
+ * Applies a `pipe` method's variadic arguments to an initial value from left
271
+ * to right.
272
+ *
273
+ * **When to use**
274
+ *
275
+ * Use to implement a custom `.pipe(...)` method from JavaScript's `arguments`
276
+ * object.
277
+ *
278
+ * **Details**
279
+ *
280
+ * This helper is intended for implementing `Pipeable.pipe` methods that
281
+ * receive JavaScript's `arguments` object. With no functions it returns the
282
+ * original value; otherwise it feeds each result into the next function.
283
+ *
284
+ * **Example** (Implementing a pipe method)
285
+ *
286
+ * ```ts
287
+ * import { Pipeable } from "effect"
288
+ *
289
+ * class NumberBox {
290
+ * constructor(readonly value: number) {}
291
+ *
292
+ * pipe(..._fns: ReadonlyArray<(value: number) => number>): number {
293
+ * return Pipeable.pipeArguments(this.value, arguments) as number
294
+ * }
295
+ * }
296
+ *
297
+ * const result = new NumberBox(5).pipe(
298
+ * (n) => n + 2,
299
+ * (n) => n * 3
300
+ * )
301
+ * console.log(result) // 21
302
+ * ```
303
+ *
304
+ * @category combinators
305
+ * @since 2.0.0
306
+ */
307
+ const pipeArguments = (self, args) => {
308
+ switch (args.length) {
309
+ case 0: return self;
310
+ case 1: return args[0](self);
311
+ case 2: return args[1](args[0](self));
312
+ case 3: return args[2](args[1](args[0](self)));
313
+ case 4: return args[3](args[2](args[1](args[0](self))));
314
+ case 5: return args[4](args[3](args[2](args[1](args[0](self)))));
315
+ case 6: return args[5](args[4](args[3](args[2](args[1](args[0](self))))));
316
+ case 7: return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))));
317
+ case 8: return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))));
318
+ case 9: return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))));
319
+ default: {
320
+ let ret = self;
321
+ for (let i = 0, len = args.length; i < len; i++) ret = args[i](ret);
322
+ return ret;
323
+ }
324
+ }
325
+ };
326
+ /**
327
+ * Reusable prototype that implements `Pipeable.pipe`.
328
+ *
329
+ * **When to use**
330
+ *
331
+ * Use when classes or object prototypes can reuse this value when they need the
332
+ * standard pipe implementation backed by `pipeArguments`.
333
+ *
334
+ * @category prototypes
335
+ * @since 3.15.0
336
+ */
337
+ const Prototype = { pipe() {
338
+ return pipeArguments(this, arguments);
339
+ } };
340
+ /**
341
+ * Provides a base constructor whose instances implement the standard `Pipeable.pipe`
342
+ * method.
343
+ *
344
+ * **When to use**
345
+ *
346
+ * Use when you need to define a class that supports Effect-style method
347
+ * chaining through `.pipe(...)`.
348
+ *
349
+ * @category constructors
350
+ * @since 3.15.0
351
+ */
352
+ const Class = (function() {
353
+ function PipeableBase() {}
354
+ PipeableBase.prototype = Prototype;
355
+ return PipeableBase;
356
+ })();
357
+ /**
358
+ * Provides small helpers for defining and reusing TypeScript functions.
359
+ *
360
+ * The main helpers are `pipe` and `flow` for left-to-right composition and
361
+ * `dual` for APIs that support both direct and pipe-friendly call styles. The
362
+ * module also contains small identity, constant, tuple, type-level, and
363
+ * memoization helpers used across the library.
364
+ *
365
+ * @since 2.0.0
366
+ */
367
+ /**
368
+ * Creates a function that can be called in data-first style or data-last
369
+ * (`pipe`-friendly) style.
370
+ *
371
+ * **When to use**
372
+ *
373
+ * Use to expose one implementation through both direct and `pipe`-friendly
374
+ * call styles.
375
+ *
376
+ * **Details**
377
+ *
378
+ * Pass either the arity of the uncurried function or a predicate that decides
379
+ * whether the current call is data-first. Arity is the common case. Use a
380
+ * predicate when optional arguments make arity ambiguous.
381
+ *
382
+ * **Example** (Selecting data-first or data-last style by arity)
383
+ *
384
+ * ```ts
385
+ * import { Function, pipe } from "effect"
386
+ *
387
+ * const sum = Function.dual<
388
+ * (that: number) => (self: number) => number,
389
+ * (self: number, that: number) => number
390
+ * >(2, (self, that) => self + that)
391
+ *
392
+ * console.log(sum(2, 3)) // 5
393
+ * console.log(pipe(2, sum(3))) // 5
394
+ * ```
395
+ *
396
+ * **Example** (Defining overloads with call signatures)
397
+ *
398
+ * ```ts
399
+ * import { Function, pipe } from "effect"
400
+ *
401
+ * const sum: {
402
+ * (that: number): (self: number) => number
403
+ * (self: number, that: number): number
404
+ * } = Function.dual(2, (self: number, that: number): number => self + that)
405
+ *
406
+ * console.log(sum(2, 3)) // 5
407
+ * console.log(pipe(2, sum(3))) // 5
408
+ * ```
409
+ *
410
+ * **Example** (Selecting data-first or data-last style with a predicate)
411
+ *
412
+ * ```ts
413
+ * import { Function, pipe } from "effect"
414
+ *
415
+ * const sum = Function.dual<
416
+ * (that: number) => (self: number) => number,
417
+ * (self: number, that: number) => number
418
+ * >(
419
+ * (args) => args.length === 2,
420
+ * (self, that) => self + that
421
+ * )
422
+ *
423
+ * console.log(sum(2, 3)) // 5
424
+ * console.log(pipe(2, sum(3))) // 5
425
+ * ```
426
+ *
427
+ * @category combinators
428
+ * @since 2.0.0
429
+ */
430
+ const dual = function(arity, body) {
431
+ if (typeof arity === "function") return function() {
432
+ return arity(arguments) ? body.apply(this, arguments) : ((self) => body(self, ...arguments));
433
+ };
434
+ switch (arity) {
435
+ case 0:
436
+ case 1: throw new RangeError(`Invalid arity ${arity}`);
437
+ case 2: return function(a, b) {
438
+ if (arguments.length >= 2) return body(a, b);
439
+ return function(self) {
440
+ return body(self, a);
441
+ };
442
+ };
443
+ case 3: return function(a, b, c) {
444
+ if (arguments.length >= 3) return body(a, b, c);
445
+ return function(self) {
446
+ return body(self, a, b);
447
+ };
448
+ };
449
+ default: return function() {
450
+ if (arguments.length >= arity) return body.apply(this, arguments);
451
+ const args = arguments;
452
+ return function(self) {
453
+ return body(self, ...args);
454
+ };
455
+ };
456
+ }
457
+ };
458
+ /**
459
+ * Returns its input argument unchanged.
460
+ *
461
+ * **When to use**
462
+ *
463
+ * Use to return a value unchanged where a function is required.
464
+ *
465
+ * **Example** (Returning the same value)
466
+ *
467
+ * ```ts
468
+ * import { identity } from "effect"
469
+ * import * as assert from "node:assert"
470
+ *
471
+ * assert.deepStrictEqual(identity(5), 5)
472
+ * ```
473
+ *
474
+ * @category combinators
475
+ * @since 2.0.0
476
+ */
477
+ const identity = (a) => a;
478
+ /**
479
+ * Returns the input value with a different static type.
480
+ *
481
+ * **When to use**
482
+ *
483
+ * Use when you need an explicit type-level cast and accept that the value is
484
+ * returned unchanged at runtime.
485
+ *
486
+ * **Gotchas**
487
+ *
488
+ * This is a type-level cast only; it performs no runtime validation or
489
+ * conversion.
490
+ *
491
+ * @see {@link satisfies} for checking assignability without changing the resulting type
492
+ *
493
+ * @category utility types
494
+ * @since 4.0.0
495
+ */
496
+ const cast = identity;
497
+ /**
498
+ * Creates a zero-argument function that always returns the provided value.
499
+ *
500
+ * **When to use**
501
+ *
502
+ * Use when you need a thunk or callback that returns the same value on every
503
+ * invocation.
504
+ *
505
+ * **Example** (Creating a constant thunk)
506
+ *
507
+ * ```ts
508
+ * import { Function } from "effect"
509
+ * import * as assert from "node:assert"
510
+ *
511
+ * const constNull = Function.constant(null)
512
+ *
513
+ * assert.deepStrictEqual(constNull(), null)
514
+ * assert.deepStrictEqual(constNull(), null)
515
+ * ```
516
+ *
517
+ * @category constructors
518
+ * @since 2.0.0
519
+ */
520
+ const constant = (value) => () => value;
521
+ /**
522
+ * Returns `true` when called.
523
+ *
524
+ * **When to use**
525
+ *
526
+ * Use when you need a thunk that returns `true` on every invocation.
527
+ *
528
+ * **Example** (Returning true from a thunk)
529
+ *
530
+ * ```ts
531
+ * import { Function } from "effect"
532
+ * import * as assert from "node:assert"
533
+ *
534
+ * assert.deepStrictEqual(Function.constTrue(), true)
535
+ * ```
536
+ *
537
+ * @category constants
538
+ * @since 2.0.0
539
+ */
540
+ const constTrue = constant(true);
541
+ /**
542
+ * Returns `false` when called.
543
+ *
544
+ * **When to use**
545
+ *
546
+ * Use when you need a thunk that returns `false` on every invocation.
547
+ *
548
+ * **Example** (Returning false from a thunk)
549
+ *
550
+ * ```ts
551
+ * import { Function } from "effect"
552
+ * import * as assert from "node:assert"
553
+ *
554
+ * assert.deepStrictEqual(Function.constFalse(), false)
555
+ * ```
556
+ *
557
+ * @category constants
558
+ * @since 2.0.0
559
+ */
560
+ const constFalse = constant(false);
561
+ /**
562
+ * Returns `null` when called.
563
+ *
564
+ * **When to use**
565
+ *
566
+ * Use when you need a thunk that returns `null` on every invocation.
567
+ *
568
+ * **Example** (Returning null from a thunk)
569
+ *
570
+ * ```ts
571
+ * import { Function } from "effect"
572
+ * import * as assert from "node:assert"
573
+ *
574
+ * assert.deepStrictEqual(Function.constNull(), null)
575
+ * ```
576
+ *
577
+ * @category constants
578
+ * @since 2.0.0
579
+ */
580
+ const constNull = constant(null);
581
+ /**
582
+ * Returns `undefined` when called.
583
+ *
584
+ * **When to use**
585
+ *
586
+ * Use when you need a thunk that returns `undefined` on every invocation.
587
+ *
588
+ * **Example** (Returning undefined from a thunk)
589
+ *
590
+ * ```ts
591
+ * import { Function } from "effect"
592
+ * import * as assert from "node:assert"
593
+ *
594
+ * assert.deepStrictEqual(Function.constUndefined(), undefined)
595
+ * ```
596
+ *
597
+ * @category constants
598
+ * @since 2.0.0
599
+ */
600
+ const constUndefined = constant(void 0);
601
+ /**
602
+ * Composes two functions, `ab` and `bc` into a single function that takes in an argument `a` of type `A` and returns a result of type `C`.
603
+ * The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
604
+ *
605
+ * **When to use**
606
+ *
607
+ * Use to compose exactly two unary functions into a reusable unary function.
608
+ *
609
+ * **Example** (Composing two functions)
610
+ *
611
+ * ```ts
612
+ * import { Function } from "effect"
613
+ * import * as assert from "node:assert"
614
+ *
615
+ * const increment = (n: number) => n + 1
616
+ * const square = (n: number) => n * n
617
+ *
618
+ * assert.strictEqual(Function.compose(increment, square)(2), 9)
619
+ * ```
620
+ *
621
+ * @see {@link flow} for composing a left-to-right sequence of functions
622
+ * @see {@link pipe} for applying a value through a left-to-right sequence immediately
623
+ *
624
+ * @category combinators
625
+ * @since 2.0.0
626
+ */
627
+ const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
628
+ /**
629
+ * Marks an impossible branch by accepting a `never` value and returning any
630
+ * type.
631
+ *
632
+ * **When to use**
633
+ *
634
+ * Use when you need a return value in a branch that exhaustive checks prove
635
+ * cannot be reached.
636
+ *
637
+ * **Gotchas**
638
+ *
639
+ * Calling `absurd` throws, because a value of type `never` should be
640
+ * impossible at runtime.
641
+ *
642
+ * **Example** (Handling impossible values)
643
+ *
644
+ * ```ts
645
+ * import { absurd } from "effect"
646
+ *
647
+ * const handleNever = (value: never) => {
648
+ * return absurd(value) // This will throw an error if called
649
+ * }
650
+ * ```
651
+ *
652
+ * @category utility types
653
+ * @since 2.0.0
654
+ */
655
+ const absurd = (_) => {
656
+ throw new Error("Called `absurd` function which should be uncallable");
657
+ };
658
+ /**
659
+ * Creates a compile-time placeholder for a value of any type.
660
+ *
661
+ * **When to use**
662
+ *
663
+ * Use as a temporary typed placeholder while developing incomplete code.
664
+ *
665
+ * **Gotchas**
666
+ *
667
+ * `hole` is intended for temporary development use. If the placeholder is
668
+ * evaluated at runtime, it throws.
669
+ *
670
+ * **Example** (Creating a development placeholder)
671
+ *
672
+ * ```ts
673
+ * import { hole } from "effect"
674
+ *
675
+ * // Intentionally not called: `hole` throws if the placeholder is evaluated.
676
+ * const buildUser = (id: number): { readonly id: number; readonly name: string } => ({
677
+ * id,
678
+ * name: hole<string>()
679
+ * })
680
+ *
681
+ * console.log(typeof buildUser) // "function"
682
+ * ```
683
+ *
684
+ * @category utility types
685
+ * @since 2.0.0
686
+ */
687
+ const hole = cast(absurd);
688
+ /**
689
+ * Drops the longest prefix of elements from an array that satisfy the given predicate.
690
+ *
691
+ * Supports both data-first and data-last (`pipe`-friendly) call styles.
692
+ *
693
+ * @param pred - The predicate to test each element with.
694
+ * @returns A new array without the matching prefix.
695
+ * @example
696
+ * ```ts
697
+ * import * as assert from "node:assert"
698
+ * import { dropWhile, pipe } from "@local/eff"
699
+ *
700
+ * // data-first
701
+ * assert.deepStrictEqual(dropWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [3, 2, 1])
702
+ *
703
+ * // data-last
704
+ * assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], dropWhile((n: number) => n < 3)), [3, 2, 1])
705
+ * ```
706
+ * @category array
707
+ */
708
+ const dropWhile = dual(2, (xs, pred) => {
709
+ const len = xs.length;
710
+ let idx = 0;
711
+ while (idx < len && pred(xs[idx])) idx++;
712
+ return xs.slice(idx);
713
+ });
714
+ /**
715
+ * Takes the longest prefix of elements from an array that satisfy the given predicate.
716
+ *
717
+ * Supports both data-first and data-last (`pipe`-friendly) call styles.
718
+ *
719
+ * @param pred - The predicate to test each element with.
720
+ * @returns A new array containing only the matching prefix.
721
+ * @example
722
+ * ```ts
723
+ * import * as assert from "node:assert"
724
+ * import { pipe, takeWhile } from "@local/eff"
725
+ *
726
+ * // data-first
727
+ * assert.deepStrictEqual(takeWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [1, 2])
728
+ *
729
+ * // data-last
730
+ * assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], takeWhile((n: number) => n < 3)), [1, 2])
731
+ * ```
732
+ * @category array
733
+ */
734
+ const takeWhile = dual(2, (xs, pred) => {
735
+ const len = xs.length;
736
+ let idx = 0;
737
+ while (idx < len && pred(xs[idx])) idx++;
738
+ return xs.slice(0, idx);
739
+ });
740
+
252
741
  //#endregion
253
742
  //#region src/rules/no-key-after-spread/no-key-after-spread.ts
254
743
  const RULE_NAME$4 = "no-key-after-spread";
@@ -256,7 +745,7 @@ var no_key_after_spread_default = createRule({
256
745
  meta: {
257
746
  type: "problem",
258
747
  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." },
748
+ messages: { default: "Placing 'key' after spread props causes deoptimization when using the automatic JSX runtime. Put 'key' before any spread props." },
260
749
  schema: []
261
750
  },
262
751
  name: RULE_NAME$4,
@@ -265,20 +754,12 @@ var no_key_after_spread_default = createRule({
265
754
  });
266
755
  function create$4(context) {
267
756
  const { jsx } = core.getJsxConfig(context);
268
- if (!(jsx === ts.JsxEmit.ReactJSX || jsx === ts.JsxEmit.ReactJSXDev)) return {};
757
+ if (jsx !== ts.JsxEmit.ReactJSX && jsx !== ts.JsxEmit.ReactJSXDev) return {};
269
758
  return { JSXOpeningElement(node) {
270
- let firstSpreadPropIndex = null;
271
- for (const [index, prop] of node.attributes.entries()) {
272
- if (prop.type === AST_NODE_TYPES.JSXSpreadAttribute) {
273
- firstSpreadPropIndex ??= index;
274
- continue;
275
- }
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",
279
- node: prop
280
- });
281
- }
759
+ for (const n of dropWhile(node.attributes, not(Check.is(AST_NODE_TYPES.JSXSpreadAttribute))).filter(isAttribute("key"))) context.report({
760
+ messageId: "default",
761
+ node: n
762
+ });
282
763
  } };
283
764
  }
284
765
 
@@ -302,15 +783,12 @@ var no_leaked_dollar_default = createRule({
302
783
  defaultOptions: []
303
784
  });
304
785
  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
- */
786
+ if (!context.sourceCode.text.includes("$")) return {};
309
787
  function visit(node) {
310
788
  for (const [index, child] of node.children.entries()) {
311
789
  if (child.type !== AST_NODE_TYPES.JSXText || !child.value.endsWith("$")) continue;
312
790
  if (node.children[index + 1]?.type !== AST_NODE_TYPES.JSXExpressionContainer) continue;
313
- if (child.value === "$" && node.children.length === 2) continue;
791
+ if (child.value.trim() === "$" && node.children.every((sibling, siblingIndex) => siblingIndex === index || siblingIndex === index + 1 || isNonSubstantiveChild(sibling))) continue;
314
792
  if (!context.sourceCode.getText(child).endsWith("$")) continue;
315
793
  const dollarStart = child.range[1] - 1;
316
794
  const dollarEnd = child.range[1];
@@ -335,6 +813,15 @@ function create$3(context) {
335
813
  JSXFragment: visit
336
814
  };
337
815
  }
816
+ /**
817
+ * Whether a JSX child carries no renderable content: padding whitespace, an
818
+ * empty expression (ex: `{}` or a comment), or an empty string expression.
819
+ * @param child The JSX child node to check.
820
+ */
821
+ function isNonSubstantiveChild(child) {
822
+ if (isWhitespaceText(child) || isEmptyStringExpression(child)) return true;
823
+ return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
824
+ }
338
825
 
339
826
  //#endregion
340
827
  //#region src/rules/no-leaked-semicolon/no-leaked-semicolon.ts
@@ -343,6 +830,7 @@ var no_leaked_semicolon_default = createRule({
343
830
  meta: {
344
831
  type: "problem",
345
832
  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." },
833
+ fixable: "code",
346
834
  hasSuggestions: true,
347
835
  messages: {
348
836
  default: "Leaked ';' in JSX. This ';' will be rendered as text nodes.",
@@ -354,30 +842,22 @@ var no_leaked_semicolon_default = createRule({
354
842
  create: create$2,
355
843
  defaultOptions: []
356
844
  });
357
- function hasLeakedSemicolon(text) {
358
- return /^;[ \t]*(?:\r\n|\r|\n)/u.test(text);
359
- }
360
845
  function create$2(context) {
361
846
  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;
847
+ if (!Check.isJSXElementOrFragment(node.parent)) return;
848
+ if (!/^;+[ \t]*(?:\r\n|\r|\n)/u.test(context.sourceCode.getText(node))) return;
366
849
  context.report({
367
850
  messageId: "default",
368
851
  node,
369
852
  suggest: [{
370
853
  fix(fixer) {
371
- return fixer.removeRange([semicolonStart, semicolonEnd]);
854
+ return fixer.removeRange([node.range[0], node.range[0] + 1]);
372
855
  },
373
856
  messageId: "removeSemicolon"
374
857
  }]
375
858
  });
376
859
  }
377
- return {
378
- JSXText: visit,
379
- Literal: visit
380
- };
860
+ return { JSXText: visit };
381
861
  }
382
862
 
383
863
  //#endregion
@@ -430,7 +910,6 @@ const schema = [{
430
910
  var no_useless_fragment_default = createRule({
431
911
  meta: {
432
912
  type: "suggestion",
433
- defaultOptions: [...defaultOptions],
434
913
  docs: { description: "Disallows useless fragment elements." },
435
914
  fixable: "code",
436
915
  messages: { default: "A fragment {{reason}} is useless." },
@@ -441,88 +920,96 @@ var no_useless_fragment_default = createRule({
441
920
  defaultOptions
442
921
  });
443
922
  function create(context, [option]) {
444
- const { allowEmptyFragment = false, allowExpressions = true } = option;
923
+ const options = {
924
+ allowEmptyFragment: option.allowEmptyFragment ?? false,
925
+ allowExpressions: option.allowExpressions ?? true
926
+ };
445
927
  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
928
  return {
518
929
  JSXElement(node) {
519
930
  if (!isFragmentElement(node, jsxConfig.jsxFragmentFactory)) return;
520
931
  if (hasAnyAttribute(context, node, ["key", "ref"])) return;
521
- visit(node);
932
+ visitFragment(context, node, options);
522
933
  },
523
934
  JSXFragment(node) {
524
- visit(node);
935
+ visitFragment(context, node, options);
936
+ }
937
+ };
938
+ }
939
+ /**
940
+ * Inspect a fragment node and report if it is useless.
941
+ *
942
+ * A fragment may be reported for **two independent reasons** on the same
943
+ * node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
944
+ * and "contains less than two children").
945
+ * @param context The rule context.
946
+ * @param node The fragment node to inspect.
947
+ * @param options The resolved rule options.
948
+ */
949
+ function visitFragment(context, node, options) {
950
+ const reasons = [];
951
+ if (isHostElement(node.parent)) reasons.push("placed inside a host component");
952
+ if (isContentUseless(node, options)) reasons.push("contains less than two children");
953
+ if (reasons.length === 0) return;
954
+ const fix = buildFix(context, node);
955
+ for (const reason of reasons) context.report({
956
+ data: { reason },
957
+ fix,
958
+ messageId: "default",
959
+ node
960
+ });
961
+ }
962
+ /**
963
+ * Whether the fragment has too few meaningful children to justify its
964
+ * existence (the "contains less than two children" reason).
965
+ * @param node The fragment node to check.
966
+ * @param options The resolved rule options.
967
+ */
968
+ function isContentUseless(node, options) {
969
+ const { allowEmptyFragment, allowExpressions } = options;
970
+ if (node.children.length === 0) return !allowEmptyFragment;
971
+ const insideJsx = Check.isJSXElementOrFragment(node.parent);
972
+ if (allowExpressions && !insideJsx && node.children.length === 1 && node.children[0]?.type === AST_NODE_TYPES.JSXText) return false;
973
+ const meaningful = getChildren(node);
974
+ if (meaningful.length === 0) return true;
975
+ if (meaningful.length > 1) return false;
976
+ if (meaningful[0]?.type === AST_NODE_TYPES.JSXExpressionContainer) return !allowExpressions;
977
+ return true;
978
+ }
979
+ /**
980
+ * Whether it is safe to auto-fix the fragment by unwrapping it.
981
+ * @param node The fragment node to check.
982
+ */
983
+ function isSafeToFix(node) {
984
+ if (node.type === AST_NODE_TYPES.JSXElement && node.openingElement.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute)) return false;
985
+ if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
986
+ if (node.children.length === 0) return false;
987
+ return node.children.every((child) => {
988
+ if (Check.isJSXElementOrFragment(child)) return true;
989
+ return child.type === AST_NODE_TYPES.JSXText && isWhitespaceText(child);
990
+ });
991
+ }
992
+ /**
993
+ * Build an autofix that unwraps the fragment, replacing it with its
994
+ * meaningful children content. Returns `null` when the fix is unsafe.
995
+ * @param context The rule context.
996
+ * @param node The fragment node to fix.
997
+ */
998
+ function buildFix(context, node) {
999
+ if (!isSafeToFix(node)) return null;
1000
+ return (fixer) => {
1001
+ const sourceCode = context.sourceCode;
1002
+ let text = "";
1003
+ for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
1004
+ const cleaned = collapseMultilineText(child.value);
1005
+ if (cleaned != null) text += cleaned;
1006
+ } else text += sourceCode.getText(child);
1007
+ let start = node.range[0];
1008
+ if (text === "" && node.children.length > 0) {
1009
+ const lineStart = Math.max(sourceCode.text.lastIndexOf("\n", start - 1), sourceCode.text.lastIndexOf("\r", start - 1)) + 1;
1010
+ if (/^[ \t]*$/u.test(sourceCode.text.slice(lineStart, start))) start = lineStart;
525
1011
  }
1012
+ return fixer.replaceTextRange([start, node.range[1]], text);
526
1013
  };
527
1014
  }
528
1015
 
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.1",
4
4
  "description": "ESLint React's ESLint plugin for React Flavored JSX rules.",
5
5
  "keywords": [
6
6
  "react",
@@ -39,18 +39,18 @@
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.1",
43
+ "@eslint-react/eslint": "5.17.1",
44
+ "@eslint-react/core": "5.17.1",
45
+ "@eslint-react/jsx": "5.17.1",
46
+ "@eslint-react/shared": "5.17.1"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/react": "^19.2.17",
50
50
  "@types/react-dom": "^19.2.3",
51
51
  "dedent": "^1.7.2",
52
52
  "eslint": "^10.7.0",
53
- "tsdown": "^0.22.7",
53
+ "tsdown": "^0.22.8",
54
54
  "typescript": "6.0.3",
55
55
  "@local/configs": "0.0.0",
56
56
  "@local/eff": "0.0.0"