eslint-plugin-react-jsx 5.16.0 → 5.17.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.js +204 -250
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
|
|
2
2
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
import { Check, Extract } from "@eslint-react/ast";
|
|
3
4
|
import * as core from "@eslint-react/core";
|
|
4
|
-
import "@eslint-react/eslint";
|
|
5
|
-
import { collapseMultilineText, findAttribute, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
6
5
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
7
|
-
import
|
|
6
|
+
import "@eslint-react/eslint";
|
|
7
|
+
import { collapseMultilineText, findAttribute, getAttributeName, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
8
8
|
import ts from "typescript";
|
|
9
9
|
|
|
10
10
|
//#region \0rolldown/runtime.js
|
|
@@ -26,7 +26,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region package.json
|
|
28
28
|
var name$2 = "eslint-plugin-react-jsx";
|
|
29
|
-
var version = "5.
|
|
29
|
+
var version = "5.17.0";
|
|
30
30
|
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/utils/create-rule.ts
|
|
@@ -36,46 +36,36 @@ function getDocsUrl(ruleName) {
|
|
|
36
36
|
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
|
-
//#region src/
|
|
39
|
+
//#region src/utils/find-create-element-children-prop.ts
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @param
|
|
43
|
-
* @
|
|
41
|
+
* Finds the statically named `children` property in the props object of a `createElement` call.
|
|
42
|
+
* @param context The rule context
|
|
43
|
+
* @param node The `CallExpression` node to inspect
|
|
44
|
+
* @returns The `children` property node, or `null` when the call has no static `children` prop
|
|
44
45
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
function findCreateElementChildrenProp(context, node) {
|
|
47
|
+
if (!core.isCreateElementCall(context, node)) return null;
|
|
48
|
+
const propsArg = node.arguments[1];
|
|
49
|
+
if (propsArg == null) return null;
|
|
50
|
+
const propsObject = Extract.unwrap(propsArg);
|
|
51
|
+
if (propsObject.type !== AST_NODE_TYPES.ObjectExpression) return null;
|
|
52
|
+
for (const prop of propsObject.properties) if (prop.type === AST_NODE_TYPES.Property && Extract.getStaticPropertyName(prop) === "children") return prop;
|
|
52
53
|
return null;
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/utils/remove-jsx-attribute.ts
|
|
54
58
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @param
|
|
58
|
-
* @param
|
|
59
|
-
|
|
60
|
-
function getPropRemovalRange$1(context, prop) {
|
|
61
|
-
const { sourceCode } = context;
|
|
62
|
-
let start = prop.range[0];
|
|
63
|
-
const end = prop.range[1];
|
|
64
|
-
while (start > 0 && /\s/.test(sourceCode.text[start - 1])) start--;
|
|
65
|
-
return [start, end];
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Compute the range covering **all** children content of a JSX element or
|
|
69
|
-
* fragment (from the start of the first child to the end of the last child).
|
|
70
|
-
*
|
|
71
|
-
* Returns `null` when there are no children at all.
|
|
72
|
-
* @param node The JSX element or fragment.
|
|
59
|
+
* Builds a fix that removes a JSX attribute along with the whitespace before it.
|
|
60
|
+
* @param context The rule context
|
|
61
|
+
* @param fixer The rule fixer
|
|
62
|
+
* @param attribute The `JSXAttribute` node to remove
|
|
63
|
+
* @returns A fix that removes the attribute and its preceding whitespace
|
|
73
64
|
*/
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return [first.range[0], last.range[1]];
|
|
65
|
+
function removeJsxAttribute(context, fixer, attribute) {
|
|
66
|
+
let start = attribute.range[0];
|
|
67
|
+
while (start > 0 && /\s/.test(context.sourceCode.text[start - 1] ?? "")) start--;
|
|
68
|
+
return fixer.removeRange([start, attribute.range[1]]);
|
|
79
69
|
}
|
|
80
70
|
|
|
81
71
|
//#endregion
|
|
@@ -101,12 +91,9 @@ var no_children_prop_with_children_default = createRule({
|
|
|
101
91
|
function create$7(context) {
|
|
102
92
|
return {
|
|
103
93
|
CallExpression(node) {
|
|
104
|
-
|
|
105
|
-
const [, propsArg, firstExtra] = node.arguments;
|
|
106
|
-
if (propsArg == null || propsArg.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
107
|
-
const childrenProp = findChildrenProperty$1(propsArg);
|
|
94
|
+
const childrenProp = findCreateElementChildrenProp(context, node);
|
|
108
95
|
if (childrenProp == null) return;
|
|
109
|
-
if (
|
|
96
|
+
if (node.arguments[2] == null) return;
|
|
110
97
|
context.report({
|
|
111
98
|
messageId: "default",
|
|
112
99
|
node: childrenProp
|
|
@@ -114,8 +101,7 @@ function create$7(context) {
|
|
|
114
101
|
},
|
|
115
102
|
JSXElement(node) {
|
|
116
103
|
const childrenProp = findAttribute(context, node, "children");
|
|
117
|
-
if (childrenProp == null) return;
|
|
118
|
-
if (!hasChildren(node)) return;
|
|
104
|
+
if (childrenProp == null || !hasChildren(node)) return;
|
|
119
105
|
if (childrenProp.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
120
106
|
context.report({
|
|
121
107
|
messageId: "default",
|
|
@@ -127,16 +113,14 @@ function create$7(context) {
|
|
|
127
113
|
messageId: "default",
|
|
128
114
|
node: childrenProp,
|
|
129
115
|
suggest: [{
|
|
130
|
-
fix(fixer)
|
|
131
|
-
const [start, end] = getPropRemovalRange$1(context, childrenProp);
|
|
132
|
-
return fixer.removeRange([start, end]);
|
|
133
|
-
},
|
|
116
|
+
fix: (fixer) => removeJsxAttribute(context, fixer, childrenProp),
|
|
134
117
|
messageId: "removeChildrenProp"
|
|
135
118
|
}, {
|
|
136
119
|
fix(fixer) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
120
|
+
const first = node.children.at(0);
|
|
121
|
+
const last = node.children.at(-1);
|
|
122
|
+
if (first == null || last == null) return [];
|
|
123
|
+
return fixer.removeRange([first.range[0], last.range[1]]);
|
|
140
124
|
},
|
|
141
125
|
messageId: "removeChildrenContent"
|
|
142
126
|
}]
|
|
@@ -145,61 +129,6 @@ function create$7(context) {
|
|
|
145
129
|
};
|
|
146
130
|
}
|
|
147
131
|
|
|
148
|
-
//#endregion
|
|
149
|
-
//#region src/rules/no-children-prop/lib.ts
|
|
150
|
-
/**
|
|
151
|
-
* Find a `children` property inside an ObjectExpression.
|
|
152
|
-
* @param node The object expression.
|
|
153
|
-
* @returns The Property node whose key is "children", or null.
|
|
154
|
-
*/
|
|
155
|
-
function findChildrenProperty(node) {
|
|
156
|
-
for (const prop of node.properties) {
|
|
157
|
-
if (prop.type !== AST_NODE_TYPES.Property) continue;
|
|
158
|
-
const key = Extract.unwrap(prop.key);
|
|
159
|
-
if (key.type === AST_NODE_TYPES.Identifier && key.name === "children") return prop;
|
|
160
|
-
if (key.type === AST_NODE_TYPES.Literal && key.value === "children") return prop;
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Compute the removal range for a JSX attribute, consuming any leading
|
|
166
|
-
* whitespace (spaces, tabs, newlines) so the resulting markup stays clean.
|
|
167
|
-
* @param context The rule context.
|
|
168
|
-
* @param prop The JSX attribute.
|
|
169
|
-
*/
|
|
170
|
-
function getPropRemovalRange(context, prop) {
|
|
171
|
-
const { sourceCode } = context;
|
|
172
|
-
let start = prop.range[0];
|
|
173
|
-
const end = prop.range[1];
|
|
174
|
-
while (start > 0 && /\s/.test(sourceCode.text[start - 1])) start--;
|
|
175
|
-
return [start, end];
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Extract the text to use as JSX children content from a `children` prop.
|
|
179
|
-
*
|
|
180
|
-
* - `children="text"` -> `text` (raw string, no quotes)
|
|
181
|
-
* - `children={<div />}` -> `<div />` (JSX element, no braces)
|
|
182
|
-
* - `children={<>…</>}` -> `<>…</>` (JSX fragment, no braces)
|
|
183
|
-
* - `children={expression}` -> `{expression}` (wrapped in braces)
|
|
184
|
-
* - `children` -> `null` (boolean shorthand, cannot extract)
|
|
185
|
-
* @param context The rule context.
|
|
186
|
-
* @param prop The JSX attribute.
|
|
187
|
-
*/
|
|
188
|
-
function getChildrenPropText(context, prop) {
|
|
189
|
-
const { sourceCode } = context;
|
|
190
|
-
const { value } = prop;
|
|
191
|
-
if (value == null) return null;
|
|
192
|
-
if (value.type === AST_NODE_TYPES.Literal) return String(value.value);
|
|
193
|
-
if (value.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
194
|
-
const { expression } = value;
|
|
195
|
-
if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return null;
|
|
196
|
-
const exprText = sourceCode.getText(expression);
|
|
197
|
-
if (Check.isJSXElementOrFragment(expression)) return exprText;
|
|
198
|
-
return `{${exprText}}`;
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
132
|
//#endregion
|
|
204
133
|
//#region src/rules/no-children-prop/no-children-prop.ts
|
|
205
134
|
const RULE_NAME$6 = "no-children-prop";
|
|
@@ -222,10 +151,7 @@ var no_children_prop_default = createRule({
|
|
|
222
151
|
function create$6(context) {
|
|
223
152
|
return {
|
|
224
153
|
CallExpression(node) {
|
|
225
|
-
|
|
226
|
-
const [, propsArg] = node.arguments;
|
|
227
|
-
if (propsArg == null || propsArg.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
228
|
-
const childrenProp = findChildrenProperty(propsArg);
|
|
154
|
+
const childrenProp = findCreateElementChildrenProp(context, node);
|
|
229
155
|
if (childrenProp == null) return;
|
|
230
156
|
context.report({
|
|
231
157
|
messageId: "default",
|
|
@@ -242,7 +168,7 @@ function create$6(context) {
|
|
|
242
168
|
});
|
|
243
169
|
return;
|
|
244
170
|
}
|
|
245
|
-
const childrenText =
|
|
171
|
+
const childrenText = getChildrenText(context, childrenProp);
|
|
246
172
|
if (childrenText == null) {
|
|
247
173
|
context.report({
|
|
248
174
|
messageId: "default",
|
|
@@ -254,27 +180,60 @@ function create$6(context) {
|
|
|
254
180
|
messageId: "default",
|
|
255
181
|
node: childrenProp,
|
|
256
182
|
suggest: [{
|
|
257
|
-
fix(
|
|
258
|
-
const sourceCode = context.sourceCode;
|
|
259
|
-
const { openingElement } = node;
|
|
260
|
-
const [removeStart, removeEnd] = getPropRemovalRange(context, childrenProp);
|
|
261
|
-
if (openingElement.selfClosing) {
|
|
262
|
-
const tagName = sourceCode.getText(openingElement.name);
|
|
263
|
-
const selfCloseOffset = sourceCode.getText(openingElement).lastIndexOf("/>");
|
|
264
|
-
let wsStart = openingElement.range[0] + selfCloseOffset;
|
|
265
|
-
while (wsStart > removeEnd && /\s/.test(sourceCode.text[wsStart - 1])) wsStart--;
|
|
266
|
-
return [fixer.removeRange([removeStart, removeEnd]), fixer.replaceTextRange([wsStart, openingElement.range[1]], `>${childrenText}</${tagName}>`)];
|
|
267
|
-
}
|
|
268
|
-
const fixes = [fixer.removeRange([removeStart, removeEnd])];
|
|
269
|
-
if (node.closingElement != null) fixes.push(fixer.insertTextBefore(node.closingElement, childrenText));
|
|
270
|
-
return fixes;
|
|
271
|
-
},
|
|
183
|
+
fix: buildFix$1(context, node, childrenProp, childrenText),
|
|
272
184
|
messageId: "moveChildrenToContent"
|
|
273
185
|
}]
|
|
274
186
|
});
|
|
275
187
|
}
|
|
276
188
|
};
|
|
277
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Turns the value of a 'children' JSXAttribute into text usable as element content
|
|
192
|
+
* @param context The rule context
|
|
193
|
+
* @param attribute The 'children' JSXAttribute node
|
|
194
|
+
* @returns The text to insert as element content, or `null` when no usable value exists
|
|
195
|
+
*/
|
|
196
|
+
function getChildrenText(context, attribute) {
|
|
197
|
+
const { value } = attribute;
|
|
198
|
+
if (value?.type === AST_NODE_TYPES.Literal) return escapeJsxText(String(value.value));
|
|
199
|
+
if (value?.type === AST_NODE_TYPES.JSXExpressionContainer && value.expression.type !== AST_NODE_TYPES.JSXEmptyExpression) {
|
|
200
|
+
const exprText = context.sourceCode.getText(value.expression);
|
|
201
|
+
return Check.isJSXElementOrFragment(value.expression) ? exprText : `{${exprText}}`;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Escapes characters that would be parsed as markup in JSX text
|
|
207
|
+
* @param text The raw string value
|
|
208
|
+
* @returns The escaped text safe for insertion as JSX content
|
|
209
|
+
*/
|
|
210
|
+
function escapeJsxText(text) {
|
|
211
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("{", "{").replaceAll("}", "}");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Builds the fix that moves the 'children' prop value into the element's content
|
|
215
|
+
* @param context The rule context object
|
|
216
|
+
* @param node The JSXElement node being reported
|
|
217
|
+
* @param prop The 'children' JSXAttribute to remove
|
|
218
|
+
* @param childrenText The text to insert as element content
|
|
219
|
+
* @returns A fixer function that applies the changes
|
|
220
|
+
*/
|
|
221
|
+
function buildFix$1(context, node, prop, childrenText) {
|
|
222
|
+
return (fixer) => {
|
|
223
|
+
const sourceCode = context.sourceCode;
|
|
224
|
+
const { openingElement } = node;
|
|
225
|
+
const removePropFix = removeJsxAttribute(context, fixer, prop);
|
|
226
|
+
if (openingElement.selfClosing) {
|
|
227
|
+
const tagName = sourceCode.getText(openingElement.name);
|
|
228
|
+
const selfCloseOffset = sourceCode.getText(openingElement).lastIndexOf("/>");
|
|
229
|
+
let wsStart = openingElement.range[0] + selfCloseOffset;
|
|
230
|
+
while (wsStart > prop.range[1] && /\s/.test(sourceCode.text[wsStart - 1])) wsStart--;
|
|
231
|
+
return [removePropFix, fixer.replaceTextRange([wsStart, openingElement.range[1]], `>${childrenText}</${tagName}>`)];
|
|
232
|
+
}
|
|
233
|
+
if (node.closingElement == null) return [removePropFix];
|
|
234
|
+
return [removePropFix, fixer.insertTextBefore(node.closingElement, childrenText)];
|
|
235
|
+
};
|
|
236
|
+
}
|
|
278
237
|
|
|
279
238
|
//#endregion
|
|
280
239
|
//#region src/rules/no-comment-textnodes/no-comment-textnodes.ts
|
|
@@ -291,22 +250,15 @@ var no_comment_textnodes_default = createRule({
|
|
|
291
250
|
defaultOptions: []
|
|
292
251
|
});
|
|
293
252
|
function create$5(context) {
|
|
294
|
-
function
|
|
295
|
-
if (Check.isOneOf([AST_NODE_TYPES.JSXAttribute, AST_NODE_TYPES.JSXExpressionContainer])(node.parent)) return false;
|
|
296
|
-
return /^\s*\/(?:\/|\*)/mu.test(context.sourceCode.getText(node));
|
|
297
|
-
}
|
|
298
|
-
const visit = (node) => {
|
|
253
|
+
function visit(node) {
|
|
299
254
|
if (!Check.isJSXElementOrFragment(node.parent)) return;
|
|
300
|
-
if (
|
|
255
|
+
if (!/^\s*\/(?:\/|\*)/mu.test(context.sourceCode.getText(node))) return;
|
|
301
256
|
context.report({
|
|
302
257
|
messageId: "default",
|
|
303
258
|
node
|
|
304
259
|
});
|
|
305
|
-
}
|
|
306
|
-
return {
|
|
307
|
-
JSXText: visit,
|
|
308
|
-
Literal: visit
|
|
309
|
-
};
|
|
260
|
+
}
|
|
261
|
+
return { JSXText: visit };
|
|
310
262
|
}
|
|
311
263
|
|
|
312
264
|
//#endregion
|
|
@@ -316,7 +268,7 @@ var no_key_after_spread_default = createRule({
|
|
|
316
268
|
meta: {
|
|
317
269
|
type: "problem",
|
|
318
270
|
docs: { description: "Prevent patterns that cause deoptimization when using the automatic JSX runtime." },
|
|
319
|
-
messages: {
|
|
271
|
+
messages: { default: "Placing 'key' after spread props causes deoptimization when using the automatic JSX runtime. Put 'key' before any spread props." },
|
|
320
272
|
schema: []
|
|
321
273
|
},
|
|
322
274
|
name: RULE_NAME$4,
|
|
@@ -325,17 +277,16 @@ var no_key_after_spread_default = createRule({
|
|
|
325
277
|
});
|
|
326
278
|
function create$4(context) {
|
|
327
279
|
const { jsx } = core.getJsxConfig(context);
|
|
328
|
-
if (
|
|
280
|
+
if (jsx !== ts.JsxEmit.ReactJSX && jsx !== ts.JsxEmit.ReactJSXDev) return {};
|
|
329
281
|
return { JSXOpeningElement(node) {
|
|
330
|
-
let
|
|
331
|
-
for (const
|
|
282
|
+
let hasSpreadBefore = false;
|
|
283
|
+
for (const prop of node.attributes) {
|
|
332
284
|
if (prop.type === AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
333
|
-
|
|
285
|
+
hasSpreadBefore = true;
|
|
334
286
|
continue;
|
|
335
287
|
}
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
messageId: "noKeyAfterSpread",
|
|
288
|
+
if (hasSpreadBefore && getAttributeName(prop) === "key") context.report({
|
|
289
|
+
messageId: "default",
|
|
339
290
|
node: prop
|
|
340
291
|
});
|
|
341
292
|
}
|
|
@@ -362,43 +313,45 @@ var no_leaked_dollar_default = createRule({
|
|
|
362
313
|
defaultOptions: []
|
|
363
314
|
});
|
|
364
315
|
function create$3(context) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
* @param node The JSXElement or JSXFragment node to be checked
|
|
368
|
-
*/
|
|
369
|
-
const visit = (node) => {
|
|
316
|
+
if (!context.sourceCode.text.includes("$")) return {};
|
|
317
|
+
function visit(node) {
|
|
370
318
|
for (const [index, child] of node.children.entries()) {
|
|
371
319
|
if (child.type !== AST_NODE_TYPES.JSXText || !child.value.endsWith("$")) continue;
|
|
372
320
|
if (node.children[index + 1]?.type !== AST_NODE_TYPES.JSXExpressionContainer) continue;
|
|
373
|
-
if (child.value === "$" && node.children.
|
|
374
|
-
|
|
321
|
+
if (child.value.trim() === "$" && node.children.every((sibling, siblingIndex) => siblingIndex === index || siblingIndex === index + 1 || isNonSubstantiveChild(sibling))) continue;
|
|
322
|
+
if (!context.sourceCode.getText(child).endsWith("$")) continue;
|
|
323
|
+
const dollarStart = child.range[1] - 1;
|
|
324
|
+
const dollarEnd = child.range[1];
|
|
375
325
|
context.report({
|
|
376
326
|
loc: {
|
|
377
|
-
end:
|
|
378
|
-
|
|
379
|
-
line: pos.line
|
|
380
|
-
},
|
|
381
|
-
start: {
|
|
382
|
-
column: pos.column - 1,
|
|
383
|
-
line: pos.line
|
|
384
|
-
}
|
|
327
|
+
end: context.sourceCode.getLocFromIndex(dollarEnd),
|
|
328
|
+
start: context.sourceCode.getLocFromIndex(dollarStart)
|
|
385
329
|
},
|
|
386
330
|
messageId: "default",
|
|
387
331
|
node: child,
|
|
388
332
|
suggest: [{
|
|
389
333
|
fix(fixer) {
|
|
390
|
-
return fixer.removeRange([
|
|
334
|
+
return fixer.removeRange([dollarStart, dollarEnd]);
|
|
391
335
|
},
|
|
392
336
|
messageId: "removeDollarSign"
|
|
393
337
|
}]
|
|
394
338
|
});
|
|
395
339
|
}
|
|
396
|
-
}
|
|
340
|
+
}
|
|
397
341
|
return {
|
|
398
342
|
JSXElement: visit,
|
|
399
343
|
JSXFragment: visit
|
|
400
344
|
};
|
|
401
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Whether a JSX child carries no renderable content: padding whitespace, an
|
|
348
|
+
* empty expression (ex: `{}` or a comment), or an empty string expression.
|
|
349
|
+
* @param child The JSX child node to check.
|
|
350
|
+
*/
|
|
351
|
+
function isNonSubstantiveChild(child) {
|
|
352
|
+
if (isWhitespaceText(child) || isEmptyStringExpression(child)) return true;
|
|
353
|
+
return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
|
|
354
|
+
}
|
|
402
355
|
|
|
403
356
|
//#endregion
|
|
404
357
|
//#region src/rules/no-leaked-semicolon/no-leaked-semicolon.ts
|
|
@@ -407,6 +360,7 @@ var no_leaked_semicolon_default = createRule({
|
|
|
407
360
|
meta: {
|
|
408
361
|
type: "problem",
|
|
409
362
|
docs: { description: "Catches `;` at the start of JSX text nodes — typically from accidentally placing a statement-ending `;` inside JSX. The `;` \"leaks\" into the rendered output." },
|
|
363
|
+
fixable: "code",
|
|
410
364
|
hasSuggestions: true,
|
|
411
365
|
messages: {
|
|
412
366
|
default: "Leaked ';' in JSX. This ';' will be rendered as text nodes.",
|
|
@@ -418,13 +372,10 @@ var no_leaked_semicolon_default = createRule({
|
|
|
418
372
|
create: create$2,
|
|
419
373
|
defaultOptions: []
|
|
420
374
|
});
|
|
421
|
-
function hasLeakedSemicolon(text) {
|
|
422
|
-
return text.startsWith(";\n") || text.startsWith(";\r");
|
|
423
|
-
}
|
|
424
375
|
function create$2(context) {
|
|
425
|
-
|
|
376
|
+
function visit(node) {
|
|
426
377
|
if (!Check.isJSXElementOrFragment(node.parent)) return;
|
|
427
|
-
if (
|
|
378
|
+
if (!/^;+[ \t]*(?:\r\n|\r|\n)/u.test(context.sourceCode.getText(node))) return;
|
|
428
379
|
context.report({
|
|
429
380
|
messageId: "default",
|
|
430
381
|
node,
|
|
@@ -435,11 +386,8 @@ function create$2(context) {
|
|
|
435
386
|
messageId: "removeSemicolon"
|
|
436
387
|
}]
|
|
437
388
|
});
|
|
438
|
-
}
|
|
439
|
-
return {
|
|
440
|
-
JSXText: visit,
|
|
441
|
-
Literal: visit
|
|
442
|
-
};
|
|
389
|
+
}
|
|
390
|
+
return { JSXText: visit };
|
|
443
391
|
}
|
|
444
392
|
|
|
445
393
|
//#endregion
|
|
@@ -459,7 +407,7 @@ var no_namespace_default = createRule({
|
|
|
459
407
|
function create$1(context) {
|
|
460
408
|
return { JSXElement(node) {
|
|
461
409
|
const name = getElementFullType(node);
|
|
462
|
-
if (
|
|
410
|
+
if (!name.includes(":")) return;
|
|
463
411
|
context.report({
|
|
464
412
|
data: { name },
|
|
465
413
|
messageId: "noNamespace",
|
|
@@ -492,7 +440,6 @@ const schema = [{
|
|
|
492
440
|
var no_useless_fragment_default = createRule({
|
|
493
441
|
meta: {
|
|
494
442
|
type: "suggestion",
|
|
495
|
-
defaultOptions: [...defaultOptions],
|
|
496
443
|
docs: { description: "Disallows useless fragment elements." },
|
|
497
444
|
fixable: "code",
|
|
498
445
|
messages: { default: "A fragment {{reason}} is useless." },
|
|
@@ -503,89 +450,96 @@ var no_useless_fragment_default = createRule({
|
|
|
503
450
|
defaultOptions
|
|
504
451
|
});
|
|
505
452
|
function create(context, [option]) {
|
|
506
|
-
const
|
|
453
|
+
const options = {
|
|
454
|
+
allowEmptyFragment: option.allowEmptyFragment ?? false,
|
|
455
|
+
allowExpressions: option.allowExpressions ?? true
|
|
456
|
+
};
|
|
507
457
|
const jsxConfig = core.getJsxConfig(context);
|
|
508
|
-
/**
|
|
509
|
-
* Whether the fragment has too few meaningful children to justify its
|
|
510
|
-
* existence (the "contains less than two children" reason).
|
|
511
|
-
* @param node The fragment node to check.
|
|
512
|
-
*/
|
|
513
|
-
function isContentUseless(node) {
|
|
514
|
-
if (node.children.length === 0) return !allowEmptyFragment;
|
|
515
|
-
const insideJsx = Check.isJSXElementOrFragment(node.parent);
|
|
516
|
-
if (!allowExpressions) {
|
|
517
|
-
if (insideJsx) return true;
|
|
518
|
-
if (node.children.length === 1) return true;
|
|
519
|
-
}
|
|
520
|
-
if (allowExpressions && !insideJsx && node.children.length === 1) {
|
|
521
|
-
const child = node.children[0];
|
|
522
|
-
if (child != null && child.type === AST_NODE_TYPES.JSXText) return false;
|
|
523
|
-
}
|
|
524
|
-
const meaningful = getChildren(node);
|
|
525
|
-
if (meaningful.length === 0) return true;
|
|
526
|
-
if (meaningful.length === 1 && meaningful[0].type !== AST_NODE_TYPES.JSXExpressionContainer) return true;
|
|
527
|
-
return false;
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
531
|
-
* @param node The fragment node to check.
|
|
532
|
-
*/
|
|
533
|
-
function isSafeToFix(node) {
|
|
534
|
-
if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
|
|
535
|
-
if (node.children.length === 0) return false;
|
|
536
|
-
return !node.children.some((child) => {
|
|
537
|
-
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) return true;
|
|
538
|
-
if (child.type === AST_NODE_TYPES.JSXText) return !isWhitespaceText(child);
|
|
539
|
-
return false;
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Build an autofix that unwraps the fragment, replacing it with its
|
|
544
|
-
* meaningful children content. Returns `null` when the fix is unsafe.
|
|
545
|
-
* @param node The fragment node to fix.
|
|
546
|
-
*/
|
|
547
|
-
function buildFix(node) {
|
|
548
|
-
if (!isSafeToFix(node)) return null;
|
|
549
|
-
return (fixer) => {
|
|
550
|
-
let text = "";
|
|
551
|
-
for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
|
|
552
|
-
const cleaned = collapseMultilineText(child.value);
|
|
553
|
-
if (cleaned != null) text += cleaned;
|
|
554
|
-
} else text += context.sourceCode.getText(child);
|
|
555
|
-
return fixer.replaceText(node, text);
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Inspect a fragment node and report if it is useless.
|
|
560
|
-
*
|
|
561
|
-
* A fragment may be reported for **two independent reasons** on the same
|
|
562
|
-
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
563
|
-
* and* "contains less than two children").
|
|
564
|
-
* @param node The fragment node to inspect.
|
|
565
|
-
*/
|
|
566
|
-
function visit(node) {
|
|
567
|
-
if (isHostElement(node.parent)) context.report({
|
|
568
|
-
data: { reason: "placed inside a host component" },
|
|
569
|
-
fix: buildFix(node),
|
|
570
|
-
messageId: "default",
|
|
571
|
-
node
|
|
572
|
-
});
|
|
573
|
-
if (isContentUseless(node)) context.report({
|
|
574
|
-
data: { reason: "contains less than two children" },
|
|
575
|
-
fix: buildFix(node),
|
|
576
|
-
messageId: "default",
|
|
577
|
-
node
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
458
|
return {
|
|
581
459
|
JSXElement(node) {
|
|
582
460
|
if (!isFragmentElement(node, jsxConfig.jsxFragmentFactory)) return;
|
|
583
461
|
if (hasAnyAttribute(context, node, ["key", "ref"])) return;
|
|
584
|
-
|
|
462
|
+
visitFragment(context, node, options);
|
|
585
463
|
},
|
|
586
464
|
JSXFragment(node) {
|
|
587
|
-
|
|
465
|
+
visitFragment(context, node, options);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Inspect a fragment node and report if it is useless.
|
|
471
|
+
*
|
|
472
|
+
* A fragment may be reported for **two independent reasons** on the same
|
|
473
|
+
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
474
|
+
* and "contains less than two children").
|
|
475
|
+
* @param context The rule context.
|
|
476
|
+
* @param node The fragment node to inspect.
|
|
477
|
+
* @param options The resolved rule options.
|
|
478
|
+
*/
|
|
479
|
+
function visitFragment(context, node, options) {
|
|
480
|
+
const reasons = [];
|
|
481
|
+
if (isHostElement(node.parent)) reasons.push("placed inside a host component");
|
|
482
|
+
if (isContentUseless(node, options)) reasons.push("contains less than two children");
|
|
483
|
+
if (reasons.length === 0) return;
|
|
484
|
+
const fix = buildFix(context, node);
|
|
485
|
+
for (const reason of reasons) context.report({
|
|
486
|
+
data: { reason },
|
|
487
|
+
fix,
|
|
488
|
+
messageId: "default",
|
|
489
|
+
node
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Whether the fragment has too few meaningful children to justify its
|
|
494
|
+
* existence (the "contains less than two children" reason).
|
|
495
|
+
* @param node The fragment node to check.
|
|
496
|
+
* @param options The resolved rule options.
|
|
497
|
+
*/
|
|
498
|
+
function isContentUseless(node, options) {
|
|
499
|
+
const { allowEmptyFragment, allowExpressions } = options;
|
|
500
|
+
if (node.children.length === 0) return !allowEmptyFragment;
|
|
501
|
+
const insideJsx = Check.isJSXElementOrFragment(node.parent);
|
|
502
|
+
if (allowExpressions && !insideJsx && node.children.length === 1 && node.children[0]?.type === AST_NODE_TYPES.JSXText) return false;
|
|
503
|
+
const meaningful = getChildren(node);
|
|
504
|
+
if (meaningful.length === 0) return true;
|
|
505
|
+
if (meaningful.length > 1) return false;
|
|
506
|
+
if (meaningful[0]?.type === AST_NODE_TYPES.JSXExpressionContainer) return !allowExpressions;
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
511
|
+
* @param node The fragment node to check.
|
|
512
|
+
*/
|
|
513
|
+
function isSafeToFix(node) {
|
|
514
|
+
if (node.type === AST_NODE_TYPES.JSXElement && node.openingElement.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute)) return false;
|
|
515
|
+
if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
|
|
516
|
+
if (node.children.length === 0) return false;
|
|
517
|
+
return node.children.every((child) => {
|
|
518
|
+
if (Check.isJSXElementOrFragment(child)) return true;
|
|
519
|
+
return child.type === AST_NODE_TYPES.JSXText && isWhitespaceText(child);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Build an autofix that unwraps the fragment, replacing it with its
|
|
524
|
+
* meaningful children content. Returns `null` when the fix is unsafe.
|
|
525
|
+
* @param context The rule context.
|
|
526
|
+
* @param node The fragment node to fix.
|
|
527
|
+
*/
|
|
528
|
+
function buildFix(context, node) {
|
|
529
|
+
if (!isSafeToFix(node)) return null;
|
|
530
|
+
return (fixer) => {
|
|
531
|
+
const sourceCode = context.sourceCode;
|
|
532
|
+
let text = "";
|
|
533
|
+
for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
|
|
534
|
+
const cleaned = collapseMultilineText(child.value);
|
|
535
|
+
if (cleaned != null) text += cleaned;
|
|
536
|
+
} else text += sourceCode.getText(child);
|
|
537
|
+
let start = node.range[0];
|
|
538
|
+
if (text === "" && node.children.length > 0) {
|
|
539
|
+
const lineStart = Math.max(sourceCode.text.lastIndexOf("\n", start - 1), sourceCode.text.lastIndexOf("\r", start - 1)) + 1;
|
|
540
|
+
if (/^[ \t]*$/u.test(sourceCode.text.slice(lineStart, start))) start = lineStart;
|
|
588
541
|
}
|
|
542
|
+
return fixer.replaceTextRange([start, node.range[1]], text);
|
|
589
543
|
};
|
|
590
544
|
}
|
|
591
545
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-jsx",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for React Flavored JSX rules.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@typescript-eslint/types": "^8.64.0",
|
|
41
41
|
"@typescript-eslint/utils": "^8.64.0",
|
|
42
|
-
"@eslint-react/
|
|
43
|
-
"@eslint-react/
|
|
44
|
-
"@eslint-react/
|
|
45
|
-
"@eslint-react/
|
|
46
|
-
"@eslint-react/
|
|
42
|
+
"@eslint-react/ast": "5.17.0",
|
|
43
|
+
"@eslint-react/core": "5.17.0",
|
|
44
|
+
"@eslint-react/eslint": "5.17.0",
|
|
45
|
+
"@eslint-react/jsx": "5.17.0",
|
|
46
|
+
"@eslint-react/shared": "5.17.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.2.17",
|