@eslint-react/jsx 5.8.18 → 5.8.19

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 (3) hide show
  1. package/dist/index.d.ts +146 -382
  2. package/dist/index.js +144 -353
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { P, match } from "ts-pattern";
6
6
 
7
7
  //#region src/collapse-multiline-text.ts
8
8
  /**
9
- * Collapse a multiline JSX text string following React's whitespace rules.
9
+ * Collapse a multiline JSX text string following React's whitespace rules
10
10
  *
11
11
  * This mirrors Babel's `cleanJSXElementLiteralChild` algorithm:
12
12
  * 1. Split the raw text into lines.
@@ -14,19 +14,17 @@ import { P, match } from "ts-pattern";
14
14
  * 3. Trim leading spaces on non-first lines and trailing spaces on non-last lines.
15
15
  * 4. Collapse tabs into spaces.
16
16
  * 5. Append a single space after each non-last non-empty line.
17
- *
18
- * @param text - The raw JSX text string to collapse.
19
- * @returns The collapsed string, or `null` if the text contains only whitespace.
20
- *
17
+ * @param text The raw JSX text string to collapse
18
+ * @returns The collapsed string, or `null` if the text contains only whitespace
21
19
  * @see https://github.com/babel/babel/blob/main/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
22
20
  */
23
21
  function collapseMultilineText(text) {
24
22
  const lines = text.split(/\r\n|\n|\r/);
25
23
  let lastNonEmptyLine = 0;
26
- for (let i = 0; i < lines.length; i++) if (/[^ \t]/.exec(lines[i]) != null) lastNonEmptyLine = i;
24
+ for (let i = 0; i < lines.length; i++) if (/[^ \t]/.exec(lines[i] ?? "") != null) lastNonEmptyLine = i;
27
25
  let str = "";
28
26
  for (let i = 0; i < lines.length; i++) {
29
- const line = lines[i];
27
+ const line = lines[i] ?? "";
30
28
  const isFirstLine = i === 0;
31
29
  const isLastLine = i === lines.length - 1;
32
30
  const isLastNonEmptyLine = i === lastNonEmptyLine;
@@ -38,32 +36,20 @@ function collapseMultilineText(text) {
38
36
  str += trimmedLine;
39
37
  }
40
38
  }
41
- return str || null;
39
+ return str === "" ? null : str;
42
40
  }
43
41
 
44
42
  //#endregion
45
43
  //#region src/get-attribute-name.ts
46
44
  /**
47
- * Get the stringified name of a `JSXAttribute` node.
45
+ * Get the stringified name of a `JSXAttribute` node
48
46
  *
49
47
  * Handles both simple identifiers and namespaced names:
50
- *
51
- * - `className` -> `"className"`
52
- * - `aria-label` -> `"aria-label"`
53
- * - `xml:space` -> `"xml:space"`
54
- *
55
- * @param node - A `JSXAttribute` AST node.
56
- * @returns The attribute name as a plain string.
57
- *
58
- * @example
59
- * ```ts
60
- * import { getAttributeName } from "@eslint-react/jsx";
61
- *
62
- * // Inside a rule visitor:
63
- * JSXAttribute(node) {
64
- * const name = getAttributeName(node); // "className"
65
- * }
66
- * ```
48
+ * - `className` -> `"className"`
49
+ * - `aria-label` -> `"aria-label"`
50
+ * - `xml:space` -> `"xml:space"`
51
+ * @param node A `JSXAttribute` AST node
52
+ * @returns The attribute name as a plain string
67
53
  */
68
54
  function getAttributeName(node) {
69
55
  if (node.name.type === AST_NODE_TYPES.JSXIdentifier) return node.name.name;
@@ -73,37 +59,24 @@ function getAttributeName(node) {
73
59
  //#endregion
74
60
  //#region src/find-attribute.ts
75
61
  /**
76
- * Find a JSX attribute (or spread attribute containing the property) by name
77
- * on a given element.
78
- *
79
- * Returns the **last** matching attribute to mirror React's behaviour where
80
- * later props win, or `undefined` when the attribute is not present.
81
- *
82
- * Spread attributes are resolved when possible: if the spread argument is an
83
- * identifier that resolves to an object expression, the object's properties
84
- * are searched for a matching key.
62
+ * Find a JSX attribute (or spread attribute containing the property) by name on a given element
85
63
  *
86
- * @param context - The ESLint rule context (needed for variable resolution in
87
- * spread attributes).
88
- * @param element - The `JSXElement` node to search.
89
- * @param name - The attribute name to look for (e.g. `"className"`).
90
- * @returns The matching `JSXAttribute` or `JSXSpreadAttribute`, or
91
- * `undefined` when not found.
64
+ * Returns the last matching attribute to mirror React's behavior where later props win,
65
+ * or `undefined` when the attribute is not present.
92
66
  *
93
- * @example
94
- * ```ts
95
- * const attr = findAttribute(context, node, "sandbox");
96
- * if (attr != null) {
97
- * // attribute (or spread containing it) exists on the element
98
- * }
99
- * ```
67
+ * Spread attributes are resolved when possible: if the spread argument is an identifier
68
+ * that resolves to an object expression, the object's properties are searched for a matching key.
69
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes)
70
+ * @param element The `JSXElement` node to search
71
+ * @param name The attribute name to look for (ex: "className")
72
+ * @returns The matching `JSXAttribute` or `JSXSpreadAttribute`, or `undefined` when not found
100
73
  */
101
74
  function findAttribute(context, element, name) {
102
- function findProperty(of, named) {
103
- for (const property of of) {
104
- if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) === named) return property;
75
+ function findProperty(properties, name) {
76
+ for (const property of properties) {
77
+ if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) === name) return property;
105
78
  if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
106
- const found = findProperty(property.argument.properties, named);
79
+ const found = findProperty(property.argument.properties, name);
107
80
  if (found != null) return found;
108
81
  }
109
82
  }
@@ -126,27 +99,14 @@ function findAttribute(context, element, name) {
126
99
  //#endregion
127
100
  //#region src/find-parent-attribute.ts
128
101
  /**
129
- * Walk **up** the AST from `node` to find the nearest ancestor that is a
130
- * `JSXAttribute` and (optionally) passes a predicate.
131
- *
132
- * This is useful when a rule visitor enters a deeplynested node (e.g. a
133
- * `Literal` inside an expression container) and needs to know which JSX
134
- * attribute it belongs to.
135
- *
136
- * @param node - The starting node for the upward search.
137
- * @param test - Optional predicate to filter candidate `JSXAttribute` nodes.
138
- * When omitted every `JSXAttribute` ancestor matches.
139
- * @returns The first matching `JSXAttribute` ancestor, or `null` if none is
140
- * found before reaching the root.
141
- *
142
- * @example
143
- * ```ts
144
- * // Inside a Literal visitor, find the owning attribute:
145
- * const attr = findParentAttribute(literalNode);
146
- * if (attr != null) {
147
- * console.log(getAttributeName(attr));
148
- * }
149
- * ```
102
+ * Walk up the AST from `node` to find the nearest ancestor that is a `JSXAttribute`
103
+ * and (optionally) passes a predicate
104
+ *
105
+ * This is useful when a rule visitor enters a deeply nested node (ex: a `Literal`
106
+ * inside an expression container) and needs to know which JSX attribute it belongs to.
107
+ * @param node The starting node for the upward search
108
+ * @param test Optional predicate to filter candidate `JSXAttribute` nodes. When omitted every `JSXAttribute` ancestor matches
109
+ * @returns The first matching `JSXAttribute` ancestor, or `null` if none is found before reaching the root
150
110
  */
151
111
  function findParentAttribute(node, test = () => true) {
152
112
  const guard = (n) => {
@@ -159,28 +119,14 @@ function findParentAttribute(node, test = () => true) {
159
119
  //#region src/resolve-attribute-value.ts
160
120
  /**
161
121
  * Resolve the value of a JSX attribute (or spread attribute) into a
162
- * {@link JsxAttributeValue} descriptor that can be inspected further.
163
- *
164
- * This is the lowlevel building block it operates on a single attribute
165
- * node that the caller has already located. For the higherlevel "find by
166
- * name **and** resolve" combo, see {@link getAttributeValue}.
167
- *
168
- * @param context - The ESLint rule context (needed for scope look‑ups).
169
- * @param attribute - A `JSXAttribute` or `JSXSpreadAttribute` node.
170
- * @returns A discriminated‑union descriptor of the attribute's value.
171
- *
172
- * @example
173
- * ```ts
174
- * import { findAttribute, resolveAttributeValue } from "@eslint-react/jsx";
175
- *
176
- * const attr = findAttribute(context, element, "sandbox");
177
- * if (attr != null) {
178
- * const value = resolveAttributeValue(context, attr);
179
- * if (value.kind === "literal") {
180
- * console.log(value.toStatic());
181
- * }
182
- * }
183
- * ```
122
+ * {@link JsxAttributeValue} descriptor that can be inspected further
123
+ *
124
+ * This is the low-level building block; it operates on a single attribute
125
+ * node that the caller has already located. For the higher-level "find by
126
+ * name and resolve" combo, see {@link getAttributeValue}.
127
+ * @param context The ESLint rule context (needed for scope look-ups)
128
+ * @param attribute A `JSXAttribute` or `JSXSpreadAttribute` node
129
+ * @returns A discriminated-union descriptor of the attribute's value
184
130
  */
185
131
  function resolveAttributeValue(context, attribute) {
186
132
  if (attribute.type === AST_NODE_TYPES.JSXAttribute) return resolveJsxAttribute(context, attribute);
@@ -259,32 +205,19 @@ function resolveJsxSpreadAttribute(context, node) {
259
205
  //#endregion
260
206
  //#region src/get-attribute-static-value.ts
261
207
  /**
262
- * Find an attribute by name on a JSX element and collapse its value to a
263
- * plain JavaScript value in a single step.
208
+ * Find an attribute by name on a JSX element and collapse its value to a plain
209
+ * JavaScript value in a single step
264
210
  *
265
211
  * This is a convenience composition of {@link findAttribute} ->
266
- * {@link resolveAttributeValue} -> `toStatic()`, with automatic handling
267
- * of the `spreadProps` case (extracts the named property from the spread
268
- * object).
269
- *
270
- * Returns `undefined` when the attribute is absent **or** when its value
271
- * cannot be statically determined.
272
- *
273
- * @param context - The ESLint rule context.
274
- * @param element - The `JSXElement` node to inspect.
275
- * @param name - The attribute name to look up (e.g. `"className"`).
276
- * @returns The static value of the attribute, or `undefined`.
277
- *
278
- * @example
279
- * ```ts
280
- * // <iframe sandbox="allow-scripts" />
281
- * const sandbox = getAttributeStaticValue(context, node, "sandbox");
282
- * // -> "allow-scripts"
283
- *
284
- * // <button type={dynamicVar} />
285
- * const type = getAttributeStaticValue(context, node, "type");
286
- * // -> undefined (cannot be resolved statically)
287
- * ```
212
+ * {@link resolveAttributeValue} -> `toStatic()`, with automatic handling of the
213
+ * `spreadProps` case (extracts the named property from the spread object).
214
+ *
215
+ * Returns `undefined` when the attribute is absent or when its value cannot be
216
+ * statically determined.
217
+ * @param context The ESLint rule context
218
+ * @param element The `JSXElement` node to inspect
219
+ * @param name The attribute name to look up (ex: "className")
220
+ * @returns The static value of the attribute, or `undefined`
288
221
  */
289
222
  function getAttributeStaticValue(context, element, name) {
290
223
  const attr = findAttribute(context, element, name);
@@ -297,99 +230,62 @@ function getAttributeStaticValue(context, element, name) {
297
230
  //#endregion
298
231
  //#region src/get-attribute-value.ts
299
232
  /**
300
- * Find an attribute by name on a JSX element **and** resolve its value in a
301
- * single call.
233
+ * Find an attribute by name on a JSX element and resolve its value in a single call
302
234
  *
303
235
  * This is a convenience composition of {@link findAttribute} and
304
236
  * {@link resolveAttributeValue} that eliminates the most common two-step
305
- * pattern in lint rules:
306
- *
307
- * ```ts
308
- * const attr = findAttribute(context, element, name);
309
- * if (attr == null) return;
310
- * const value = resolveAttributeValue(context, attr);
311
- * ```
312
- *
313
- * @param context - The ESLint rule context.
314
- * @param element - The `JSXElement` node to search.
315
- * @param name - The attribute name to look up (e.g. `"className"`).
316
- * @returns A {@link JsxAttributeValue} descriptor, or `undefined` when the
317
- * attribute is not present on the element.
318
- *
319
- * @example
320
- * ```ts
321
- * const value = getAttributeValue(context, node, "sandbox");
322
- * if (value?.kind === "literal") {
323
- * console.log(value.toStatic()); // the literal value
324
- * }
325
- * ```
237
+ * pattern in lint rules.
238
+ * @param context The ESLint rule context
239
+ * @param element The `JSXElement` node to search
240
+ * @param name The attribute name to look up (ex: "className")
241
+ * @returns A {@link JsxAttributeValue} descriptor, or `null` when the attribute is not present on the element
326
242
  */
327
243
  function getAttributeValue(context, element, name) {
328
244
  const attr = findAttribute(context, element, name);
329
- if (attr == null) return void 0;
245
+ if (attr == null) return null;
330
246
  return resolveAttributeValue(context, attr);
331
247
  }
332
248
 
333
249
  //#endregion
334
250
  //#region src/is-whitespace.ts
335
251
  /**
336
- * Check whether a JSX child node is **whitespace padding** that React would
337
- * trim away during rendering.
252
+ * Check whether a JSX child node is whitespace padding that React would
253
+ * trim away during rendering
338
254
  *
339
255
  * A child is considered whitespace padding when it is a `JSXText` node whose
340
256
  * content is empty after applying React's whitespace normalization
341
257
  * (see {@link collapseMultilineText}, modelled after Babel's
342
258
  * `cleanJSXElementLiteralChild`). This is the whitespace that appears between
343
- * JSX tags purely for formatting:
344
- *
345
- * ```jsx
346
- * <div>
347
- * <span /> ← the text between </span> and the next tag is padding
348
- * <span />
349
- * </div>
350
- * ```
351
- *
352
- * @param node - A JSX child node.
353
- * @returns `true` when the node is purely formatting whitespace.
259
+ * JSX tags purely for formatting.
260
+ * @param node A JSX child node
261
+ * @returns `true` when the node is purely formatting whitespace
354
262
  */
355
263
  function isWhitespace(node) {
356
264
  if (node.type !== AST_NODE_TYPES.JSXText) return false;
357
265
  return collapseMultilineText(node.value) == null && node.value.includes("\n");
358
266
  }
359
267
  /**
360
- * Check whether a JSX child node is **any** whitespaceonly text.
268
+ * Check whether a JSX child node is any whitespace-only text
361
269
  *
362
- * This is a looser variant of {@link isWhitespace} it matches every
270
+ * This is a looser variant of {@link isWhitespace}; it matches every
363
271
  * `JSXText` node whose raw content is empty after trimming, regardless of
364
272
  * whether it contains a newline.
365
- *
366
- * @param node - A JSX child node.
367
- * @returns `true` when the node is a whitespace‑only `JSXText`.
273
+ * @param node A JSX child node
274
+ * @returns `true` when the node is a whitespace-only `JSXText`
368
275
  */
369
276
  function isWhitespaceText(node) {
370
277
  if (node.type !== AST_NODE_TYPES.JSXText) return false;
371
278
  return node.raw.trim() === "";
372
279
  }
373
280
  /**
374
- * Check whether a JSX child node is an **empty string expression** (`{""}`).
281
+ * Check whether a JSX child node is an empty string expression (`{""}`)
375
282
  *
376
283
  * React's reconciler and SSR renderer explicitly skip empty strings,
377
284
  * producing no DOM node (see `ReactChildFiber.js` and `ReactFizzConfigDOM.js`).
378
285
  * Such expressions are therefore treated as non-rendered children, in the same
379
286
  * way as whitespace padding.
380
- *
381
- * @param node - A JSX child node.
382
- * @returns `true` when the node is a `{""}` expression container.
383
- *
384
- * @example
385
- * ```ts
386
- * import { isEmptyStringExpression } from "@eslint-react/jsx";
387
- *
388
- * // <div>{""}</div> -> the expression container is an empty string expression
389
- * const meaningful = element.children.filter(
390
- * (child) => !isEmptyStringExpression(child),
391
- * );
392
- * ```
287
+ * @param node A JSX child node
288
+ * @returns `true` when the node is a `{""}` expression container
393
289
  */
394
290
  function isEmptyStringExpression(node) {
395
291
  if (node.type !== AST_NODE_TYPES.JSXExpressionContainer) return false;
@@ -401,7 +297,7 @@ function isEmptyStringExpression(node) {
401
297
  //#endregion
402
298
  //#region src/get-children.ts
403
299
  /**
404
- * Get the **meaningful** children of a JSX element or fragment.
300
+ * Get the meaningful children of a JSX element or fragment
405
301
  *
406
302
  * Mirrors Babel's `buildChildren` helper:
407
303
  * 1. Iterate over `element.children`.
@@ -409,23 +305,8 @@ function isEmptyStringExpression(node) {
409
305
  * 3. Skip `JSXExpressionContainer` nodes whose expression is empty.
410
306
  * 4. Skip `JSXEmptyExpression` nodes.
411
307
  * 5. Collect everything else.
412
- *
413
- * @param element - A `JSXElement` or `JSXFragment` node.
414
- * @returns An array of children nodes that contribute to rendered output.
415
- *
416
- * @example
417
- * ```ts
418
- * import { getChildren } from "@eslint-react/jsx";
419
- *
420
- * // <div>
421
- * // <span />
422
- * // </div>
423
- * //
424
- * // Raw children: [JSXText("\n "), JSXElement(<span />), JSXText("\n")]
425
- * // getChildren: [JSXElement(<span />)]
426
- *
427
- * const meaningful = getChildren(node);
428
- * ```
308
+ * @param element A `JSXElement` or `JSXFragment` node
309
+ * @returns An array of children nodes that contribute to rendered output
429
310
  */
430
311
  function getChildren(element) {
431
312
  const elements = [];
@@ -450,34 +331,34 @@ function getChildren(element) {
450
331
  //#endregion
451
332
  //#region src/get-element-type.ts
452
333
  /**
453
- * Get the string representation of a JSX element's type.
454
- *
455
- * - `<div>` -> `"div"`
456
- * - `<Foo.Bar>` -> `"Foo.Bar"`
457
- * - `<React.Fragment>` -> `"React.Fragment"`
458
- * - `<></>` -> `""`
459
- *
460
- * @param node - A `JSXElement` or `JSXFragment` node.
461
- * @returns The fully-qualified element type string.
334
+ * Get the string representation of a JSX element's type
335
+ *
336
+ * - `<div>` -> `"div"`
337
+ * - `<Foo.Bar>` -> `"Foo.Bar"`
338
+ * - `<React.Fragment>` -> `"React.Fragment"`
339
+ * - `<></>` -> `""`
340
+ * @param node A `JSXElement` or `JSXFragment` node
341
+ * @returns The fully-qualified element type string
462
342
  */
463
343
  function getElementFullType(node) {
464
344
  if (node.type === AST_NODE_TYPES.JSXFragment) return "";
465
345
  function getQualifiedName(node) {
466
- if (node.type === AST_NODE_TYPES.JSXIdentifier) return node.name;
467
- if (node.type === AST_NODE_TYPES.JSXNamespacedName) return node.namespace.name + ":" + node.name.name;
468
- return getQualifiedName(node.object) + "." + getQualifiedName(node.property);
346
+ switch (node.type) {
347
+ case AST_NODE_TYPES.JSXIdentifier: return node.name;
348
+ case AST_NODE_TYPES.JSXNamespacedName: return node.namespace.name + ":" + node.name.name;
349
+ default: return getQualifiedName(node.object) + "." + getQualifiedName(node.property);
350
+ }
469
351
  }
470
352
  return getQualifiedName(node.openingElement.name);
471
353
  }
472
354
  /**
473
- * Get the **self name** (last dot-separated segment) of a JSX element type.
355
+ * Get the self name (last dot-separated segment) of a JSX element type
474
356
  *
475
- * - `<Foo.Bar.Baz>` -> `"Baz"`
476
- * - `<div>` -> `"div"`
477
- * - `<></>` -> `""`
478
- *
479
- * @param node - A `JSXElement` or `JSXFragment` node.
480
- * @returns The last segment of the element type, or `""` for fragments.
357
+ * - `<Foo.Bar.Baz>` -> `"Baz"`
358
+ * - `<div>` -> `"div"`
359
+ * - `<></>` -> `""`
360
+ * @param node A `JSXElement` or `JSXFragment` node
361
+ * @returns The last segment of the element type, or `""` for fragments
481
362
  */
482
363
  function getElementSelfType(node) {
483
364
  return getElementFullType(node).split(".").at(-1) ?? "";
@@ -486,27 +367,16 @@ function getElementSelfType(node) {
486
367
  //#endregion
487
368
  //#region src/has-any-attribute.ts
488
369
  /**
489
- * Check whether a JSX element carries **at least one** of the given attributes.
370
+ * Check whether a JSX element carries at least one of the given attributes
490
371
  *
491
372
  * This is a batch variant of {@link hasAttribute} for the common pattern of
492
- * short-circuiting on multiple prop names:
493
- *
494
- * ```ts
495
- * // before
496
- * if (hasAttribute(ctx, el, "key")) return;
497
- * if (hasAttribute(ctx, el, "ref")) return;
498
- *
499
- * // after
500
- * if (hasAnyAttribute(ctx, el, ["key", "ref"])) return;
501
- * ```
373
+ * short-circuiting on multiple prop names.
502
374
  *
503
375
  * Spread attributes are taken into account (see {@link findAttribute}).
504
- *
505
- * @param context - The ESLint rule context (needed for variable resolution in
506
- * spread attributes).
507
- * @param element - The `JSXElement` node to inspect.
508
- * @param names - The attribute names to look for.
509
- * @returns `true` when **at least one** of the attributes is present.
376
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes)
377
+ * @param element The `JSXElement` node to inspect
378
+ * @param names The attribute names to look for
379
+ * @returns `true` when at least one of the attributes is present
510
380
  */
511
381
  function hasAnyAttribute(context, element, names) {
512
382
  return names.some((name) => findAttribute(context, element, name) != null);
@@ -515,28 +385,17 @@ function hasAnyAttribute(context, element, names) {
515
385
  //#endregion
516
386
  //#region src/has-attribute.ts
517
387
  /**
518
- * Check whether a JSX element carries a given attribute (prop).
388
+ * Check whether a JSX element carries a given attribute (prop)
519
389
  *
520
390
  * This is a thin convenience wrapper around {@link findAttribute} for the
521
391
  * common case where you only need a boolean answer.
522
392
  *
523
393
  * Spread attributes are taken into account: `<Comp {...{ disabled: true }} />`
524
394
  * will report `true` for `"disabled"`.
525
- *
526
- * @param context - The ESLint rule context (needed for variable resolution in
527
- * spread attributes).
528
- * @param element - The `JSXElement` node to inspect.
529
- * @param name - The attribute name to look for (e.g. `"className"`).
530
- * @returns `true` when the attribute is present on the element.
531
- *
532
- * @example
533
- * ```ts
534
- * import { hasAttribute } from "@eslint-react/jsx";
535
- *
536
- * if (hasAttribute(context, node, "key")) {
537
- * // element has a `key` prop
538
- * }
539
- * ```
395
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes)
396
+ * @param element The `JSXElement` node to inspect
397
+ * @param name The attribute name to look for (ex: "className")
398
+ * @returns `true` when the attribute is present on the element
540
399
  */
541
400
  function hasAttribute(context, element, name) {
542
401
  return findAttribute(context, element, name) != null;
@@ -545,12 +404,11 @@ function hasAttribute(context, element, name) {
545
404
  //#endregion
546
405
  //#region src/has-children.ts
547
406
  /**
548
- * Check whether a JSX element (or fragment) has **meaningful** children
549
- * that is, at least one child that is not purely whitespace text or an empty
550
- * string expression.
407
+ * Check whether a JSX element (or fragment) has meaningful children, that is,
408
+ * at least one child that is not purely whitespace text or an empty string expression
551
409
  *
552
410
  * A `JSXText` child whose `raw` content is empty after trimming is considered
553
- * non-meaningful because it is typically a code-formatting artefact
411
+ * non-meaningful because it is typically a code-formatting artifact
554
412
  * (indentation between tags). While React's client renderer preserves these
555
413
  * nodes as text nodes, they rarely represent intentionally rendered content.
556
414
  *
@@ -558,32 +416,14 @@ function hasAttribute(context, element, name) {
558
416
  * non-meaningful because React's reconciler and SSR renderer explicitly skip
559
417
  * empty strings, producing no DOM node.
560
418
  *
561
- * Unlike {@link getChildren} which only filters whitespace that contains a
562
- * newline this check treats **any** whitespace-only text as non-meaningful
563
- * (see {@link isWhitespaceText}). As a result `hasChildren(node)` is **not**
419
+ * Unlike {@link getChildren} (which only filters whitespace that contains a
420
+ * newline) this check treats any whitespace-only text as non-meaningful
421
+ * (see {@link isWhitespaceText}). As a result `hasChildren(node)` is not
564
422
  * always equal to `getChildren(node).length > 0`: they differ for
565
423
  * whitespace-only children that have no newline, such as `<div> </div>` or
566
424
  * `<div>\t\t</div>`. Choose the API that matches your rule's intent.
567
- *
568
- * @param element - A `JSXElement` or `JSXFragment` node.
569
- * @returns `true` when the element has at least one meaningful child.
570
- *
571
- * @example
572
- * ```ts
573
- * import { hasChildren } from "@eslint-react/jsx";
574
- *
575
- * // <div>hello</div> -> true
576
- * // <div> {expr} </div> -> true
577
- * // <div> </div> -> false (whitespace-only)
578
- * // <div> -> false (whitespace-only, with newlines)
579
- * // </div>
580
- * // <div></div> -> false (no children at all)
581
- * // <div>{""}</div> -> false (empty string expression)
582
- *
583
- * if (hasChildren(node)) {
584
- * // element renders visible content
585
- * }
586
- * ```
425
+ * @param element A `JSXElement` or `JSXFragment` node
426
+ * @returns `true` when the element has at least one meaningful child
587
427
  */
588
428
  function hasChildren(element) {
589
429
  if (element.children.length === 0) return false;
@@ -593,28 +433,16 @@ function hasChildren(element) {
593
433
  //#endregion
594
434
  //#region src/has-every-attribute.ts
595
435
  /**
596
- * Check whether a JSX element carries **all** of the given attributes (props).
436
+ * Check whether a JSX element carries all of the given attributes (props)
597
437
  *
598
438
  * This is a batch variant of {@link hasAttribute} for the common pattern
599
439
  * where a rule needs to verify that a set of required props are all present.
600
440
  *
601
441
  * Spread attributes are taken into account (see {@link findAttribute}).
602
- *
603
- * @param context - The ESLint rule context (needed for variable resolution in
604
- * spread attributes).
605
- * @param element - The `JSXElement` node to inspect.
606
- * @param names - The attribute names to look for.
607
- * @returns `true` when **every** name in `names` is present on the element.
608
- *
609
- * @example
610
- * ```ts
611
- * import { hasEveryAttribute } from "@eslint-react/jsx";
612
- *
613
- * // Ensure both `alt` and `src` are provided on an <img>
614
- * if (hasEveryAttribute(context, node, ["alt", "src"])) {
615
- * // element has both props
616
- * }
617
- * ```
442
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes)
443
+ * @param element The `JSXElement` node to inspect
444
+ * @param names The attribute names to look for
445
+ * @returns `true` when every name in `names` is present on the element
618
446
  */
619
447
  function hasEveryAttribute(context, element, names) {
620
448
  return names.every((name) => findAttribute(context, element, name) != null);
@@ -624,74 +452,44 @@ function hasEveryAttribute(context, element, names) {
624
452
  //#region src/is-element.ts
625
453
  /**
626
454
  * Check whether a node is a `JSXElement` (or `JSXFragment`) and optionally
627
- * matches a given test.
455
+ * matches a given test
628
456
  *
629
457
  * Modelled after
630
458
  * [`hast-util-is-element`](https://github.com/syntax-tree/hast-util-is-element):
631
459
  * the `test` parameter controls what counts as a match.
632
460
  *
633
- * When called **without** a test, the function acts as a simple type-guard
461
+ * When called without a test, the function acts as a simple type-guard
634
462
  * for `JSXElement | JSXFragment`.
635
- *
636
- * @param node - The AST node to test.
637
- * @param test - Optional test to match the element type against.
638
- * @returns `true` when the node is a matching JSX element.
639
- *
640
- * @example
641
- * ```ts
642
- * import { isElement } from "@eslint-react/jsx";
643
- *
644
- * // Type-guard only — any JSX element or fragment
645
- * if (isElement(node)) { … }
646
- *
647
- * // Match a single tag name
648
- * if (isElement(node, "iframe")) { … }
649
- *
650
- * // Match one of several tag names
651
- * if (isElement(node, ["button", "input", "select"])) { … }
652
- *
653
- * // Custom predicate
654
- * if (isElement(node, (type) => type.endsWith(".Provider"))) { … }
655
- * ```
463
+ * @param node The AST node to test
464
+ * @param test Optional test to match the element type against
465
+ * @returns `true` when the node is a matching JSX element
656
466
  */
657
467
  function isElement(node, test) {
658
468
  if (node == null) return false;
659
469
  if (node.type !== AST_NODE_TYPES.JSXElement && node.type !== AST_NODE_TYPES.JSXFragment) return false;
660
470
  if (test == null) return true;
661
471
  const elementType = getElementFullType(node);
662
- if (typeof test === "string") return elementType === test;
663
- if (typeof test === "function") return test(elementType, node);
664
- return test.includes(elementType);
472
+ switch (typeof test) {
473
+ case "string": return elementType === test;
474
+ case "function": return test(elementType, node);
475
+ default: return test.includes(elementType);
476
+ }
665
477
  }
666
478
 
667
479
  //#endregion
668
480
  //#region src/is-fragment-element.ts
669
481
  /**
670
- * Check whether a node is a React **Fragment** element.
482
+ * Check whether a node is a React Fragment element
671
483
  *
672
- * Recognises both the shorthand `<>…</>` syntax (`JSXFragment`) and the
484
+ * Recognizes both the shorthand `<>...</>` syntax (`JSXFragment`) and the
673
485
  * explicit `<Fragment>` / `<React.Fragment>` form (`JSXElement`).
674
486
  *
675
- * The comparison is performed against the **self name** (last dotseparated
676
- * segment) of both the node and the configured factory, so
677
- * `<React.Fragment>` matches `"React.Fragment"` and `<Fragment>` matches
678
- * `"Fragment"`.
679
- *
680
- * @param node - The AST node to test.
681
- * @param jsxFragmentFactory - The configured fragment factory string
682
- * (e.g. `"React.Fragment"`). Defaults to
683
- * `"React.Fragment"`.
684
- * @returns `true` when the node represents a React Fragment.
685
- *
686
- * @example
687
- * ```ts
688
- * // Using the default factory
689
- * if (isFragmentElement(node)) { … }
690
- *
691
- * // With a custom factory from jsxConfig
692
- * const config = getJsxConfig(context);
693
- * if (isFragmentElement(node, config.jsxFragmentFactory)) { … }
694
- * ```
487
+ * The comparison is performed against the self name (last dot-separated
488
+ * segment) of both the node and the configured factory, so `<React.Fragment>`
489
+ * matches `"React.Fragment"` and `<Fragment>` matches `"Fragment"`.
490
+ * @param node The AST node to test
491
+ * @param jsxFragmentFactory The configured fragment factory string (ex: "React.Fragment")
492
+ * @returns `true` when the node represents a React Fragment
695
493
  */
696
494
  function isFragmentElement(node, jsxFragmentFactory = "React.Fragment") {
697
495
  if (node.type === AST_NODE_TYPES.JSXFragment) return true;
@@ -703,23 +501,13 @@ function isFragmentElement(node, jsxFragmentFactory = "React.Fragment") {
703
501
  //#endregion
704
502
  //#region src/is-host-element.ts
705
503
  /**
706
- * Check whether a node is a **host** (intrinsic / DOM) element.
504
+ * Check whether a node is a host (intrinsic / DOM) element
707
505
  *
708
506
  * A host element is a `JSXElement` whose tag name is a plain `JSXIdentifier`
709
- * starting with a lowercase letter the same heuristic React uses to
507
+ * starting with a lowercase letter, the same heuristic React uses to
710
508
  * distinguish `<div>` from `<MyComponent>`.
711
- *
712
- * @param node - The AST node to test.
713
- * @returns `true` when the node is a `JSXElement` with a lowercase tag name.
714
- *
715
- * @example
716
- * ```ts
717
- * // <div className="box" /> -> true
718
- * // <span /> -> true
719
- * // <MyComponent /> -> false
720
- * // <Foo.Bar /> -> false
721
- * isHostElement(node);
722
- * ```
509
+ * @param node The AST node to test
510
+ * @returns `true` when the node is a `JSXElement` with a lowercase tag name
723
511
  */
724
512
  function isHostElement(node) {
725
513
  return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
@@ -727,6 +515,9 @@ function isHostElement(node) {
727
515
 
728
516
  //#endregion
729
517
  //#region src/jsx-detection-hint.ts
518
+ /**
519
+ * Hints for JSX detection
520
+ */
730
521
  const JsxDetectionHint = {
731
522
  None: 0n,
732
523
  DoNotIncludeJsxWithNullValue: 1n << 0n,
@@ -742,9 +533,9 @@ const JsxDetectionHint = {
742
533
  RequireBothBranchesOfConditionalExpressionToBeJsx: 1n << 10n
743
534
  };
744
535
  /**
745
- * Default JSX detection configuration.
536
+ * Default JSX detection hint
746
537
  *
747
- * Skips number, bigint, boolean, string, and undefined literals
538
+ * Skips number, bigint, boolean, string, and undefined literals,
748
539
  * the value types that are commonly returned alongside JSX in React
749
540
  * components but are not themselves renderable elements.
750
541
  */