@eslint-react/jsx 5.8.18 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +146 -382
- package/dist/index.js +144 -353
- 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
|
-
* @
|
|
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
|
|
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
|
-
* - `
|
|
52
|
-
* - `
|
|
53
|
-
*
|
|
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
|
-
*
|
|
87
|
-
*
|
|
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
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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(
|
|
103
|
-
for (const property of
|
|
104
|
-
if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) ===
|
|
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,
|
|
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
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* This is useful when a rule visitor enters a deeply
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* @
|
|
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 low
|
|
165
|
-
* node that the caller has already located.
|
|
166
|
-
* name
|
|
167
|
-
*
|
|
168
|
-
* @param
|
|
169
|
-
* @
|
|
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
|
-
*
|
|
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
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* @param
|
|
274
|
-
* @
|
|
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
|
|
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
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
268
|
+
* Check whether a JSX child node is any whitespace-only text
|
|
361
269
|
*
|
|
362
|
-
* This is a looser variant of {@link isWhitespace}
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
* @
|
|
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>`
|
|
456
|
-
* - `<Foo.Bar>`
|
|
457
|
-
* - `<React.Fragment>`
|
|
458
|
-
* - `<></>`
|
|
459
|
-
*
|
|
460
|
-
* @
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
355
|
+
* Get the self name (last dot-separated segment) of a JSX element type
|
|
474
356
|
*
|
|
475
|
-
* - `<Foo.Bar.Baz>`
|
|
476
|
-
* - `<div>`
|
|
477
|
-
* - `<></>`
|
|
478
|
-
*
|
|
479
|
-
* @
|
|
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
|
|
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
|
|
506
|
-
*
|
|
507
|
-
* @
|
|
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
|
|
527
|
-
*
|
|
528
|
-
* @
|
|
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
|
|
549
|
-
*
|
|
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
|
|
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}
|
|
562
|
-
* newline
|
|
563
|
-
* (see {@link isWhitespaceText}). As a result `hasChildren(node)` is
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
604
|
-
*
|
|
605
|
-
* @
|
|
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
|
|
461
|
+
* When called without a test, the function acts as a simple type-guard
|
|
634
462
|
* for `JSXElement | JSXFragment`.
|
|
635
|
-
*
|
|
636
|
-
* @param
|
|
637
|
-
* @
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
|
482
|
+
* Check whether a node is a React Fragment element
|
|
671
483
|
*
|
|
672
|
-
*
|
|
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
|
|
676
|
-
* segment) of both the node and the configured factory, so
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* @
|
|
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
|
|
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
|
|
507
|
+
* starting with a lowercase letter, the same heuristic React uses to
|
|
710
508
|
* distinguish `<div>` from `<MyComponent>`.
|
|
711
|
-
*
|
|
712
|
-
* @
|
|
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
|
|
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
|
*/
|