@eslint-react/jsx 5.17.1 → 5.17.3

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 +194 -228
  2. package/dist/index.js +610 -657
  3. package/package.json +8 -7
package/dist/index.js CHANGED
@@ -2,470 +2,7 @@ import { Traverse } from "@eslint-react/ast";
2
2
  import { resolve } from "@eslint-react/var";
3
3
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
4
4
  import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
5
- import { P, match } from "ts-pattern";
6
5
 
7
- //#region src/collapse-multiline-text.ts
8
- /**
9
- * Collapse a multiline JSX text string following React's whitespace rules.
10
- *
11
- * This mirrors Babel's `cleanJSXElementLiteralChild` algorithm:
12
- * 1. Split the raw text into lines.
13
- * 2. Find the last non-empty line.
14
- * 3. Trim leading spaces on non-first lines and trailing spaces on non-last lines.
15
- * 4. Collapse tabs into spaces.
16
- * 5. Append a single space after each non-last non-empty line.
17
- * @param text The raw JSX text string to collapse.
18
- * @returns The collapsed string, or `null` if the text contains only whitespace.
19
- * @see https://github.com/babel/babel/blob/main/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
20
- */
21
- function collapseMultilineText(text) {
22
- const lines = text.split(/\r\n|\n|\r/);
23
- let lastNonEmptyLine = 0;
24
- for (let i = 0; i < lines.length; i++) if (/[^ \t]/.exec(lines[i] ?? "") != null) lastNonEmptyLine = i;
25
- let str = "";
26
- for (let i = 0; i < lines.length; i++) {
27
- const line = lines[i] ?? "";
28
- const isFirstLine = i === 0;
29
- const isLastLine = i === lines.length - 1;
30
- const isLastNonEmptyLine = i === lastNonEmptyLine;
31
- let trimmedLine = line.replace(/\t/g, " ");
32
- if (!isFirstLine) trimmedLine = trimmedLine.replace(/^ +/, "");
33
- if (!isLastLine) trimmedLine = trimmedLine.replace(/ +$/, "");
34
- if (trimmedLine.length > 0) {
35
- if (!isLastNonEmptyLine) trimmedLine += " ";
36
- str += trimmedLine;
37
- }
38
- }
39
- return str === "" ? null : str;
40
- }
41
-
42
- //#endregion
43
- //#region src/get-attribute-name.ts
44
- /**
45
- * Get the stringified name of a `JSXAttribute` node.
46
- *
47
- * Handles both simple identifiers and namespaced names:
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.
53
- */
54
- function getAttributeName(node) {
55
- if (node.name.type === AST_NODE_TYPES.JSXIdentifier) return node.name.name;
56
- return node.name.namespace.name + ":" + node.name.name.name;
57
- }
58
-
59
- //#endregion
60
- //#region src/find-attribute.ts
61
- /**
62
- * Find a JSX attribute (or spread attribute containing the property) by name on a given element.
63
- *
64
- * Returns the last matching attribute to mirror React's behavior where later props win,
65
- * or `undefined` when the attribute is not present.
66
- *
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
- * Nested object expressions and nested spread identifiers are also resolved.
70
- * @param context The ESLint rule context (needed for variable resolution in spread attributes).
71
- * @param element The `JSXElement` node to search.
72
- * @param name The attribute name to look for (ex: "className").
73
- * @returns The matching `JSXAttribute` or `JSXSpreadAttribute`, or `undefined` when not found.
74
- */
75
- function findAttribute(context, element, name) {
76
- function findProperty(properties, name, seen = /* @__PURE__ */ new Set()) {
77
- for (const property of properties) {
78
- if (property.type === AST_NODE_TYPES.Property && !property.computed && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === name) return property;
79
- if (property.type !== AST_NODE_TYPES.SpreadElement) continue;
80
- const argument = property.argument;
81
- if (argument.type === AST_NODE_TYPES.Identifier) {
82
- const initNode = resolve(context, argument);
83
- if (initNode?.type === AST_NODE_TYPES.ObjectExpression) {
84
- if (seen.has(initNode)) continue;
85
- seen.add(initNode);
86
- const found = findProperty(initNode.properties, name, seen);
87
- if (found != null) return found;
88
- }
89
- continue;
90
- }
91
- if (argument.type === AST_NODE_TYPES.ObjectExpression) {
92
- if (seen.has(argument)) continue;
93
- seen.add(argument);
94
- const found = findProperty(argument.properties, name, seen);
95
- if (found != null) return found;
96
- }
97
- }
98
- return null;
99
- }
100
- return element.openingElement.attributes.findLast((attr) => {
101
- if (attr.type === AST_NODE_TYPES.JSXAttribute) return getAttributeName(attr) === name;
102
- switch (attr.argument.type) {
103
- case AST_NODE_TYPES.Identifier: {
104
- const initNode = resolve(context, attr.argument);
105
- if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return findProperty(initNode.properties, name) != null;
106
- return false;
107
- }
108
- case AST_NODE_TYPES.ObjectExpression: return findProperty(attr.argument.properties, name) != null;
109
- }
110
- return false;
111
- });
112
- }
113
-
114
- //#endregion
115
- //#region src/find-parent-attribute.ts
116
- /**
117
- * Walk up the AST from `node` to find the nearest ancestor that is a `JSXAttribute`
118
- * and (optionally) passes a predicate.
119
- *
120
- * This is useful when a rule visitor enters a deeply nested node (ex: a `Literal`
121
- * inside an expression container) and needs to know which JSX attribute it belongs to.
122
- * @param node The starting node for the upward search.
123
- * @param test Optional predicate to filter candidate `JSXAttribute` nodes. When omitted every `JSXAttribute` ancestor matches.
124
- * @returns The first matching `JSXAttribute` ancestor, or `null` if none is found before reaching the root.
125
- */
126
- function findParentAttribute(node, test = () => true) {
127
- const guard = (n) => {
128
- return n.type === AST_NODE_TYPES.JSXAttribute && test(n);
129
- };
130
- return Traverse.findParent(node, guard);
131
- }
132
-
133
- //#endregion
134
- //#region src/resolve-attribute-value.ts
135
- /**
136
- * Resolve the value of a JSX attribute (or spread attribute) into a
137
- * {@link JsxAttributeValue} descriptor that can be inspected further.
138
- *
139
- * This is the low-level building block; it operates on a single attribute
140
- * node that the caller has already located. For the higher-level "find by
141
- * name and resolve" combo, see {@link getAttributeValue}.
142
- * @param context The ESLint rule context (needed for scope look-ups).
143
- * @param attribute A `JSXAttribute` or `JSXSpreadAttribute` node.
144
- * @returns A discriminated-union descriptor of the attribute's value.
145
- */
146
- function resolveAttributeValue(context, attribute) {
147
- if (attribute.type === AST_NODE_TYPES.JSXAttribute) return resolveJsxAttribute(context, attribute);
148
- return resolveJsxSpreadAttribute(context, attribute);
149
- }
150
- function resolveJsxAttribute(context, node) {
151
- const scope = context.sourceCode.getScope(node);
152
- if (node.value == null) return {
153
- kind: "boolean",
154
- node: null,
155
- toStatic() {
156
- return true;
157
- }
158
- };
159
- switch (node.value.type) {
160
- case AST_NODE_TYPES.Literal: {
161
- const staticValue = node.value.value;
162
- return {
163
- kind: "literal",
164
- node: node.value,
165
- toStatic() {
166
- return staticValue;
167
- }
168
- };
169
- }
170
- case AST_NODE_TYPES.JSXExpressionContainer: {
171
- const expr = node.value.expression;
172
- if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return {
173
- kind: "missing",
174
- node: expr,
175
- toStatic() {
176
- return null;
177
- }
178
- };
179
- return {
180
- kind: "unknown",
181
- node: expr,
182
- toStatic() {
183
- return getStaticValue(expr, scope)?.value;
184
- }
185
- };
186
- }
187
- case AST_NODE_TYPES.JSXElement: return {
188
- kind: "element",
189
- node: node.value,
190
- toStatic() {
191
- return null;
192
- }
193
- };
194
- case AST_NODE_TYPES.JSXSpreadChild: return {
195
- kind: "spreadChild",
196
- getChildren() {
197
- return null;
198
- },
199
- node: node.value.expression,
200
- toStatic() {
201
- return null;
202
- }
203
- };
204
- }
205
- }
206
- function resolveJsxSpreadAttribute(context, node) {
207
- const scope = context.sourceCode.getScope(node);
208
- return {
209
- kind: "spreadProps",
210
- getProperty(name) {
211
- return match(getStaticValue(node.argument, scope)?.value).with({ [name]: P.select(P.unknown) }, (v) => v).otherwise(() => null);
212
- },
213
- node: node.argument,
214
- toStatic() {
215
- return null;
216
- }
217
- };
218
- }
219
-
220
- //#endregion
221
- //#region src/get-attribute-static-value.ts
222
- /**
223
- * Find an attribute by name on a JSX element and collapse its value to a plain
224
- * JavaScript value in a single step.
225
- *
226
- * This is a convenience composition of {@link findAttribute} ->
227
- * {@link resolveAttributeValue} -> `toStatic()`, with automatic handling of the
228
- * `spreadProps` case (extracts the named property from the spread object).
229
- *
230
- * Returns `null` when the attribute is absent, `undefined` when the value cannot
231
- * be statically determined (including empty expression containers), and the
232
- * resolved static value otherwise.
233
- * @param context The ESLint rule context.
234
- * @param element The `JSXElement` node to inspect.
235
- * @param name The attribute name to look up (ex: "className").
236
- * @returns The static value of the attribute, `null` when absent, or `undefined` when indeterminate.
237
- */
238
- function getAttributeStaticValue(context, element, name) {
239
- const attr = findAttribute(context, element, name);
240
- if (attr == null) return null;
241
- const resolved = resolveAttributeValue(context, attr);
242
- if (resolved.kind === "spreadProps") return resolved.getProperty(name);
243
- if (resolved.kind === "missing") return;
244
- return resolved.toStatic();
245
- }
246
-
247
- //#endregion
248
- //#region src/get-attribute-value.ts
249
- /**
250
- * Find an attribute by name on a JSX element and resolve its value in a single call.
251
- *
252
- * This is a convenience composition of {@link findAttribute} and
253
- * {@link resolveAttributeValue} that eliminates the most common two-step
254
- * pattern in lint rules.
255
- * @param context The ESLint rule context.
256
- * @param element The `JSXElement` node to search.
257
- * @param name The attribute name to look up (ex: "className").
258
- * @returns A {@link JsxAttributeValue} descriptor, or `null` when the attribute is not present on the element.
259
- */
260
- function getAttributeValue(context, element, name) {
261
- const attr = findAttribute(context, element, name);
262
- if (attr == null) return null;
263
- return resolveAttributeValue(context, attr);
264
- }
265
-
266
- //#endregion
267
- //#region src/is-whitespace.ts
268
- /**
269
- * Check whether a JSX child node is whitespace padding that React would
270
- * trim away during rendering.
271
- *
272
- * A child is considered whitespace padding when it is a `JSXText` node whose
273
- * content is empty after applying React's whitespace normalization
274
- * (see {@link collapseMultilineText}, modelled after Babel's
275
- * `cleanJSXElementLiteralChild`). This is the whitespace that appears between
276
- * JSX tags purely for formatting.
277
- * @param node A JSX child node.
278
- * @returns `true` when the node is purely formatting whitespace.
279
- */
280
- function isWhitespace(node) {
281
- if (node.type !== AST_NODE_TYPES.JSXText) return false;
282
- return collapseMultilineText(node.value) == null && node.value.includes("\n");
283
- }
284
- /**
285
- * Check whether a JSX child node is any whitespace-only text.
286
- *
287
- * This is a looser variant of {@link isWhitespace}; it matches every
288
- * `JSXText` node whose raw content is empty after trimming, regardless of
289
- * whether it contains a newline.
290
- * @param node A JSX child node.
291
- * @returns `true` when the node is a whitespace-only `JSXText`.
292
- */
293
- function isWhitespaceText(node) {
294
- if (node.type !== AST_NODE_TYPES.JSXText) return false;
295
- return node.raw.trim() === "";
296
- }
297
- /**
298
- * Check whether a JSX child node is an empty string expression (`{""}`).
299
- *
300
- * React's reconciler and SSR renderer explicitly skip empty strings,
301
- * producing no DOM node (see `ReactChildFiber.js` and `ReactFizzConfigDOM.js`).
302
- * Such expressions are therefore treated as non-rendered children, in the same
303
- * way as whitespace padding.
304
- * @param node A JSX child node.
305
- * @returns `true` when the node is a `{""}` expression container.
306
- */
307
- function isEmptyStringExpression(node) {
308
- if (node.type !== AST_NODE_TYPES.JSXExpressionContainer) return false;
309
- const expr = node.expression;
310
- if (expr.type !== AST_NODE_TYPES.Literal) return false;
311
- return expr.value === "";
312
- }
313
-
314
- //#endregion
315
- //#region src/get-children.ts
316
- /**
317
- * Get the meaningful children of a JSX element or fragment.
318
- *
319
- * Mirrors Babel's `buildChildren` helper:
320
- * 1. Iterate over `element.children`.
321
- * 2. Skip `JSXText` nodes that clean to nothing (padding whitespace).
322
- * 3. Skip `JSXExpressionContainer` nodes whose expression is empty.
323
- * 4. Skip `JSXEmptyExpression` nodes.
324
- * 5. Collect everything else.
325
- * @param element A `JSXElement` or `JSXFragment` node.
326
- * @returns An array of children nodes that contribute to rendered output.
327
- */
328
- function getChildren(element) {
329
- const elements = [];
330
- for (const child of element.children) {
331
- if (child.type === AST_NODE_TYPES.JSXText) {
332
- if (collapseMultilineText(child.value) == null && child.value.includes("\n")) continue;
333
- elements.push(child);
334
- continue;
335
- }
336
- if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
337
- const { expression } = child;
338
- if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) continue;
339
- if (isEmptyStringExpression(child)) continue;
340
- elements.push(child);
341
- continue;
342
- }
343
- elements.push(child);
344
- }
345
- return elements;
346
- }
347
-
348
- //#endregion
349
- //#region src/get-element-type.ts
350
- /**
351
- * Get the string representation of a JSX element's type.
352
- *
353
- * - `<div>` -> `"div"`
354
- * - `<Foo.Bar>` -> `"Foo.Bar"`
355
- * - `<React.Fragment>` -> `"React.Fragment"`
356
- * - `<></>` -> `""`.
357
- * @param node A `JSXElement` or `JSXFragment` node.
358
- * @returns The fully-qualified element type string.
359
- */
360
- function getElementFullType(node) {
361
- if (node.type === AST_NODE_TYPES.JSXFragment) return "";
362
- function getQualifiedName(node) {
363
- switch (node.type) {
364
- case AST_NODE_TYPES.JSXIdentifier: return node.name;
365
- case AST_NODE_TYPES.JSXNamespacedName: return node.namespace.name + ":" + node.name.name;
366
- default: return getQualifiedName(node.object) + "." + getQualifiedName(node.property);
367
- }
368
- }
369
- return getQualifiedName(node.openingElement.name);
370
- }
371
- /**
372
- * Get the self name (last dot-separated segment) of a JSX element type.
373
- *
374
- * - `<Foo.Bar.Baz>` -> `"Baz"`
375
- * - `<div>` -> `"div"`
376
- * - `<></>` -> `""`.
377
- * @param node A `JSXElement` or `JSXFragment` node.
378
- * @returns The last segment of the element type, or `""` for fragments.
379
- */
380
- function getElementSelfType(node) {
381
- return getElementFullType(node).split(".").at(-1) ?? "";
382
- }
383
-
384
- //#endregion
385
- //#region src/has-any-attribute.ts
386
- /**
387
- * Check whether a JSX element carries at least one of the given attributes.
388
- *
389
- * This is a batch variant of {@link hasAttribute} for the common pattern of
390
- * short-circuiting on multiple prop names.
391
- *
392
- * Spread attributes are taken into account (see {@link findAttribute}).
393
- * @param context The ESLint rule context (needed for variable resolution in spread attributes).
394
- * @param element The `JSXElement` node to inspect.
395
- * @param names The attribute names to look for.
396
- * @returns `true` when at least one of the attributes is present.
397
- */
398
- function hasAnyAttribute(context, element, names) {
399
- return names.some((name) => findAttribute(context, element, name) != null);
400
- }
401
-
402
- //#endregion
403
- //#region src/has-attribute.ts
404
- /**
405
- * Check whether a JSX element carries a given attribute (prop).
406
- *
407
- * This is a thin convenience wrapper around {@link findAttribute} for the
408
- * common case where you only need a boolean answer.
409
- *
410
- * Spread attributes are taken into account: `<Comp {...{ disabled: true }} />`
411
- * will report `true` for `"disabled"`.
412
- * @param context The ESLint rule context (needed for variable resolution in spread attributes).
413
- * @param element The `JSXElement` node to inspect.
414
- * @param name The attribute name to look for (ex: "className").
415
- * @returns `true` when the attribute is present on the element.
416
- */
417
- function hasAttribute(context, element, name) {
418
- return findAttribute(context, element, name) != null;
419
- }
420
-
421
- //#endregion
422
- //#region src/has-children.ts
423
- /**
424
- * Check whether a JSX element (or fragment) has meaningful children, that is,
425
- * at least one child that is not purely whitespace text or an empty string expression.
426
- *
427
- * A `JSXText` child whose `raw` content is empty after trimming is considered
428
- * non-meaningful because it is typically a code-formatting artifact
429
- * (indentation between tags). While React's client renderer preserves these
430
- * nodes as text nodes, they rarely represent intentionally rendered content.
431
- *
432
- * An empty string expression (`children={""}`) is also considered
433
- * non-meaningful because React's reconciler and SSR renderer explicitly skip
434
- * empty strings, producing no DOM node.
435
- *
436
- * Unlike {@link getChildren} (which only filters whitespace that contains a
437
- * newline) this check treats any whitespace-only text as non-meaningful
438
- * (see {@link isWhitespaceText}). As a result `hasChildren(node)` is not
439
- * always equal to `getChildren(node).length > 0`: they differ for
440
- * whitespace-only children that have no newline, such as `<div> </div>` or
441
- * `<div>\t\t</div>`. Choose the API that matches your rule's intent.
442
- * @param element A `JSXElement` or `JSXFragment` node.
443
- * @returns `true` when the element has at least one meaningful child.
444
- */
445
- function hasChildren(element) {
446
- if (element.children.length === 0) return false;
447
- return !element.children.every((child) => isWhitespaceText(child) || isEmptyStringExpression(child));
448
- }
449
-
450
- //#endregion
451
- //#region src/has-every-attribute.ts
452
- /**
453
- * Check whether a JSX element carries all of the given attributes (props).
454
- *
455
- * This is a batch variant of {@link hasAttribute} for the common pattern
456
- * where a rule needs to verify that a set of required props are all present.
457
- *
458
- * Spread attributes are taken into account (see {@link findAttribute}).
459
- * @param context The ESLint rule context (needed for variable resolution in spread attributes).
460
- * @param element The `JSXElement` node to inspect.
461
- * @param names The attribute names to look for.
462
- * @returns `true` when every name in `names` is present on the element.
463
- */
464
- function hasEveryAttribute(context, element, names) {
465
- return names.every((name) => findAttribute(context, element, name) != null);
466
- }
467
-
468
- //#endregion
469
6
  //#region ../../.pkgs/eff/dist/index.js
470
7
  /**
471
8
  * Applies a `pipe` method's variadic arguments to an initial value from left
@@ -749,217 +286,667 @@ const constTrue = constant(true);
749
286
  * **Example** (Returning false from a thunk)
750
287
  *
751
288
  * ```ts
752
- * import { Function } from "effect"
289
+ * import { Function } from "effect"
290
+ * import * as assert from "node:assert"
291
+ *
292
+ * assert.deepStrictEqual(Function.constFalse(), false)
293
+ * ```
294
+ *
295
+ * @category constants
296
+ * @since 2.0.0
297
+ */
298
+ const constFalse = constant(false);
299
+ /**
300
+ * Returns `null` when called.
301
+ *
302
+ * **When to use**
303
+ *
304
+ * Use when you need a thunk that returns `null` on every invocation.
305
+ *
306
+ * **Example** (Returning null from a thunk)
307
+ *
308
+ * ```ts
309
+ * import { Function } from "effect"
310
+ * import * as assert from "node:assert"
311
+ *
312
+ * assert.deepStrictEqual(Function.constNull(), null)
313
+ * ```
314
+ *
315
+ * @category constants
316
+ * @since 2.0.0
317
+ */
318
+ const constNull = constant(null);
319
+ /**
320
+ * Returns `undefined` when called.
321
+ *
322
+ * **When to use**
323
+ *
324
+ * Use when you need a thunk that returns `undefined` on every invocation.
325
+ *
326
+ * **Example** (Returning undefined from a thunk)
327
+ *
328
+ * ```ts
329
+ * import { Function } from "effect"
330
+ * import * as assert from "node:assert"
331
+ *
332
+ * assert.deepStrictEqual(Function.constUndefined(), undefined)
333
+ * ```
334
+ *
335
+ * @category constants
336
+ * @since 2.0.0
337
+ */
338
+ const constUndefined = constant(void 0);
339
+ /**
340
+ * 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`.
341
+ * The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
342
+ *
343
+ * **When to use**
344
+ *
345
+ * Use to compose exactly two unary functions into a reusable unary function.
346
+ *
347
+ * **Example** (Composing two functions)
348
+ *
349
+ * ```ts
350
+ * import { Function } from "effect"
351
+ * import * as assert from "node:assert"
352
+ *
353
+ * const increment = (n: number) => n + 1
354
+ * const square = (n: number) => n * n
355
+ *
356
+ * assert.strictEqual(Function.compose(increment, square)(2), 9)
357
+ * ```
358
+ *
359
+ * @see {@link flow} for composing a left-to-right sequence of functions
360
+ * @see {@link pipe} for applying a value through a left-to-right sequence immediately
361
+ *
362
+ * @category combinators
363
+ * @since 2.0.0
364
+ */
365
+ const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
366
+ /**
367
+ * Marks an impossible branch by accepting a `never` value and returning any
368
+ * type.
369
+ *
370
+ * **When to use**
371
+ *
372
+ * Use when you need a return value in a branch that exhaustive checks prove
373
+ * cannot be reached.
374
+ *
375
+ * **Gotchas**
376
+ *
377
+ * Calling `absurd` throws, because a value of type `never` should be
378
+ * impossible at runtime.
379
+ *
380
+ * **Example** (Handling impossible values)
381
+ *
382
+ * ```ts
383
+ * import { absurd } from "effect"
384
+ *
385
+ * const handleNever = (value: never) => {
386
+ * return absurd(value) // This will throw an error if called
387
+ * }
388
+ * ```
389
+ *
390
+ * @category utility types
391
+ * @since 2.0.0
392
+ */
393
+ const absurd = (_) => {
394
+ throw new Error("Called `absurd` function which should be uncallable");
395
+ };
396
+ /**
397
+ * Creates a compile-time placeholder for a value of any type.
398
+ *
399
+ * **When to use**
400
+ *
401
+ * Use as a temporary typed placeholder while developing incomplete code.
402
+ *
403
+ * **Gotchas**
404
+ *
405
+ * `hole` is intended for temporary development use. If the placeholder is
406
+ * evaluated at runtime, it throws.
407
+ *
408
+ * **Example** (Creating a development placeholder)
409
+ *
410
+ * ```ts
411
+ * import { hole } from "effect"
412
+ *
413
+ * // Intentionally not called: `hole` throws if the placeholder is evaluated.
414
+ * const buildUser = (id: number): { readonly id: number; readonly name: string } => ({
415
+ * id,
416
+ * name: hole<string>()
417
+ * })
418
+ *
419
+ * console.log(typeof buildUser) // "function"
420
+ * ```
421
+ *
422
+ * @category utility types
423
+ * @since 2.0.0
424
+ */
425
+ const hole = cast(absurd);
426
+ /**
427
+ * Drops the longest prefix of elements from an array that satisfy the given predicate.
428
+ *
429
+ * Supports both data-first and data-last (`pipe`-friendly) call styles.
430
+ *
431
+ * @param pred - The predicate to test each element with.
432
+ * @returns A new array without the matching prefix.
433
+ * @example
434
+ * ```ts
435
+ * import * as assert from "node:assert"
436
+ * import { dropWhile, pipe } from "@local/eff"
437
+ *
438
+ * // data-first
439
+ * assert.deepStrictEqual(dropWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [3, 2, 1])
440
+ *
441
+ * // data-last
442
+ * assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], dropWhile((n: number) => n < 3)), [3, 2, 1])
443
+ * ```
444
+ * @category array
445
+ */
446
+ const dropWhile = dual(2, (xs, pred) => {
447
+ const len = xs.length;
448
+ let idx = 0;
449
+ while (idx < len && pred(xs[idx])) idx++;
450
+ return xs.slice(idx);
451
+ });
452
+ /**
453
+ * Takes the longest prefix of elements from an array that satisfy the given predicate.
454
+ *
455
+ * Supports both data-first and data-last (`pipe`-friendly) call styles.
456
+ *
457
+ * @param pred - The predicate to test each element with.
458
+ * @returns A new array containing only the matching prefix.
459
+ * @example
460
+ * ```ts
753
461
  * import * as assert from "node:assert"
462
+ * import { pipe, takeWhile } from "@local/eff"
754
463
  *
755
- * assert.deepStrictEqual(Function.constFalse(), false)
756
- * ```
464
+ * // data-first
465
+ * assert.deepStrictEqual(takeWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [1, 2])
757
466
  *
758
- * @category constants
759
- * @since 2.0.0
467
+ * // data-last
468
+ * assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], takeWhile((n: number) => n < 3)), [1, 2])
469
+ * ```
470
+ * @category array
760
471
  */
761
- const constFalse = constant(false);
472
+ const takeWhile = dual(2, (xs, pred) => {
473
+ const len = xs.length;
474
+ let idx = 0;
475
+ while (idx < len && pred(xs[idx])) idx++;
476
+ return xs.slice(0, idx);
477
+ });
478
+
479
+ //#endregion
480
+ //#region src/attribute-name.ts
762
481
  /**
763
- * Returns `null` when called.
764
- *
765
- * **When to use**
482
+ * Get the stringified name of a `JSXAttribute` node.
766
483
  *
767
- * Use when you need a thunk that returns `null` on every invocation.
484
+ * Handles both simple identifiers and namespaced names:
485
+ * - `className` -> `"className"`
486
+ * - `aria-label` -> `"aria-label"`
487
+ * - `xml:space` -> `"xml:space"`.
488
+ * @param node A `JSXAttribute` AST node.
489
+ * @returns The attribute name as a plain string.
490
+ */
491
+ function getAttributeName(node) {
492
+ if (node.name.type === AST_NODE_TYPES.JSXIdentifier) return node.name.name;
493
+ return node.name.namespace.name + ":" + node.name.name.name;
494
+ }
495
+ /**
496
+ * Check whether a node is a `JSXAttribute` with the given name.
768
497
  *
769
- * **Example** (Returning null from a thunk)
498
+ * Only plain identifier names are matched (ex: `className`); namespaced
499
+ * attributes (ex: `xml:space`) do not match.
770
500
  *
771
- * ```ts
772
- * import { Function } from "effect"
773
- * import * as assert from "node:assert"
501
+ * Supports both data-first and data-last (curried) call styles:
502
+ * - `isAttribute(node, "className")`
503
+ * - `isAttribute("className")(node)`.
504
+ * @param node The AST node to test.
505
+ * @param name The attribute name to match (ex: "className").
506
+ * @returns `true` when the node is a `JSXAttribute` named `name`.
507
+ */
508
+ const isAttribute = dual(2, (node, name) => {
509
+ return node.type === AST_NODE_TYPES.JSXAttribute && node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === name;
510
+ });
511
+
512
+ //#endregion
513
+ //#region src/attribute-find.ts
514
+ /**
515
+ * Find a JSX attribute (or spread attribute containing the property) by name on a given element.
774
516
  *
775
- * assert.deepStrictEqual(Function.constNull(), null)
776
- * ```
517
+ * Returns the last matching attribute to mirror React's behavior where later props win,
518
+ * or `undefined` when the attribute is not present.
777
519
  *
778
- * @category constants
779
- * @since 2.0.0
520
+ * Spread attributes are resolved when possible: if the spread argument is an identifier
521
+ * that resolves to an object expression, the object's properties are searched for a matching key.
522
+ * Nested object expressions and nested spread identifiers are also resolved
523
+ * (see {@link findSpreadProperty}).
524
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes).
525
+ * @param element The `JSXElement` node to search.
526
+ * @param name The attribute name to look for (ex: "className").
527
+ * @returns The matching `JSXAttribute` or `JSXSpreadAttribute`, or `undefined` when not found.
780
528
  */
781
- const constNull = constant(null);
529
+ function findAttribute(context, element, name) {
530
+ return element.openingElement.attributes.findLast((attr) => {
531
+ if (attr.type === AST_NODE_TYPES.JSXAttribute) return getAttributeName(attr) === name;
532
+ return findSpreadProperty(context, attr.argument, name) != null;
533
+ });
534
+ }
782
535
  /**
783
- * Returns `undefined` when called.
784
- *
785
- * **When to use**
536
+ * Walk up the AST from `node` to find the nearest ancestor that is a `JSXAttribute`
537
+ * and (optionally) passes a predicate.
786
538
  *
787
- * Use when you need a thunk that returns `undefined` on every invocation.
539
+ * This is useful when a rule visitor enters a deeply nested node (ex: a `Literal`
540
+ * inside an expression container) and needs to know which JSX attribute it belongs to.
541
+ * @param node The starting node for the upward search.
542
+ * @param test Optional predicate to filter candidate `JSXAttribute` nodes. When omitted every `JSXAttribute` ancestor matches.
543
+ * @returns The first matching `JSXAttribute` ancestor, or `undefined` if none is found before reaching the root.
544
+ */
545
+ function findParentAttribute(node, test = () => true) {
546
+ const guard = (n) => {
547
+ return n.type === AST_NODE_TYPES.JSXAttribute && test(n);
548
+ };
549
+ return Traverse.findParent(node, guard) ?? void 0;
550
+ }
551
+ /**
552
+ * Find the `Property` node that provides a given key inside a spread argument.
553
+ *
554
+ * This is the single resolution routine shared by {@link findAttribute} (existence
555
+ * checks) and the `spreadProps` variant of `resolveAttributeValue` (value extraction):
556
+ *
557
+ * - An `Identifier` argument is resolved to its initializer via variable
558
+ * resolution, following alias chains (`const b = a`) like `getStaticValue`
559
+ * does; an `ObjectExpression` argument is searched directly.
560
+ * - Properties are walked **in reverse** so that later entries win, matching
561
+ * JavaScript object semantics (`{ ...a, k: 1 }` -> the literal `k`).
562
+ * - Nested `SpreadElement`s (identifiers or inline object expressions) are
563
+ * searched recursively; a `seen` set guards against circular references.
564
+ * - Plain identifier keys and string literal keys are matched directly;
565
+ * computed keys are matched when they are statically evaluable
566
+ * (ex: `{ ["class" + "Name"]: 1 }`).
567
+ * @param context The ESLint rule context (needed for variable resolution).
568
+ * @param argument The spread argument expression to search.
569
+ * @param name The property name to look for.
570
+ * @param seen Internal set of already-visited nodes (cycle guard).
571
+ * @returns The matching `Property` node, or `undefined` when the key is not found.
572
+ */
573
+ function findSpreadProperty(context, argument, name, seen = /* @__PURE__ */ new Set()) {
574
+ let objectExpression;
575
+ if (argument.type === AST_NODE_TYPES.Identifier) {
576
+ let initNode = resolve(context, argument);
577
+ while (initNode != null && initNode.type === AST_NODE_TYPES.Identifier && !seen.has(initNode)) {
578
+ seen.add(initNode);
579
+ initNode = resolve(context, initNode);
580
+ }
581
+ if (initNode?.type === AST_NODE_TYPES.ObjectExpression) objectExpression = initNode;
582
+ } else if (argument.type === AST_NODE_TYPES.ObjectExpression) objectExpression = argument;
583
+ if (objectExpression == null || seen.has(objectExpression)) return void 0;
584
+ seen.add(objectExpression);
585
+ const { properties } = objectExpression;
586
+ for (let i = properties.length - 1; i >= 0; i--) {
587
+ const property = properties[i];
588
+ if (property == null) continue;
589
+ if (property.type === AST_NODE_TYPES.Property) {
590
+ const { key } = property;
591
+ if (property.computed) {
592
+ if (getStaticValue(key, context.sourceCode.getScope(key))?.value === name) return property;
593
+ continue;
594
+ }
595
+ if (key.type === AST_NODE_TYPES.Identifier && key.name === name) return property;
596
+ if (key.type === AST_NODE_TYPES.Literal && key.value === name) return property;
597
+ continue;
598
+ }
599
+ const found = findSpreadProperty(context, property.argument, name, seen);
600
+ if (found != null) return found;
601
+ }
602
+ }
603
+
604
+ //#endregion
605
+ //#region src/attribute-has.ts
606
+ /**
607
+ * Check whether a JSX element carries a given attribute (prop).
788
608
  *
789
- * **Example** (Returning undefined from a thunk)
609
+ * This is a thin convenience wrapper around {@link findAttribute} for the
610
+ * common case where you only need a boolean answer.
790
611
  *
791
- * ```ts
792
- * import { Function } from "effect"
793
- * import * as assert from "node:assert"
612
+ * Spread attributes are taken into account: `<Comp {...{ disabled: true }} />`
613
+ * will report `true` for `"disabled"`.
614
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes).
615
+ * @param element The `JSXElement` node to inspect.
616
+ * @param name The attribute name to look for (ex: "className").
617
+ * @returns `true` when the attribute is present on the element.
618
+ */
619
+ function hasAttribute(context, element, name) {
620
+ return findAttribute(context, element, name) != null;
621
+ }
622
+ /**
623
+ * Check whether a JSX element carries at least one of the given attributes.
794
624
  *
795
- * assert.deepStrictEqual(Function.constUndefined(), undefined)
796
- * ```
625
+ * This is a batch variant of {@link hasAttribute} for the common pattern of
626
+ * short-circuiting on multiple prop names.
797
627
  *
798
- * @category constants
799
- * @since 2.0.0
628
+ * Spread attributes are taken into account (see {@link findAttribute}).
629
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes).
630
+ * @param element The `JSXElement` node to inspect.
631
+ * @param names The attribute names to look for.
632
+ * @returns `true` when at least one of the attributes is present.
800
633
  */
801
- const constUndefined = constant(void 0);
634
+ function hasAnyAttribute(context, element, names) {
635
+ return names.some((name) => findAttribute(context, element, name) != null);
636
+ }
802
637
  /**
803
- * 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`.
804
- * The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
638
+ * Check whether a JSX element carries all of the given attributes (props).
805
639
  *
806
- * **When to use**
640
+ * This is a batch variant of {@link hasAttribute} for the common pattern
641
+ * where a rule needs to verify that a set of required props are all present.
807
642
  *
808
- * Use to compose exactly two unary functions into a reusable unary function.
643
+ * Spread attributes are taken into account (see {@link findAttribute}).
644
+ * @param context The ESLint rule context (needed for variable resolution in spread attributes).
645
+ * @param element The `JSXElement` node to inspect.
646
+ * @param names The attribute names to look for.
647
+ * @returns `true` when every name in `names` is present on the element.
648
+ */
649
+ function hasEveryAttribute(context, element, names) {
650
+ return names.every((name) => findAttribute(context, element, name) != null);
651
+ }
652
+
653
+ //#endregion
654
+ //#region src/attribute-value.ts
655
+ /**
656
+ * Resolve the value of a JSX attribute (or spread attribute) into an
657
+ * {@link AttributeValue} descriptor that can be inspected further.
809
658
  *
810
- * **Example** (Composing two functions)
659
+ * This is the low-level building block; it operates on a single attribute
660
+ * node that the caller has already located. For the higher-level "find by
661
+ * name and resolve" combo, see {@link getAttributeValue}.
811
662
  *
812
- * ```ts
813
- * import { Function } from "effect"
814
- * import * as assert from "node:assert"
663
+ * When the attribute is a `JSXSpreadAttribute`, passing `name` (typically the
664
+ * same name the attribute was found by) makes `toStatic()` return the static
665
+ * value of that named property, eliminating the need to branch on
666
+ * `kind === "spreadProps"` at the call site.
667
+ * @param context The ESLint rule context (needed for scope look-ups).
668
+ * @param attribute A `JSXAttribute` or `JSXSpreadAttribute` node.
669
+ * @param name Optional property name used to resolve `toStatic()` for spread attributes.
670
+ * @returns A discriminated-union descriptor of the attribute's value.
671
+ */
672
+ function resolveAttributeValue(context, attribute, name) {
673
+ if (attribute.type === AST_NODE_TYPES.JSXAttribute) return resolveJsxAttribute(context, attribute);
674
+ return resolveJsxSpreadAttribute(context, attribute, name);
675
+ }
676
+ /**
677
+ * Find an attribute by name on a JSX element and resolve its value in a single call.
815
678
  *
816
- * const increment = (n: number) => n + 1
817
- * const square = (n: number) => n * n
679
+ * This is a convenience composition of {@link findAttribute} and
680
+ * {@link resolveAttributeValue} that eliminates the most common two-step
681
+ * pattern in lint rules.
682
+ * @param context The ESLint rule context.
683
+ * @param element The `JSXElement` node to search.
684
+ * @param name The attribute name to look up (ex: "className").
685
+ * @returns An {@link AttributeValue} descriptor, or `undefined` when the attribute is not present on the element.
686
+ */
687
+ function getAttributeValue(context, element, name) {
688
+ const attr = findAttribute(context, element, name);
689
+ if (attr == null) return void 0;
690
+ return resolveAttributeValue(context, attr, name);
691
+ }
692
+ /**
693
+ * Find an attribute by name on a JSX element and collapse its value to a plain
694
+ * JavaScript value in a single step.
818
695
  *
819
- * assert.strictEqual(Function.compose(increment, square)(2), 9)
820
- * ```
696
+ * This is a convenience composition of {@link findAttribute} ->
697
+ * {@link resolveAttributeValue} -> `toStatic()`, with automatic handling of the
698
+ * `spreadProps` case (extracts the named property from the spread object).
821
699
  *
822
- * @see {@link flow} for composing a left-to-right sequence of functions
823
- * @see {@link pipe} for applying a value through a left-to-right sequence immediately
700
+ * Returns `undefined` both when the attribute is absent and when its value
701
+ * cannot be statically determined; use {@link findAttribute} or
702
+ * {@link hasAttribute} when presence itself matters.
703
+ * @param context The ESLint rule context.
704
+ * @param element The `JSXElement` node to inspect.
705
+ * @param name The attribute name to look up (ex: "className").
706
+ * @returns The static value of the attribute, or `undefined` when absent or indeterminate.
707
+ */
708
+ function getAttributeStaticValue(context, element, name) {
709
+ return getAttributeValue(context, element, name)?.toStatic();
710
+ }
711
+ function resolveJsxAttribute(context, node) {
712
+ const scope = context.sourceCode.getScope(node);
713
+ if (node.value == null) return {
714
+ kind: "boolean",
715
+ node: null,
716
+ toStatic() {
717
+ return true;
718
+ }
719
+ };
720
+ switch (node.value.type) {
721
+ case AST_NODE_TYPES.Literal: {
722
+ const staticValue = node.value.value;
723
+ return {
724
+ kind: "literal",
725
+ node: node.value,
726
+ toStatic() {
727
+ return staticValue;
728
+ }
729
+ };
730
+ }
731
+ case AST_NODE_TYPES.JSXExpressionContainer: {
732
+ const expr = node.value.expression;
733
+ if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return {
734
+ kind: "missing",
735
+ node: expr,
736
+ toStatic() {}
737
+ };
738
+ return {
739
+ kind: "unknown",
740
+ node: expr,
741
+ toStatic() {
742
+ return getStaticValue(expr, scope)?.value;
743
+ }
744
+ };
745
+ }
746
+ case AST_NODE_TYPES.JSXElement: return {
747
+ kind: "element",
748
+ node: node.value,
749
+ toStatic() {}
750
+ };
751
+ case AST_NODE_TYPES.JSXSpreadChild: return {
752
+ kind: "spreadChild",
753
+ node: node.value,
754
+ toStatic() {}
755
+ };
756
+ }
757
+ }
758
+ function resolveJsxSpreadAttribute(context, node, name) {
759
+ const getProperty = (propertyName) => {
760
+ const property = findSpreadProperty(context, node.argument, propertyName);
761
+ if (property == null) return void 0;
762
+ const propertyScope = context.sourceCode.getScope(property.value);
763
+ return getStaticValue(property.value, propertyScope)?.value;
764
+ };
765
+ return {
766
+ kind: "spreadProps",
767
+ getProperty,
768
+ node: node.argument,
769
+ toStatic() {
770
+ return name == null ? void 0 : getProperty(name);
771
+ }
772
+ };
773
+ }
774
+
775
+ //#endregion
776
+ //#region src/text.ts
777
+ /**
778
+ * Collapse a multiline JSX text string following React's whitespace rules.
824
779
  *
825
- * @category combinators
826
- * @since 2.0.0
780
+ * This mirrors Babel's `cleanJSXElementLiteralChild` algorithm:
781
+ * 1. Split the raw text into lines.
782
+ * 2. Find the last non-empty line.
783
+ * 3. Trim leading spaces on non-first lines and trailing spaces on non-last lines.
784
+ * 4. Collapse tabs into spaces.
785
+ * 5. Append a single space after each non-last non-empty line.
786
+ * @param text The raw JSX text string to collapse.
787
+ * @returns The collapsed string, or `null` if the text contains only whitespace.
788
+ * @see https://github.com/babel/babel/blob/main/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
827
789
  */
828
- const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
790
+ function collapseMultilineText(text) {
791
+ const lines = text.split(/\r\n|\n|\r/);
792
+ let lastNonEmptyLine = 0;
793
+ for (let i = 0; i < lines.length; i++) if (/[^ \t]/.exec(lines[i] ?? "") != null) lastNonEmptyLine = i;
794
+ let str = "";
795
+ for (let i = 0; i < lines.length; i++) {
796
+ const line = lines[i] ?? "";
797
+ const isFirstLine = i === 0;
798
+ const isLastLine = i === lines.length - 1;
799
+ const isLastNonEmptyLine = i === lastNonEmptyLine;
800
+ let trimmedLine = line.replace(/\t/g, " ");
801
+ if (!isFirstLine) trimmedLine = trimmedLine.replace(/^ +/, "");
802
+ if (!isLastLine) trimmedLine = trimmedLine.replace(/ +$/, "");
803
+ if (trimmedLine.length > 0) {
804
+ if (!isLastNonEmptyLine) trimmedLine += " ";
805
+ str += trimmedLine;
806
+ }
807
+ }
808
+ return str === "" ? null : str;
809
+ }
829
810
  /**
830
- * Marks an impossible branch by accepting a `never` value and returning any
831
- * type.
832
- *
833
- * **When to use**
834
- *
835
- * Use when you need a return value in a branch that exhaustive checks prove
836
- * cannot be reached.
837
- *
838
- * **Gotchas**
839
- *
840
- * Calling `absurd` throws, because a value of type `never` should be
841
- * impossible at runtime.
842
- *
843
- * **Example** (Handling impossible values)
844
- *
845
- * ```ts
846
- * import { absurd } from "effect"
811
+ * Check whether a JSX child node is whitespace padding that React would
812
+ * trim away during rendering.
847
813
  *
848
- * const handleNever = (value: never) => {
849
- * return absurd(value) // This will throw an error if called
850
- * }
851
- * ```
814
+ * A child is considered whitespace padding when it is a `JSXText` node whose
815
+ * content is empty after applying React's whitespace normalization
816
+ * (see {@link collapseMultilineText}, modelled after Babel's
817
+ * `cleanJSXElementLiteralChild`) **and** it contains a newline. This is the
818
+ * whitespace that appears between JSX tags purely for formatting.
852
819
  *
853
- * @category utility types
854
- * @since 2.0.0
820
+ * For the looser "any whitespace-only text" check, see {@link isWhitespaceText}.
821
+ * @param node A JSX child node.
822
+ * @returns `true` when the node is purely formatting whitespace.
855
823
  */
856
- const absurd = (_) => {
857
- throw new Error("Called `absurd` function which should be uncallable");
858
- };
824
+ function isPaddingWhitespace(node) {
825
+ if (node.type !== AST_NODE_TYPES.JSXText) return false;
826
+ return collapseMultilineText(node.value) == null && node.value.includes("\n");
827
+ }
859
828
  /**
860
- * Creates a compile-time placeholder for a value of any type.
861
- *
862
- * **When to use**
863
- *
864
- * Use as a temporary typed placeholder while developing incomplete code.
865
- *
866
- * **Gotchas**
867
- *
868
- * `hole` is intended for temporary development use. If the placeholder is
869
- * evaluated at runtime, it throws.
870
- *
871
- * **Example** (Creating a development placeholder)
872
- *
873
- * ```ts
874
- * import { hole } from "effect"
875
- *
876
- * // Intentionally not called: `hole` throws if the placeholder is evaluated.
877
- * const buildUser = (id: number): { readonly id: number; readonly name: string } => ({
878
- * id,
879
- * name: hole<string>()
880
- * })
881
- *
882
- * console.log(typeof buildUser) // "function"
883
- * ```
829
+ * Check whether a JSX child node is any whitespace-only text.
884
830
  *
885
- * @category utility types
886
- * @since 2.0.0
831
+ * This is a looser variant of {@link isPaddingWhitespace}; it matches every
832
+ * `JSXText` node whose raw content is empty after trimming, regardless of
833
+ * whether it contains a newline.
834
+ * @param node A JSX child node.
835
+ * @returns `true` when the node is a whitespace-only `JSXText`.
887
836
  */
888
- const hole = cast(absurd);
837
+ function isWhitespaceText(node) {
838
+ if (node.type !== AST_NODE_TYPES.JSXText) return false;
839
+ return node.raw.trim() === "";
840
+ }
889
841
  /**
890
- * Drops the longest prefix of elements from an array that satisfy the given predicate.
891
- *
892
- * Supports both data-first and data-last (`pipe`-friendly) call styles.
893
- *
894
- * @param pred - The predicate to test each element with.
895
- * @returns A new array without the matching prefix.
896
- * @example
897
- * ```ts
898
- * import * as assert from "node:assert"
899
- * import { dropWhile, pipe } from "@local/eff"
900
- *
901
- * // data-first
902
- * assert.deepStrictEqual(dropWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [3, 2, 1])
842
+ * Check whether a JSX child node is an empty string expression (`{""}`).
903
843
  *
904
- * // data-last
905
- * assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], dropWhile((n: number) => n < 3)), [3, 2, 1])
906
- * ```
907
- * @category array
844
+ * React's reconciler and SSR renderer explicitly skip empty strings,
845
+ * producing no DOM node (see `ReactChildFiber.js` and `ReactFizzConfigDOM.js`).
846
+ * Such expressions are therefore treated as non-rendered children, in the same
847
+ * way as whitespace padding.
848
+ * @param node A JSX child node.
849
+ * @returns `true` when the node is a `{""}` expression container.
908
850
  */
909
- const dropWhile = dual(2, (xs, pred) => {
910
- const len = xs.length;
911
- let idx = 0;
912
- while (idx < len && pred(xs[idx])) idx++;
913
- return xs.slice(idx);
914
- });
851
+ function isEmptyStringExpression(node) {
852
+ if (node.type !== AST_NODE_TYPES.JSXExpressionContainer) return false;
853
+ const expr = node.expression;
854
+ if (expr.type !== AST_NODE_TYPES.Literal) return false;
855
+ return expr.value === "";
856
+ }
857
+
858
+ //#endregion
859
+ //#region src/children.ts
915
860
  /**
916
- * Takes the longest prefix of elements from an array that satisfy the given predicate.
861
+ * Get the meaningful children of a JSX element or fragment.
917
862
  *
918
- * Supports both data-first and data-last (`pipe`-friendly) call styles.
863
+ * Mirrors Babel's `buildChildren` helper:
864
+ * 1. Iterate over `element.children`.
865
+ * 2. Skip `JSXText` nodes that clean to nothing (padding whitespace).
866
+ * 3. Skip `JSXExpressionContainer` nodes whose expression is empty.
867
+ * 4. Skip empty string expressions (`{""}`), which produce no DOM node.
868
+ * 5. Collect everything else.
869
+ * @param element A `JSXElement` or `JSXFragment` node.
870
+ * @returns An array of children nodes that contribute to rendered output.
871
+ */
872
+ function getChildren(element) {
873
+ const children = [];
874
+ for (const child of element.children) {
875
+ if (isPaddingWhitespace(child)) continue;
876
+ if (child.type === AST_NODE_TYPES.JSXExpressionContainer) {
877
+ if (child.expression.type === AST_NODE_TYPES.JSXEmptyExpression) continue;
878
+ if (isEmptyStringExpression(child)) continue;
879
+ }
880
+ children.push(child);
881
+ }
882
+ return children;
883
+ }
884
+ /**
885
+ * Check whether a JSX element (or fragment) has meaningful children, that is,
886
+ * at least one child that is not purely whitespace text or an empty string expression.
919
887
  *
920
- * @param pred - The predicate to test each element with.
921
- * @returns A new array containing only the matching prefix.
922
- * @example
923
- * ```ts
924
- * import * as assert from "node:assert"
925
- * import { pipe, takeWhile } from "@local/eff"
888
+ * A `JSXText` child whose `raw` content is empty after trimming is considered
889
+ * non-meaningful because it is typically a code-formatting artifact
890
+ * (indentation between tags). While React's client renderer preserves these
891
+ * nodes as text nodes, they rarely represent intentionally rendered content.
926
892
  *
927
- * // data-first
928
- * assert.deepStrictEqual(takeWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [1, 2])
893
+ * An empty string expression (`children={""}`) is also considered
894
+ * non-meaningful because React's reconciler and SSR renderer explicitly skip
895
+ * empty strings, producing no DOM node.
929
896
  *
930
- * // data-last
931
- * assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], takeWhile((n: number) => n < 3)), [1, 2])
932
- * ```
933
- * @category array
897
+ * Unlike {@link getChildren} (which only filters whitespace that contains a
898
+ * newline) this check treats any whitespace-only text as non-meaningful
899
+ * (see {@link isWhitespaceText}). As a result `hasChildren(node)` is not
900
+ * always equal to `getChildren(node).length > 0`: they differ for
901
+ * whitespace-only children that have no newline, such as `<div> </div>` or
902
+ * `<div>\t\t</div>`. Choose the API that matches your rule's intent.
903
+ * @param element A `JSXElement` or `JSXFragment` node.
904
+ * @returns `true` when the element has at least one meaningful child.
934
905
  */
935
- const takeWhile = dual(2, (xs, pred) => {
936
- const len = xs.length;
937
- let idx = 0;
938
- while (idx < len && pred(xs[idx])) idx++;
939
- return xs.slice(0, idx);
940
- });
906
+ function hasChildren(element) {
907
+ if (element.children.length === 0) return false;
908
+ return !element.children.every((child) => isWhitespaceText(child) || isEmptyStringExpression(child));
909
+ }
941
910
 
942
911
  //#endregion
943
- //#region src/is-attribute.ts
912
+ //#region src/element-type.ts
944
913
  /**
945
- * Check whether a node is a `JSXAttribute` with the given name.
914
+ * Get the string representation of a JSX element's type.
946
915
  *
947
- * Only plain identifier names are matched (ex: `className`); namespaced
948
- * attributes (ex: `xml:space`) do not match.
916
+ * - `<div>` -> `"div"`
917
+ * - `<Foo.Bar>` -> `"Foo.Bar"`
918
+ * - `<React.Fragment>` -> `"React.Fragment"`
919
+ * - `<xml:space>` -> `"xml:space"`
920
+ * - `<></>` -> `""`.
921
+ * @param node A `JSXElement` or `JSXFragment` node.
922
+ * @returns The fully-qualified element type string.
923
+ */
924
+ function getElementFullType(node) {
925
+ if (node.type === AST_NODE_TYPES.JSXFragment) return "";
926
+ function getQualifiedName(node) {
927
+ switch (node.type) {
928
+ case AST_NODE_TYPES.JSXIdentifier: return node.name;
929
+ case AST_NODE_TYPES.JSXNamespacedName: return node.namespace.name + ":" + node.name.name;
930
+ default: return getQualifiedName(node.object) + "." + getQualifiedName(node.property);
931
+ }
932
+ }
933
+ return getQualifiedName(node.openingElement.name);
934
+ }
935
+ /**
936
+ * Get the self name (last dot-separated segment) of a JSX element type.
949
937
  *
950
- * Supports both data-first and data-last (curried) call styles:
951
- * - `isAttribute(node, "className")`
952
- * - `isAttribute("className")(node)`.
953
- * @param node The AST node to test.
954
- * @param name The attribute name to match (ex: "className").
955
- * @returns `true` when the node is a `JSXAttribute` named `name`.
938
+ * - `<Foo.Bar.Baz>` -> `"Baz"`
939
+ * - `<div>` -> `"div"`
940
+ * - `<></>` -> `""`.
941
+ * @param node A `JSXElement` or `JSXFragment` node.
942
+ * @returns The last segment of the element type, or `""` for fragments.
956
943
  */
957
- const isAttribute = dual(2, (node, name) => {
958
- return node.type === AST_NODE_TYPES.JSXAttribute && node.name.type === AST_NODE_TYPES.JSXIdentifier && node.name.name === name;
959
- });
944
+ function getElementSelfType(node) {
945
+ return getElementFullType(node).split(".").at(-1) ?? "";
946
+ }
960
947
 
961
948
  //#endregion
962
- //#region src/is-element.ts
949
+ //#region src/element-is.ts
963
950
  /**
964
951
  * Check whether a node is a `JSXElement` (or `JSXFragment`) and optionally
965
952
  * matches a given test.
@@ -985,9 +972,6 @@ function isElement(node, test) {
985
972
  default: return test.includes(elementType);
986
973
  }
987
974
  }
988
-
989
- //#endregion
990
- //#region src/is-fragment-element.ts
991
975
  /**
992
976
  * Check whether a node is a React Fragment element.
993
977
  *
@@ -1007,9 +991,6 @@ function isFragmentElement(node, jsxFragmentFactory = "React.Fragment") {
1007
991
  const fragment = jsxFragmentFactory.split(".").at(-1) ?? "Fragment";
1008
992
  return getElementFullType(node).split(".").at(-1) === fragment;
1009
993
  }
1010
-
1011
- //#endregion
1012
- //#region src/is-host-element.ts
1013
994
  /**
1014
995
  * Check whether a node is a host (intrinsic / DOM) element.
1015
996
  *
@@ -1028,32 +1009,4 @@ function isHostElement(node) {
1028
1009
  }
1029
1010
 
1030
1011
  //#endregion
1031
- //#region src/jsx-detection-hint.ts
1032
- /**
1033
- * Hints for JSX detection.
1034
- */
1035
- const JsxDetectionHint = {
1036
- None: 0n,
1037
- DoNotIncludeJsxWithNullValue: 1n << 0n,
1038
- DoNotIncludeJsxWithNumberValue: 1n << 1n,
1039
- DoNotIncludeJsxWithBigIntValue: 1n << 2n,
1040
- DoNotIncludeJsxWithStringValue: 1n << 3n,
1041
- DoNotIncludeJsxWithBooleanValue: 1n << 4n,
1042
- DoNotIncludeJsxWithUndefinedValue: 1n << 5n,
1043
- DoNotIncludeJsxWithEmptyArrayValue: 1n << 6n,
1044
- DoNotIncludeJsxWithCreateElementValue: 1n << 7n,
1045
- RequireAllArrayElementsToBeJsx: 1n << 8n,
1046
- RequireBothSidesOfLogicalExpressionToBeJsx: 1n << 9n,
1047
- RequireBothBranchesOfConditionalExpressionToBeJsx: 1n << 10n
1048
- };
1049
- /**
1050
- * Default JSX detection hint.
1051
- *
1052
- * Skips number, bigint, boolean, string, and undefined literals,
1053
- * the value types that are commonly returned alongside JSX in React
1054
- * components but are not themselves renderable elements.
1055
- */
1056
- const DEFAULT_JSX_DETECTION_HINT = 0n | JsxDetectionHint.DoNotIncludeJsxWithNumberValue | JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | JsxDetectionHint.DoNotIncludeJsxWithStringValue | JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue;
1057
-
1058
- //#endregion
1059
- export { DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, collapseMultilineText, findAttribute, findParentAttribute, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isAttribute, isElement, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespace, isWhitespaceText, resolveAttributeValue };
1012
+ export { collapseMultilineText, findAttribute, findParentAttribute, findSpreadProperty, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isAttribute, isElement, isEmptyStringExpression, isFragmentElement, isHostElement, isPaddingWhitespace, isWhitespaceText, resolveAttributeValue };