@eslint-react/jsx 1.46.0 → 4.0.0-beta.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 +664 -83
- package/dist/index.js +846 -269
- package/package.json +23 -26
- package/dist/index.d.mts +0 -102
- package/dist/index.mjs +0 -257
package/dist/index.js
CHANGED
|
@@ -1,296 +1,873 @@
|
|
|
1
|
-
|
|
1
|
+
import * as ast from "@eslint-react/ast";
|
|
2
|
+
import { findParent } from "@eslint-react/ast";
|
|
3
|
+
import { resolve } from "@eslint-react/var";
|
|
4
|
+
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/types";
|
|
5
|
+
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
6
|
+
import { P, match } from "ts-pattern";
|
|
7
|
+
import { RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME } from "@eslint-react/shared";
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
//#region src/get-attribute-name.ts
|
|
10
|
+
/**
|
|
11
|
+
* Get the stringified name of a `JSXAttribute` node.
|
|
12
|
+
*
|
|
13
|
+
* Handles both simple identifiers and namespaced names:
|
|
14
|
+
*
|
|
15
|
+
* - `className` -> `"className"`
|
|
16
|
+
* - `aria-label` -> `"aria-label"`
|
|
17
|
+
* - `xml:space` -> `"xml:space"`
|
|
18
|
+
*
|
|
19
|
+
* @param node - A `JSXAttribute` AST node.
|
|
20
|
+
* @returns The attribute name as a plain string.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { getAttributeName } from "@eslint-react/jsx";
|
|
25
|
+
*
|
|
26
|
+
* // Inside a rule visitor:
|
|
27
|
+
* JSXAttribute(node) {
|
|
28
|
+
* const name = getAttributeName(node); // "className"
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function getAttributeName(node) {
|
|
33
|
+
if (node.name.type === "JSXIdentifier") return node.name.name;
|
|
34
|
+
return node.name.namespace.name + ":" + node.name.name.name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/find-attribute.ts
|
|
39
|
+
/**
|
|
40
|
+
* Find a JSX attribute (or spread attribute containing the property) by name
|
|
41
|
+
* on a given element.
|
|
42
|
+
*
|
|
43
|
+
* Returns the **last** matching attribute to mirror React's behaviour where
|
|
44
|
+
* later props win, or `undefined` when the attribute is not present.
|
|
45
|
+
*
|
|
46
|
+
* Spread attributes are resolved when possible: if the spread argument is an
|
|
47
|
+
* identifier that resolves to an object expression, the object's properties
|
|
48
|
+
* are searched for a matching key.
|
|
49
|
+
*
|
|
50
|
+
* @param context - The ESLint rule context (needed for variable resolution in
|
|
51
|
+
* spread attributes).
|
|
52
|
+
* @param element - The `JSXElement` node to search.
|
|
53
|
+
* @param name - The attribute name to look for (e.g. `"className"`).
|
|
54
|
+
* @returns The matching `JSXAttribute` or `JSXSpreadAttribute`, or
|
|
55
|
+
* `undefined` when not found.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const attr = findAttribute(context, node, "sandbox");
|
|
60
|
+
* if (attr != null) {
|
|
61
|
+
* // attribute (or spread containing it) exists on the element
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
function findAttribute(context, element, name) {
|
|
66
|
+
return element.openingElement.attributes.findLast((attr) => {
|
|
67
|
+
if (attr.type === AST_NODE_TYPES.JSXAttribute) return getAttributeName(attr) === name;
|
|
68
|
+
switch (attr.argument.type) {
|
|
69
|
+
case AST_NODE_TYPES.Identifier: {
|
|
70
|
+
const initNode = resolve(context, attr.argument);
|
|
71
|
+
if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return ast.findProperty(initNode.properties, name) != null;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
case AST_NODE_TYPES.ObjectExpression: return ast.findProperty(attr.argument.properties, name) != null;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
8
79
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/find-parent-attribute.ts
|
|
82
|
+
/**
|
|
83
|
+
* Walk **up** the AST from `node` to find the nearest ancestor that is a
|
|
84
|
+
* `JSXAttribute` and (optionally) passes a predicate.
|
|
85
|
+
*
|
|
86
|
+
* This is useful when a rule visitor enters a deeply‑nested node (e.g. a
|
|
87
|
+
* `Literal` inside an expression container) and needs to know which JSX
|
|
88
|
+
* attribute it belongs to.
|
|
89
|
+
*
|
|
90
|
+
* @param node - The starting node for the upward search.
|
|
91
|
+
* @param test - Optional predicate to filter candidate `JSXAttribute` nodes.
|
|
92
|
+
* When omitted every `JSXAttribute` ancestor matches.
|
|
93
|
+
* @returns The first matching `JSXAttribute` ancestor, or `null` if none is
|
|
94
|
+
* found before reaching the root.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* // Inside a Literal visitor, find the owning attribute:
|
|
99
|
+
* const attr = findParentAttribute(literalNode);
|
|
100
|
+
* if (attr != null) {
|
|
101
|
+
* console.log(getAttributeName(attr));
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
function findParentAttribute(node, test = () => true) {
|
|
106
|
+
const guard = (n) => {
|
|
107
|
+
return n.type === AST_NODE_TYPES.JSXAttribute && test(n);
|
|
108
|
+
};
|
|
109
|
+
return findParent(node, guard);
|
|
25
110
|
}
|
|
26
111
|
|
|
27
|
-
|
|
28
|
-
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/resolve-attribute-value.ts
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the value of a JSX attribute (or spread attribute) into a
|
|
116
|
+
* {@link JsxAttributeValue} descriptor that can be inspected further.
|
|
117
|
+
*
|
|
118
|
+
* This is the low‑level building block – it operates on a single attribute
|
|
119
|
+
* node that the caller has already located. For the higher‑level "find by
|
|
120
|
+
* name **and** resolve" combo, see {@link getAttributeValue}.
|
|
121
|
+
*
|
|
122
|
+
* @param context - The ESLint rule context (needed for scope look‑ups).
|
|
123
|
+
* @param attribute - A `JSXAttribute` or `JSXSpreadAttribute` node.
|
|
124
|
+
* @returns A discriminated‑union descriptor of the attribute's value.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* import { findAttribute, resolveAttributeValue } from "@eslint-react/jsx";
|
|
129
|
+
*
|
|
130
|
+
* const attr = findAttribute(context, element, "sandbox");
|
|
131
|
+
* if (attr != null) {
|
|
132
|
+
* const value = resolveAttributeValue(context, attr);
|
|
133
|
+
* if (value.kind === "literal") {
|
|
134
|
+
* console.log(value.toStatic());
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function resolveAttributeValue(context, attribute) {
|
|
140
|
+
if (attribute.type === AST_NODE_TYPES.JSXAttribute) return resolveJsxAttribute(context, attribute);
|
|
141
|
+
return resolveJsxSpreadAttribute(context, attribute);
|
|
142
|
+
}
|
|
143
|
+
function resolveJsxAttribute(context, node) {
|
|
144
|
+
const scope = context.sourceCode.getScope(node);
|
|
145
|
+
if (node.value == null) return {
|
|
146
|
+
kind: "boolean",
|
|
147
|
+
node: null,
|
|
148
|
+
toStatic() {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
switch (node.value.type) {
|
|
153
|
+
case AST_NODE_TYPES.Literal: {
|
|
154
|
+
const staticValue = node.value.value;
|
|
155
|
+
return {
|
|
156
|
+
kind: "literal",
|
|
157
|
+
node: node.value,
|
|
158
|
+
toStatic() {
|
|
159
|
+
return staticValue;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
case AST_NODE_TYPES.JSXExpressionContainer: {
|
|
164
|
+
const expr = node.value.expression;
|
|
165
|
+
if (expr.type === AST_NODE_TYPES.JSXEmptyExpression) return {
|
|
166
|
+
kind: "missing",
|
|
167
|
+
node: expr,
|
|
168
|
+
toStatic() {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
kind: "unknown",
|
|
174
|
+
node: expr,
|
|
175
|
+
toStatic() {
|
|
176
|
+
return getStaticValue(expr, scope)?.value;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
case AST_NODE_TYPES.JSXElement: return {
|
|
181
|
+
kind: "element",
|
|
182
|
+
node: node.value,
|
|
183
|
+
toStatic() {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
case AST_NODE_TYPES.JSXSpreadChild: return {
|
|
188
|
+
kind: "spreadChild",
|
|
189
|
+
getChildren(_at) {
|
|
190
|
+
return null;
|
|
191
|
+
},
|
|
192
|
+
node: node.value.expression,
|
|
193
|
+
toStatic() {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function resolveJsxSpreadAttribute(context, node) {
|
|
200
|
+
const scope = context.sourceCode.getScope(node);
|
|
201
|
+
return {
|
|
202
|
+
kind: "spreadProps",
|
|
203
|
+
getProperty(name) {
|
|
204
|
+
return match(getStaticValue(node.argument, scope)?.value).with({ [name]: P.select(P.unknown) }, (v) => v).otherwise(() => null);
|
|
205
|
+
},
|
|
206
|
+
node: node.argument,
|
|
207
|
+
toStatic() {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
29
212
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/get-attribute-static-value.ts
|
|
215
|
+
/**
|
|
216
|
+
* Find an attribute by name on a JSX element and collapse its value to a
|
|
217
|
+
* plain JavaScript value in a single step.
|
|
218
|
+
*
|
|
219
|
+
* This is a convenience composition of {@link findAttribute} ->
|
|
220
|
+
* {@link resolveAttributeValue} -> `toStatic()`, with automatic handling
|
|
221
|
+
* of the `spreadProps` case (extracts the named property from the spread
|
|
222
|
+
* object).
|
|
223
|
+
*
|
|
224
|
+
* Returns `undefined` when the attribute is absent **or** when its value
|
|
225
|
+
* cannot be statically determined.
|
|
226
|
+
*
|
|
227
|
+
* @param context - The ESLint rule context.
|
|
228
|
+
* @param element - The `JSXElement` node to inspect.
|
|
229
|
+
* @param name - The attribute name to look up (e.g. `"className"`).
|
|
230
|
+
* @returns The static value of the attribute, or `undefined`.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* // <iframe sandbox="allow-scripts" />
|
|
235
|
+
* const sandbox = getAttributeStaticValue(context, node, "sandbox");
|
|
236
|
+
* // -> "allow-scripts"
|
|
237
|
+
*
|
|
238
|
+
* // <button type={dynamicVar} />
|
|
239
|
+
* const type = getAttributeStaticValue(context, node, "type");
|
|
240
|
+
* // -> undefined (cannot be resolved statically)
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
function getAttributeStaticValue(context, element, name) {
|
|
244
|
+
const attr = findAttribute(context, element, name);
|
|
245
|
+
if (attr == null) return void 0;
|
|
246
|
+
const resolved = resolveAttributeValue(context, attr);
|
|
247
|
+
if (resolved.kind === "spreadProps") return resolved.getProperty(name);
|
|
248
|
+
return resolved.toStatic();
|
|
50
249
|
}
|
|
51
250
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/get-attribute-value.ts
|
|
253
|
+
/**
|
|
254
|
+
* Find an attribute by name on a JSX element **and** resolve its value in a
|
|
255
|
+
* single call.
|
|
256
|
+
*
|
|
257
|
+
* This is a convenience composition of {@link findAttribute} and
|
|
258
|
+
* {@link resolveAttributeValue} that eliminates the most common two-step
|
|
259
|
+
* pattern in lint rules:
|
|
260
|
+
*
|
|
261
|
+
* ```ts
|
|
262
|
+
* const attr = findAttribute(context, element, name);
|
|
263
|
+
* if (attr == null) return;
|
|
264
|
+
* const value = resolveAttributeValue(context, attr);
|
|
265
|
+
* ```
|
|
266
|
+
*
|
|
267
|
+
* @param context - The ESLint rule context.
|
|
268
|
+
* @param element - The `JSXElement` node to search.
|
|
269
|
+
* @param name - The attribute name to look up (e.g. `"className"`).
|
|
270
|
+
* @returns A {@link JsxAttributeValue} descriptor, or `undefined` when the
|
|
271
|
+
* attribute is not present on the element.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* const value = getAttributeValue(context, node, "sandbox");
|
|
276
|
+
* if (value?.kind === "literal") {
|
|
277
|
+
* console.log(value.toStatic()); // the literal value
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
function getAttributeValue(context, element, name) {
|
|
282
|
+
const attr = findAttribute(context, element, name);
|
|
283
|
+
if (attr == null) return void 0;
|
|
284
|
+
return resolveAttributeValue(context, attr);
|
|
55
285
|
}
|
|
56
286
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/get-children.ts
|
|
289
|
+
/**
|
|
290
|
+
* Get the **meaningful** children of a JSX element or fragment.
|
|
291
|
+
*
|
|
292
|
+
* Filters out "padding spaces" — `JSXText` nodes that consist entirely of
|
|
293
|
+
* whitespace and contain at least one newline. These nodes are artefacts of
|
|
294
|
+
* source formatting that React trims away during rendering and are therefore
|
|
295
|
+
* not considered meaningful content.
|
|
296
|
+
*
|
|
297
|
+
* @param element - A `JSXElement` or `JSXFragment` node.
|
|
298
|
+
* @returns An array of children nodes that contribute to rendered output.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```ts
|
|
302
|
+
* import { getChildren } from "@eslint-react/jsx";
|
|
303
|
+
*
|
|
304
|
+
* // <div>
|
|
305
|
+
* // <span />
|
|
306
|
+
* // </div>
|
|
307
|
+
* //
|
|
308
|
+
* // Raw children: [JSXText("\n "), JSXElement(<span />), JSXText("\n")]
|
|
309
|
+
* // getChildren: [JSXElement(<span />)]
|
|
310
|
+
*
|
|
311
|
+
* const meaningful = getChildren(node);
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
function getChildren(element) {
|
|
315
|
+
return element.children.filter((child) => !isPaddingSpaces(child));
|
|
78
316
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
kind: "lazy",
|
|
93
|
-
node: node.value.expression,
|
|
94
|
-
initialScope
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return { kind: "none", node, initialScope };
|
|
98
|
-
case types.AST_NODE_TYPES.JSXSpreadAttribute: {
|
|
99
|
-
const staticValue = VAR__namespace.toStaticValue({
|
|
100
|
-
kind: "lazy",
|
|
101
|
-
node: node.argument,
|
|
102
|
-
initialScope
|
|
103
|
-
});
|
|
104
|
-
if (staticValue.kind === "none") {
|
|
105
|
-
return staticValue;
|
|
106
|
-
}
|
|
107
|
-
return tsPattern.match(staticValue.value).with({ [name]: tsPattern.P.select(tsPattern.P.any) }, (value) => ({
|
|
108
|
-
kind: "some",
|
|
109
|
-
node: node.argument,
|
|
110
|
-
initialScope,
|
|
111
|
-
value
|
|
112
|
-
})).otherwise(() => ({ kind: "none", node, initialScope }));
|
|
113
|
-
}
|
|
114
|
-
default:
|
|
115
|
-
return { kind: "none", node, initialScope };
|
|
116
|
-
}
|
|
317
|
+
/**
|
|
318
|
+
* A `JSXText` node is considered **padding spaces** when it is purely
|
|
319
|
+
* whitespace *and* contains at least one newline character.
|
|
320
|
+
*
|
|
321
|
+
* These nodes are formatting artefacts (indentation between JSX tags) that
|
|
322
|
+
* React discards at render time.
|
|
323
|
+
*
|
|
324
|
+
* @param node
|
|
325
|
+
* @internal
|
|
326
|
+
*/
|
|
327
|
+
function isPaddingSpaces(node) {
|
|
328
|
+
if (node.type !== AST_NODE_TYPES.JSXText) return false;
|
|
329
|
+
return node.raw.trim() === "" && node.raw.includes("\n");
|
|
117
330
|
}
|
|
118
331
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/get-element-type.ts
|
|
334
|
+
/**
|
|
335
|
+
* Get the string representation of a JSX element's type.
|
|
336
|
+
*
|
|
337
|
+
* - `<div>` -> `"div"`
|
|
338
|
+
* - `<Foo.Bar>` -> `"Foo.Bar"`
|
|
339
|
+
* - `<React.Fragment>` -> `"React.Fragment"`
|
|
340
|
+
* - `<></>` -> `""`
|
|
341
|
+
*
|
|
342
|
+
* @param node - A `JSXElement` or `JSXFragment` node.
|
|
343
|
+
* @returns The fully-qualified element type string.
|
|
344
|
+
*/
|
|
345
|
+
function getElementFullType(node) {
|
|
346
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return "";
|
|
347
|
+
function getQualifiedName(node) {
|
|
348
|
+
if (node.type === AST_NODE_TYPES.JSXIdentifier) return node.name;
|
|
349
|
+
if (node.type === AST_NODE_TYPES.JSXNamespacedName) return node.namespace.name + ":" + node.name.name;
|
|
350
|
+
return getQualifiedName(node.object) + "." + getQualifiedName(node.property);
|
|
351
|
+
}
|
|
352
|
+
return getQualifiedName(node.openingElement.name);
|
|
122
353
|
}
|
|
123
|
-
|
|
124
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Get the **self name** (last dot-separated segment) of a JSX element type.
|
|
356
|
+
*
|
|
357
|
+
* - `<Foo.Bar.Baz>` -> `"Baz"`
|
|
358
|
+
* - `<div>` -> `"div"`
|
|
359
|
+
* - `<></>` -> `""`
|
|
360
|
+
*
|
|
361
|
+
* @param node - A `JSXElement` or `JSXFragment` node.
|
|
362
|
+
* @returns The last segment of the element type, or `""` for fragments.
|
|
363
|
+
*/
|
|
364
|
+
function getElementSelfType(node) {
|
|
365
|
+
return getElementFullType(node).split(".").at(-1) ?? "";
|
|
125
366
|
}
|
|
126
|
-
|
|
127
|
-
|
|
367
|
+
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/has-any-attribute.ts
|
|
370
|
+
/**
|
|
371
|
+
* Check whether a JSX element carries **at least one** of the given attributes.
|
|
372
|
+
*
|
|
373
|
+
* This is a batch variant of {@link hasAttribute} for the common pattern of
|
|
374
|
+
* short-circuiting on multiple prop names:
|
|
375
|
+
*
|
|
376
|
+
* ```ts
|
|
377
|
+
* // before
|
|
378
|
+
* if (hasAttribute(ctx, el, "key")) return;
|
|
379
|
+
* if (hasAttribute(ctx, el, "ref")) return;
|
|
380
|
+
*
|
|
381
|
+
* // after
|
|
382
|
+
* if (hasAnyAttribute(ctx, el, ["key", "ref"])) return;
|
|
383
|
+
* ```
|
|
384
|
+
*
|
|
385
|
+
* Spread attributes are taken into account (see {@link findAttribute}).
|
|
386
|
+
*
|
|
387
|
+
* @param context - The ESLint rule context (needed for variable resolution in
|
|
388
|
+
* spread attributes).
|
|
389
|
+
* @param element - The `JSXElement` node to inspect.
|
|
390
|
+
* @param names - The attribute names to look for.
|
|
391
|
+
* @returns `true` when **at least one** of the attributes is present.
|
|
392
|
+
*/
|
|
393
|
+
function hasAnyAttribute(context, element, names) {
|
|
394
|
+
return names.some((name) => findAttribute(context, element, name) != null);
|
|
128
395
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/has-attribute.ts
|
|
399
|
+
/**
|
|
400
|
+
* Check whether a JSX element carries a given attribute (prop).
|
|
401
|
+
*
|
|
402
|
+
* This is a thin convenience wrapper around {@link findAttribute} for the
|
|
403
|
+
* common case where you only need a boolean answer.
|
|
404
|
+
*
|
|
405
|
+
* Spread attributes are taken into account: `<Comp {...{ disabled: true }} />`
|
|
406
|
+
* will report `true` for `"disabled"`.
|
|
407
|
+
*
|
|
408
|
+
* @param context - The ESLint rule context (needed for variable resolution in
|
|
409
|
+
* spread attributes).
|
|
410
|
+
* @param element - The `JSXElement` node to inspect.
|
|
411
|
+
* @param name - The attribute name to look for (e.g. `"className"`).
|
|
412
|
+
* @returns `true` when the attribute is present on the element.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```ts
|
|
416
|
+
* import { hasAttribute } from "@eslint-react/jsx";
|
|
417
|
+
*
|
|
418
|
+
* if (hasAttribute(context, node, "key")) {
|
|
419
|
+
* // element has a `key` prop
|
|
420
|
+
* }
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
function hasAttribute(context, element, name) {
|
|
424
|
+
return findAttribute(context, element, name) != null;
|
|
134
425
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
426
|
+
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/has-children.ts
|
|
429
|
+
/**
|
|
430
|
+
* Check whether a JSX element (or fragment) has **meaningful** children —
|
|
431
|
+
* that is, at least one child that is not purely whitespace text.
|
|
432
|
+
*
|
|
433
|
+
* A `JSXText` child whose `raw` content is empty after trimming is
|
|
434
|
+
* considered non-meaningful regardless of whether it contains a line break.
|
|
435
|
+
* This matches React's rendering behaviour where whitespace-only text nodes
|
|
436
|
+
* do not produce visible output.
|
|
437
|
+
*
|
|
438
|
+
* @param element - A `JSXElement` or `JSXFragment` node.
|
|
439
|
+
* @returns `true` when the element has at least one meaningful child.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* import { hasChildren } from "@eslint-react/jsx";
|
|
444
|
+
*
|
|
445
|
+
* // <div>hello</div> -> true
|
|
446
|
+
* // <div> {expr} </div> -> true
|
|
447
|
+
* // <div> </div> -> false (whitespace-only)
|
|
448
|
+
* // <div> -> false (whitespace-only, with newlines)
|
|
449
|
+
* // </div>
|
|
450
|
+
* // <div></div> -> false (no children at all)
|
|
451
|
+
*
|
|
452
|
+
* if (hasChildren(node)) {
|
|
453
|
+
* // element renders visible content
|
|
454
|
+
* }
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
function hasChildren(element) {
|
|
458
|
+
if (element.children.length === 0) return false;
|
|
459
|
+
return !element.children.every((child) => isWhitespaceText$1(child));
|
|
140
460
|
}
|
|
141
|
-
|
|
142
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Whether a JSX child node is a whitespace-only `JSXText` node.
|
|
463
|
+
*
|
|
464
|
+
* Any `JSXText` whose raw content consists entirely of whitespace characters
|
|
465
|
+
* (spaces, tabs, newlines, etc.) is considered whitespace text. Non-text
|
|
466
|
+
* nodes always return `false`.
|
|
467
|
+
* @param node
|
|
468
|
+
*/
|
|
469
|
+
function isWhitespaceText$1(node) {
|
|
470
|
+
if (node.type !== AST_NODE_TYPES.JSXText) return false;
|
|
471
|
+
return node.raw.trim() === "";
|
|
143
472
|
}
|
|
144
|
-
|
|
145
|
-
|
|
473
|
+
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/has-every-attribute.ts
|
|
476
|
+
/**
|
|
477
|
+
* Check whether a JSX element carries **all** of the given attributes (props).
|
|
478
|
+
*
|
|
479
|
+
* This is a batch variant of {@link hasAttribute} for the common pattern
|
|
480
|
+
* where a rule needs to verify that a set of required props are all present.
|
|
481
|
+
*
|
|
482
|
+
* Spread attributes are taken into account (see {@link findAttribute}).
|
|
483
|
+
*
|
|
484
|
+
* @param context - The ESLint rule context (needed for variable resolution in
|
|
485
|
+
* spread attributes).
|
|
486
|
+
* @param element - The `JSXElement` node to inspect.
|
|
487
|
+
* @param names - The attribute names to look for.
|
|
488
|
+
* @returns `true` when **every** name in `names` is present on the element.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```ts
|
|
492
|
+
* import { hasEveryAttribute } from "@eslint-react/jsx";
|
|
493
|
+
*
|
|
494
|
+
* // Ensure both `alt` and `src` are provided on an <img>
|
|
495
|
+
* if (hasEveryAttribute(context, node, ["alt", "src"])) {
|
|
496
|
+
* // element has both props
|
|
497
|
+
* }
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
function hasEveryAttribute(context, element, names) {
|
|
501
|
+
return names.every((name) => findAttribute(context, element, name) != null);
|
|
146
502
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
503
|
+
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/is-element.ts
|
|
506
|
+
/**
|
|
507
|
+
* Check whether a node is a `JSXElement` (or `JSXFragment`) and optionally
|
|
508
|
+
* matches a given test.
|
|
509
|
+
*
|
|
510
|
+
* Modelled after
|
|
511
|
+
* [`hast-util-is-element`](https://github.com/syntax-tree/hast-util-is-element):
|
|
512
|
+
* the `test` parameter controls what counts as a match.
|
|
513
|
+
*
|
|
514
|
+
* When called **without** a test, the function acts as a simple type-guard
|
|
515
|
+
* for `JSXElement | JSXFragment`.
|
|
516
|
+
*
|
|
517
|
+
* @param node - The AST node to test.
|
|
518
|
+
* @param test - Optional test to match the element type against.
|
|
519
|
+
* @returns `true` when the node is a matching JSX element.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```ts
|
|
523
|
+
* import { isElement } from "@eslint-react/jsx";
|
|
524
|
+
*
|
|
525
|
+
* // Type-guard only — any JSX element or fragment
|
|
526
|
+
* if (isElement(node)) { … }
|
|
527
|
+
*
|
|
528
|
+
* // Match a single tag name
|
|
529
|
+
* if (isElement(node, "iframe")) { … }
|
|
530
|
+
*
|
|
531
|
+
* // Match one of several tag names
|
|
532
|
+
* if (isElement(node, ["button", "input", "select"])) { … }
|
|
533
|
+
*
|
|
534
|
+
* // Custom predicate
|
|
535
|
+
* if (isElement(node, (type) => type.endsWith(".Provider"))) { … }
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
function isElement(node, test) {
|
|
539
|
+
if (node == null) return false;
|
|
540
|
+
if (node.type !== AST_NODE_TYPES.JSXElement && node.type !== AST_NODE_TYPES.JSXFragment) return false;
|
|
541
|
+
if (test == null) return true;
|
|
542
|
+
const elementType = getElementFullType(node);
|
|
543
|
+
if (typeof test === "string") return elementType === test;
|
|
544
|
+
if (typeof test === "function") return test(elementType, node);
|
|
545
|
+
return test.includes(elementType);
|
|
152
546
|
}
|
|
153
547
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/is-fragment-element.ts
|
|
550
|
+
/**
|
|
551
|
+
* Check whether a node is a React **Fragment** element.
|
|
552
|
+
*
|
|
553
|
+
* Recognises both the shorthand `<>…</>` syntax (`JSXFragment`) and the
|
|
554
|
+
* explicit `<Fragment>` / `<React.Fragment>` form (`JSXElement`).
|
|
555
|
+
*
|
|
556
|
+
* The comparison is performed against the **self name** (last dot‑separated
|
|
557
|
+
* segment) of both the node and the configured factory, so
|
|
558
|
+
* `<React.Fragment>` matches `"React.Fragment"` and `<Fragment>` matches
|
|
559
|
+
* `"Fragment"`.
|
|
560
|
+
*
|
|
561
|
+
* @param node - The AST node to test.
|
|
562
|
+
* @param jsxFragmentFactory - The configured fragment factory string
|
|
563
|
+
* (e.g. `"React.Fragment"`). Defaults to
|
|
564
|
+
* `"React.Fragment"`.
|
|
565
|
+
* @returns `true` when the node represents a React Fragment.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* // Using the default factory
|
|
570
|
+
* if (isFragmentElement(node)) { … }
|
|
571
|
+
*
|
|
572
|
+
* // With a custom factory from jsxConfig
|
|
573
|
+
* const config = getJsxConfig(context);
|
|
574
|
+
* if (isFragmentElement(node, config.jsxFragmentFactory)) { … }
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
function isFragmentElement(node, jsxFragmentFactory = "React.Fragment") {
|
|
578
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return true;
|
|
579
|
+
if (node.type !== AST_NODE_TYPES.JSXElement) return false;
|
|
580
|
+
const fragment = jsxFragmentFactory.split(".").at(-1) ?? "Fragment";
|
|
581
|
+
return getElementFullType(node).split(".").at(-1) === fragment;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/is-host-element.ts
|
|
586
|
+
/**
|
|
587
|
+
* Check whether a node is a **host** (intrinsic / DOM) element.
|
|
588
|
+
*
|
|
589
|
+
* A host element is a `JSXElement` whose tag name is a plain `JSXIdentifier`
|
|
590
|
+
* starting with a lowercase letter – the same heuristic React uses to
|
|
591
|
+
* distinguish `<div>` from `<MyComponent>`.
|
|
592
|
+
*
|
|
593
|
+
* @param node - The AST node to test.
|
|
594
|
+
* @returns `true` when the node is a `JSXElement` with a lowercase tag name.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* ```ts
|
|
598
|
+
* // <div className="box" /> -> true
|
|
599
|
+
* // <span /> -> true
|
|
600
|
+
* // <MyComponent /> -> false
|
|
601
|
+
* // <Foo.Bar /> -> false
|
|
602
|
+
* isHostElement(node);
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
function isHostElement(node) {
|
|
606
|
+
return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/jsx-detection-hint.ts
|
|
611
|
+
const JsxDetectionHint = {
|
|
612
|
+
None: 0n,
|
|
613
|
+
DoNotIncludeJsxWithNullValue: 1n << 0n,
|
|
614
|
+
DoNotIncludeJsxWithNumberValue: 1n << 1n,
|
|
615
|
+
DoNotIncludeJsxWithBigIntValue: 1n << 2n,
|
|
616
|
+
DoNotIncludeJsxWithStringValue: 1n << 3n,
|
|
617
|
+
DoNotIncludeJsxWithBooleanValue: 1n << 4n,
|
|
618
|
+
DoNotIncludeJsxWithUndefinedValue: 1n << 5n,
|
|
619
|
+
DoNotIncludeJsxWithEmptyArrayValue: 1n << 6n,
|
|
620
|
+
DoNotIncludeJsxWithCreateElementValue: 1n << 7n,
|
|
621
|
+
RequireAllArrayElementsToBeJsx: 1n << 8n,
|
|
622
|
+
RequireBothSidesOfLogicalExpressionToBeJsx: 1n << 9n,
|
|
623
|
+
RequireBothBranchesOfConditionalExpressionToBeJsx: 1n << 10n
|
|
168
624
|
};
|
|
169
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Default JSX detection configuration.
|
|
627
|
+
*
|
|
628
|
+
* Skips number, bigint, boolean, string, and undefined literals –
|
|
629
|
+
* the value types that are commonly returned alongside JSX in React
|
|
630
|
+
* components but are not themselves renderable elements.
|
|
631
|
+
*/
|
|
632
|
+
const DEFAULT_JSX_DETECTION_HINT = 0n | JsxDetectionHint.DoNotIncludeJsxWithNumberValue | JsxDetectionHint.DoNotIncludeJsxWithBigIntValue | JsxDetectionHint.DoNotIncludeJsxWithBooleanValue | JsxDetectionHint.DoNotIncludeJsxWithStringValue | JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue;
|
|
633
|
+
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/is-jsx-like.ts
|
|
636
|
+
/**
|
|
637
|
+
* Determine whether a node represents JSX-like content based on heuristics.
|
|
638
|
+
*
|
|
639
|
+
* The detection behaviour is configurable through {@link JsxDetectionHint}
|
|
640
|
+
* bit-flags so that callers can opt individual value kinds in or out.
|
|
641
|
+
*
|
|
642
|
+
* @param context - The ESLint rule context (needed for variable resolution).
|
|
643
|
+
* @param node - The AST node to analyse.
|
|
644
|
+
* @param hint - Optional bit-flags to adjust detection behaviour.
|
|
645
|
+
* Defaults to {@link DEFAULT_JSX_DETECTION_HINT}.
|
|
646
|
+
* @returns Whether the node is considered JSX-like.
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* ```ts
|
|
650
|
+
* import { isJsxLike } from "@eslint-react/jsx";
|
|
651
|
+
*
|
|
652
|
+
* if (isJsxLike(context, node)) {
|
|
653
|
+
* // node looks like it evaluates to a React element
|
|
654
|
+
* }
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
function isJsxLike(context, node, hint = DEFAULT_JSX_DETECTION_HINT) {
|
|
658
|
+
if (node == null) return false;
|
|
659
|
+
if (ast.isJSX(node)) return true;
|
|
660
|
+
switch (node.type) {
|
|
661
|
+
case AST_NODE_TYPES.Literal:
|
|
662
|
+
switch (typeof node.value) {
|
|
663
|
+
case "boolean": return !(hint & JsxDetectionHint.DoNotIncludeJsxWithBooleanValue);
|
|
664
|
+
case "string": return !(hint & JsxDetectionHint.DoNotIncludeJsxWithStringValue);
|
|
665
|
+
case "number": return !(hint & JsxDetectionHint.DoNotIncludeJsxWithNumberValue);
|
|
666
|
+
case "bigint": return !(hint & JsxDetectionHint.DoNotIncludeJsxWithBigIntValue);
|
|
667
|
+
}
|
|
668
|
+
if (node.value == null) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithNullValue);
|
|
669
|
+
return false;
|
|
670
|
+
case AST_NODE_TYPES.TemplateLiteral: return !(hint & JsxDetectionHint.DoNotIncludeJsxWithStringValue);
|
|
671
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
672
|
+
if (node.elements.length === 0) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue);
|
|
673
|
+
if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return node.elements.every((n) => isJsxLike(context, n, hint));
|
|
674
|
+
return node.elements.some((n) => isJsxLike(context, n, hint));
|
|
675
|
+
case AST_NODE_TYPES.LogicalExpression:
|
|
676
|
+
if (hint & JsxDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx) return isJsxLike(context, node.left, hint) && isJsxLike(context, node.right, hint);
|
|
677
|
+
return isJsxLike(context, node.left, hint) || isJsxLike(context, node.right, hint);
|
|
678
|
+
case AST_NODE_TYPES.ConditionalExpression: {
|
|
679
|
+
const consequentIsJsx = Array.isArray(node.consequent) ? checkArray(context, node.consequent, hint) : isJsxLike(context, node.consequent, hint);
|
|
680
|
+
const alternateIsJsx = isJsxLike(context, node.alternate, hint);
|
|
681
|
+
if (hint & JsxDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx) return consequentIsJsx && alternateIsJsx;
|
|
682
|
+
return consequentIsJsx || alternateIsJsx;
|
|
683
|
+
}
|
|
684
|
+
case AST_NODE_TYPES.SequenceExpression: return isJsxLike(context, node.expressions.at(-1) ?? null, hint);
|
|
685
|
+
case AST_NODE_TYPES.CallExpression:
|
|
686
|
+
if (hint & JsxDetectionHint.DoNotIncludeJsxWithCreateElementValue) return false;
|
|
687
|
+
switch (node.callee.type) {
|
|
688
|
+
case AST_NODE_TYPES.Identifier: return node.callee.name === "createElement";
|
|
689
|
+
case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "createElement";
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
case AST_NODE_TYPES.Identifier:
|
|
693
|
+
if (node.name === "undefined") return !(hint & JsxDetectionHint.DoNotIncludeJsxWithUndefinedValue);
|
|
694
|
+
if (ast.isJSXTagNameExpression(node)) return true;
|
|
695
|
+
return isJsxLike(context, resolve(context, node), hint);
|
|
696
|
+
}
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
function checkArray(context, elements, hint) {
|
|
700
|
+
if (elements.length === 0) return !(hint & JsxDetectionHint.DoNotIncludeJsxWithEmptyArrayValue);
|
|
701
|
+
if (hint & JsxDetectionHint.RequireAllArrayElementsToBeJsx) return elements.every((n) => isJsxLike(context, n, hint));
|
|
702
|
+
return elements.some((n) => isJsxLike(context, n, hint));
|
|
703
|
+
}
|
|
170
704
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
types.AST_NODE_TYPES.JSXOpeningElement,
|
|
184
|
-
types.AST_NODE_TYPES.JSXOpeningFragment,
|
|
185
|
-
types.AST_NODE_TYPES.JSXSpreadAttribute,
|
|
186
|
-
types.AST_NODE_TYPES.JSXSpreadChild,
|
|
187
|
-
types.AST_NODE_TYPES.JSXText
|
|
188
|
-
]);
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/is-jsx-text.ts
|
|
707
|
+
/**
|
|
708
|
+
* Check whether a node is a JSX text node.
|
|
709
|
+
*
|
|
710
|
+
* Returns `true` for both `JSXText` nodes and `Literal` nodes that appear
|
|
711
|
+
* as direct children of a JSX element (the parser may represent inline text
|
|
712
|
+
* with either node type depending on context).
|
|
713
|
+
*
|
|
714
|
+
* @param node - The AST node to test.
|
|
715
|
+
* @returns `true` when `node` is a `JSXText` or `Literal`.
|
|
716
|
+
*/
|
|
189
717
|
function isJsxText(node) {
|
|
190
|
-
|
|
191
|
-
|
|
718
|
+
if (node == null) return false;
|
|
719
|
+
return node.type === AST_NODE_TYPES.JSXText || node.type === AST_NODE_TYPES.Literal;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/is-whitespace.ts
|
|
724
|
+
/**
|
|
725
|
+
* Check whether a JSX child node is **whitespace padding** that React would
|
|
726
|
+
* trim away during rendering.
|
|
727
|
+
*
|
|
728
|
+
* A child is considered whitespace padding when it is a `JSXText` node whose
|
|
729
|
+
* raw content is empty after trimming **and** contains at least one newline.
|
|
730
|
+
* This is the whitespace that appears between JSX tags purely for formatting:
|
|
731
|
+
*
|
|
732
|
+
* ```jsx
|
|
733
|
+
* <div>
|
|
734
|
+
* <span /> ← the text between </span> and the next tag is padding
|
|
735
|
+
* <span />
|
|
736
|
+
* </div>
|
|
737
|
+
* ```
|
|
738
|
+
*
|
|
739
|
+
* Use {@link isWhitespaceText} for a looser check that also matches
|
|
740
|
+
* whitespace‑only text that does **not** contain a newline.
|
|
741
|
+
*
|
|
742
|
+
* @param node - A JSX child node.
|
|
743
|
+
* @returns `true` when the node is purely formatting whitespace.
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* import { isWhitespace } from "@eslint-react/jsx";
|
|
748
|
+
*
|
|
749
|
+
* const meaningful = element.children.filter(
|
|
750
|
+
* (child) => !isWhitespace(child),
|
|
751
|
+
* );
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
754
|
+
function isWhitespace(node) {
|
|
755
|
+
if (node.type !== AST_NODE_TYPES.JSXText) return false;
|
|
756
|
+
return node.raw.trim() === "" && node.raw.includes("\n");
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Check whether a JSX child node is **any** whitespace‑only text.
|
|
760
|
+
*
|
|
761
|
+
* This is a looser variant of {@link isWhitespace} — it matches every
|
|
762
|
+
* `JSXText` node whose raw content is empty after trimming, regardless of
|
|
763
|
+
* whether it contains a newline.
|
|
764
|
+
*
|
|
765
|
+
* @param node - A JSX child node.
|
|
766
|
+
* @returns `true` when the node is a whitespace‑only `JSXText`.
|
|
767
|
+
*/
|
|
768
|
+
function isWhitespaceText(node) {
|
|
769
|
+
if (node.type !== AST_NODE_TYPES.JSXText) return false;
|
|
770
|
+
return node.raw.trim() === "";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/jsx-config.ts
|
|
775
|
+
/**
|
|
776
|
+
* TypeScript `jsx` compiler option values.
|
|
777
|
+
*
|
|
778
|
+
* Mirrors `ts.JsxEmit` so that consumers do not need a direct dependency on
|
|
779
|
+
* the TypeScript compiler.
|
|
780
|
+
*/
|
|
781
|
+
const JsxEmit = {
|
|
782
|
+
None: 0,
|
|
783
|
+
Preserve: 1,
|
|
784
|
+
React: 2,
|
|
785
|
+
ReactNative: 3,
|
|
786
|
+
ReactJSX: 4,
|
|
787
|
+
ReactJSXDev: 5
|
|
788
|
+
};
|
|
789
|
+
/**
|
|
790
|
+
* Weak‑map cache keyed by `sourceCode` so that the (potentially expensive)
|
|
791
|
+
* pragma‑scanning pass runs at most once per file.
|
|
792
|
+
*/
|
|
793
|
+
const annotationCache = /* @__PURE__ */ new WeakMap();
|
|
794
|
+
/**
|
|
795
|
+
* Weak‑map cache for the fully‑merged config (compiler options + annotation).
|
|
796
|
+
*/
|
|
797
|
+
const mergedCache = /* @__PURE__ */ new WeakMap();
|
|
798
|
+
/**
|
|
799
|
+
* Read JSX configuration from the TypeScript compiler options exposed by the
|
|
800
|
+
* parser services.
|
|
801
|
+
*
|
|
802
|
+
* Falls back to sensible React defaults when no compiler options are
|
|
803
|
+
* available (e.g. when the file is parsed without type information).
|
|
804
|
+
*
|
|
805
|
+
* @param context - The ESLint rule context.
|
|
806
|
+
* @returns Fully‑populated `JsxConfig` derived from compiler options.
|
|
807
|
+
*/
|
|
808
|
+
function getJsxConfigFromCompilerOptions(context) {
|
|
809
|
+
const options = context.sourceCode.parserServices?.program?.getCompilerOptions() ?? {};
|
|
810
|
+
return {
|
|
811
|
+
jsx: options.jsx ?? JsxEmit.ReactJSX,
|
|
812
|
+
jsxFactory: options.jsxFactory ?? "React.createElement",
|
|
813
|
+
jsxFragmentFactory: options.jsxFragmentFactory ?? "React.Fragment",
|
|
814
|
+
jsxImportSource: options.jsxImportSource ?? "react"
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Extract JSX configuration from `@jsx`, `@jsxFrag`, `@jsxRuntime` and
|
|
819
|
+
* `@jsxImportSource` pragma comments in the source file.
|
|
820
|
+
*
|
|
821
|
+
* The result is cached per `sourceCode` instance via a `WeakMap` so that
|
|
822
|
+
* repeated calls from different rules analysing the same file are free.
|
|
823
|
+
*
|
|
824
|
+
* @param context - The ESLint rule context.
|
|
825
|
+
* @returns Partial `JsxConfig` containing only the values found in pragmas.
|
|
826
|
+
*/
|
|
827
|
+
function getJsxConfigFromAnnotation(context) {
|
|
828
|
+
const cached = annotationCache.get(context.sourceCode);
|
|
829
|
+
if (cached != null) return cached;
|
|
830
|
+
const options = {};
|
|
831
|
+
if (!context.sourceCode.text.includes("@jsx")) {
|
|
832
|
+
annotationCache.set(context.sourceCode, options);
|
|
833
|
+
return options;
|
|
834
|
+
}
|
|
835
|
+
let jsx, jsxFrag, jsxRuntime, jsxImportSource;
|
|
836
|
+
for (const comment of context.sourceCode.getAllComments().reverse()) {
|
|
837
|
+
const value = comment.value;
|
|
838
|
+
jsx ??= value.match(RE_ANNOTATION_JSX)?.[1];
|
|
839
|
+
jsxFrag ??= value.match(RE_ANNOTATION_JSX_FRAG)?.[1];
|
|
840
|
+
jsxRuntime ??= value.match(RE_ANNOTATION_JSX_RUNTIME)?.[1];
|
|
841
|
+
jsxImportSource ??= value.match(RE_ANNOTATION_JSX_IMPORT_SOURCE)?.[1];
|
|
842
|
+
}
|
|
843
|
+
if (jsx != null) options.jsxFactory = jsx;
|
|
844
|
+
if (jsxFrag != null) options.jsxFragmentFactory = jsxFrag;
|
|
845
|
+
if (jsxRuntime != null) options.jsx = jsxRuntime === "classic" ? JsxEmit.React : JsxEmit.ReactJSX;
|
|
846
|
+
if (jsxImportSource != null) options.jsxImportSource = jsxImportSource;
|
|
847
|
+
annotationCache.set(context.sourceCode, options);
|
|
848
|
+
return options;
|
|
192
849
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
case types.AST_NODE_TYPES.TemplateLiteral: {
|
|
214
|
-
return !(hint & JSXDetectionHint.SkipStringLiteral);
|
|
215
|
-
}
|
|
216
|
-
case types.AST_NODE_TYPES.ArrayExpression: {
|
|
217
|
-
if (hint & JSXDetectionHint.StrictArray) {
|
|
218
|
-
return node.elements.every((n) => isJsxLike(code, n, hint));
|
|
219
|
-
}
|
|
220
|
-
return node.elements.some((n) => isJsxLike(code, n, hint));
|
|
221
|
-
}
|
|
222
|
-
case types.AST_NODE_TYPES.LogicalExpression: {
|
|
223
|
-
if (hint & JSXDetectionHint.StrictLogical) {
|
|
224
|
-
return isJsxLike(code, node.left, hint) && isJsxLike(code, node.right, hint);
|
|
225
|
-
}
|
|
226
|
-
return isJsxLike(code, node.left, hint) || isJsxLike(code, node.right, hint);
|
|
227
|
-
}
|
|
228
|
-
case types.AST_NODE_TYPES.ConditionalExpression: {
|
|
229
|
-
let leftHasJSX2 = function(node2) {
|
|
230
|
-
if (Array.isArray(node2.consequent)) {
|
|
231
|
-
if (node2.consequent.length === 0) {
|
|
232
|
-
return !(hint & JSXDetectionHint.SkipEmptyArray);
|
|
233
|
-
}
|
|
234
|
-
if (hint & JSXDetectionHint.StrictArray) {
|
|
235
|
-
return node2.consequent.every((n) => isJsxLike(code, n, hint));
|
|
236
|
-
}
|
|
237
|
-
return node2.consequent.some((n) => isJsxLike(code, n, hint));
|
|
238
|
-
}
|
|
239
|
-
return isJsxLike(code, node2.consequent, hint);
|
|
240
|
-
}, rightHasJSX2 = function(node2) {
|
|
241
|
-
return isJsxLike(code, node2.alternate, hint);
|
|
242
|
-
};
|
|
243
|
-
if (hint & JSXDetectionHint.StrictConditional) {
|
|
244
|
-
return leftHasJSX2(node) && rightHasJSX2(node);
|
|
245
|
-
}
|
|
246
|
-
return leftHasJSX2(node) || rightHasJSX2(node);
|
|
247
|
-
}
|
|
248
|
-
case types.AST_NODE_TYPES.SequenceExpression: {
|
|
249
|
-
const exp = node.expressions.at(-1);
|
|
250
|
-
return isJsxLike(code, exp, hint);
|
|
251
|
-
}
|
|
252
|
-
case types.AST_NODE_TYPES.CallExpression: {
|
|
253
|
-
if (hint & JSXDetectionHint.SkipCreateElement) {
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
switch (node.callee.type) {
|
|
257
|
-
case types.AST_NODE_TYPES.Identifier:
|
|
258
|
-
return node.callee.name === "createElement";
|
|
259
|
-
case types.AST_NODE_TYPES.MemberExpression:
|
|
260
|
-
return node.callee.property.type === types.AST_NODE_TYPES.Identifier && node.callee.property.name === "createElement";
|
|
261
|
-
}
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
case types.AST_NODE_TYPES.Identifier: {
|
|
265
|
-
const { name } = node;
|
|
266
|
-
if (name === "undefined") {
|
|
267
|
-
return !(hint & JSXDetectionHint.SkipUndefined);
|
|
268
|
-
}
|
|
269
|
-
if (AST2__namespace.isJSXTagNameExpression(node)) {
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
const variable = VAR__namespace.findVariable(name, code.getScope(node));
|
|
273
|
-
const variableNode = variable && VAR__namespace.getVariableInitNode(variable, 0);
|
|
274
|
-
return !!variableNode && isJsxLike(code, variableNode, hint);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return false;
|
|
850
|
+
/**
|
|
851
|
+
* Get the fully‑merged JSX configuration for the current file.
|
|
852
|
+
*
|
|
853
|
+
* Compiler options provide the base values; pragma annotations found in the
|
|
854
|
+
* source override them where present. The result is cached per `sourceCode`.
|
|
855
|
+
*
|
|
856
|
+
* This is the main entry‑point most consumers should use.
|
|
857
|
+
*
|
|
858
|
+
* @param context - The ESLint rule context.
|
|
859
|
+
* @returns Fully‑populated, merged `JsxConfig`.
|
|
860
|
+
*/
|
|
861
|
+
function getJsxConfig(context) {
|
|
862
|
+
const cached = mergedCache.get(context.sourceCode);
|
|
863
|
+
if (cached != null) return cached;
|
|
864
|
+
const merged = {
|
|
865
|
+
...getJsxConfigFromCompilerOptions(context),
|
|
866
|
+
...getJsxConfigFromAnnotation(context)
|
|
867
|
+
};
|
|
868
|
+
mergedCache.set(context.sourceCode, merged);
|
|
869
|
+
return merged;
|
|
278
870
|
}
|
|
279
871
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
exports.findParentAttribute = findParentAttribute;
|
|
283
|
-
exports.getAttribute = getAttribute;
|
|
284
|
-
exports.getAttributeName = getAttributeName;
|
|
285
|
-
exports.getAttributeValue = getAttributeValue;
|
|
286
|
-
exports.getElementType = getElementType;
|
|
287
|
-
exports.hasAnyAttribute = hasAnyAttribute;
|
|
288
|
-
exports.hasAttribute = hasAttribute;
|
|
289
|
-
exports.hasEveryAttribute = hasEveryAttribute;
|
|
290
|
-
exports.isFragmentElement = isFragmentElement;
|
|
291
|
-
exports.isHostElement = isHostElement;
|
|
292
|
-
exports.isJSX = isJSX;
|
|
293
|
-
exports.isJsxLike = isJsxLike;
|
|
294
|
-
exports.isJsxText = isJsxText;
|
|
295
|
-
exports.isKeyedElement = isKeyedElement;
|
|
296
|
-
exports.toString = toString;
|
|
872
|
+
//#endregion
|
|
873
|
+
export { DEFAULT_JSX_DETECTION_HINT, JsxDetectionHint, JsxEmit, findAttribute, findParentAttribute, getAttributeName, getAttributeStaticValue, getAttributeValue, getChildren, getElementFullType, getElementSelfType, getJsxConfig, getJsxConfigFromAnnotation, getJsxConfigFromCompilerOptions, hasAnyAttribute, hasAttribute, hasChildren, hasEveryAttribute, isElement, isFragmentElement, isHostElement, isJsxLike, isJsxText, isWhitespace, isWhitespaceText, resolveAttributeValue };
|