eslint-plugin-react-jsx 5.16.1 → 5.17.1
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 +678 -191
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
|
|
2
|
-
import { Check, Extract } from "@eslint-react/ast";
|
|
3
|
-
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
4
2
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
3
|
+
import { Check, Extract } from "@eslint-react/ast";
|
|
5
4
|
import * as core from "@eslint-react/core";
|
|
5
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
6
6
|
import "@eslint-react/eslint";
|
|
7
|
-
import { collapseMultilineText, findAttribute, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
7
|
+
import { collapseMultilineText, findAttribute, getChildren, getElementFullType, hasAnyAttribute, hasChildren, isAttribute, isEmptyStringExpression, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
8
8
|
import ts from "typescript";
|
|
9
9
|
|
|
10
10
|
//#region \0rolldown/runtime.js
|
|
@@ -26,45 +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.
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/utils/common.ts
|
|
33
|
-
function findChildrenProperty(node) {
|
|
34
|
-
for (const prop of node.properties) if (prop.type === AST_NODE_TYPES.Property && Extract.getStaticPropertyName(prop) === "children") return prop;
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
function getChildrenContentRange(node) {
|
|
38
|
-
const first = node.children.at(0);
|
|
39
|
-
const last = node.children.at(-1);
|
|
40
|
-
if (first == null || last == null) return null;
|
|
41
|
-
return [first.range[0], last.range[1]];
|
|
42
|
-
}
|
|
43
|
-
function getChildrenPropText(context, prop) {
|
|
44
|
-
const { sourceCode } = context;
|
|
45
|
-
const { value } = prop;
|
|
46
|
-
if (value == null) return null;
|
|
47
|
-
if (value.type === AST_NODE_TYPES.Literal) return escapeJsxText(String(value.value));
|
|
48
|
-
if (value.type !== AST_NODE_TYPES.JSXExpressionContainer) return null;
|
|
49
|
-
const { expression } = value;
|
|
50
|
-
if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return null;
|
|
51
|
-
const exprText = sourceCode.getText(expression);
|
|
52
|
-
if (Check.isJSXElementOrFragment(expression)) return exprText;
|
|
53
|
-
return `{${exprText}}`;
|
|
54
|
-
}
|
|
55
|
-
function escapeJsxText(text) {
|
|
56
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("{", "{").replaceAll("}", "}");
|
|
57
|
-
}
|
|
58
|
-
function getPropRemovalRange(context, prop) {
|
|
59
|
-
const { sourceCode } = context;
|
|
60
|
-
let start = prop.range[0];
|
|
61
|
-
const end = prop.range[1];
|
|
62
|
-
while (start > 0 && /\s/.test(sourceCode.text[start - 1] ?? "")) start--;
|
|
63
|
-
return [start, end];
|
|
64
|
-
}
|
|
65
|
-
function isDirectJsxChild(node) {
|
|
66
|
-
return Check.isJSXElementOrFragment(node.parent);
|
|
67
|
-
}
|
|
29
|
+
var version = "5.17.1";
|
|
68
30
|
|
|
69
31
|
//#endregion
|
|
70
32
|
//#region src/utils/create-rule.ts
|
|
@@ -73,6 +35,39 @@ function getDocsUrl(ruleName) {
|
|
|
73
35
|
}
|
|
74
36
|
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
75
37
|
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/utils/find-create-element-children-prop.ts
|
|
40
|
+
/**
|
|
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
|
|
45
|
+
*/
|
|
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;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/utils/remove-jsx-attribute.ts
|
|
58
|
+
/**
|
|
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
|
|
64
|
+
*/
|
|
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]]);
|
|
69
|
+
}
|
|
70
|
+
|
|
76
71
|
//#endregion
|
|
77
72
|
//#region src/rules/no-children-prop-with-children/no-children-prop-with-children.ts
|
|
78
73
|
const RULE_NAME$7 = "no-children-prop-with-children";
|
|
@@ -96,14 +91,9 @@ var no_children_prop_with_children_default = createRule({
|
|
|
96
91
|
function create$7(context) {
|
|
97
92
|
return {
|
|
98
93
|
CallExpression(node) {
|
|
99
|
-
|
|
100
|
-
const [, propsArg, firstExtra] = node.arguments;
|
|
101
|
-
if (propsArg == null) return;
|
|
102
|
-
const propsObject = Extract.unwrap(propsArg);
|
|
103
|
-
if (propsObject.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
104
|
-
const childrenProp = findChildrenProperty(propsObject);
|
|
94
|
+
const childrenProp = findCreateElementChildrenProp(context, node);
|
|
105
95
|
if (childrenProp == null) return;
|
|
106
|
-
if (
|
|
96
|
+
if (node.arguments[2] == null) return;
|
|
107
97
|
context.report({
|
|
108
98
|
messageId: "default",
|
|
109
99
|
node: childrenProp
|
|
@@ -111,8 +101,7 @@ function create$7(context) {
|
|
|
111
101
|
},
|
|
112
102
|
JSXElement(node) {
|
|
113
103
|
const childrenProp = findAttribute(context, node, "children");
|
|
114
|
-
if (childrenProp == null) return;
|
|
115
|
-
if (!hasChildren(node)) return;
|
|
104
|
+
if (childrenProp == null || !hasChildren(node)) return;
|
|
116
105
|
if (childrenProp.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
117
106
|
context.report({
|
|
118
107
|
messageId: "default",
|
|
@@ -124,16 +113,14 @@ function create$7(context) {
|
|
|
124
113
|
messageId: "default",
|
|
125
114
|
node: childrenProp,
|
|
126
115
|
suggest: [{
|
|
127
|
-
fix(fixer)
|
|
128
|
-
const [start, end] = getPropRemovalRange(context, childrenProp);
|
|
129
|
-
return fixer.removeRange([start, end]);
|
|
130
|
-
},
|
|
116
|
+
fix: (fixer) => removeJsxAttribute(context, fixer, childrenProp),
|
|
131
117
|
messageId: "removeChildrenProp"
|
|
132
118
|
}, {
|
|
133
119
|
fix(fixer) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
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]]);
|
|
137
124
|
},
|
|
138
125
|
messageId: "removeChildrenContent"
|
|
139
126
|
}]
|
|
@@ -164,12 +151,7 @@ var no_children_prop_default = createRule({
|
|
|
164
151
|
function create$6(context) {
|
|
165
152
|
return {
|
|
166
153
|
CallExpression(node) {
|
|
167
|
-
|
|
168
|
-
const [, propsArg] = node.arguments;
|
|
169
|
-
if (propsArg == null) return;
|
|
170
|
-
const propsObject = Extract.unwrap(propsArg);
|
|
171
|
-
if (propsObject.type !== AST_NODE_TYPES.ObjectExpression) return;
|
|
172
|
-
const childrenProp = findChildrenProperty(propsObject);
|
|
154
|
+
const childrenProp = findCreateElementChildrenProp(context, node);
|
|
173
155
|
if (childrenProp == null) return;
|
|
174
156
|
context.report({
|
|
175
157
|
messageId: "default",
|
|
@@ -186,7 +168,7 @@ function create$6(context) {
|
|
|
186
168
|
});
|
|
187
169
|
return;
|
|
188
170
|
}
|
|
189
|
-
const childrenText =
|
|
171
|
+
const childrenText = getChildrenText(context, childrenProp);
|
|
190
172
|
if (childrenText == null) {
|
|
191
173
|
context.report({
|
|
192
174
|
messageId: "default",
|
|
@@ -198,27 +180,60 @@ function create$6(context) {
|
|
|
198
180
|
messageId: "default",
|
|
199
181
|
node: childrenProp,
|
|
200
182
|
suggest: [{
|
|
201
|
-
fix(
|
|
202
|
-
const sourceCode = context.sourceCode;
|
|
203
|
-
const { openingElement } = node;
|
|
204
|
-
const [removeStart, removeEnd] = getPropRemovalRange(context, childrenProp);
|
|
205
|
-
if (openingElement.selfClosing) {
|
|
206
|
-
const tagName = sourceCode.getText(openingElement.name);
|
|
207
|
-
const selfCloseOffset = sourceCode.getText(openingElement).lastIndexOf("/>");
|
|
208
|
-
let wsStart = openingElement.range[0] + selfCloseOffset;
|
|
209
|
-
while (wsStart > removeEnd && /\s/.test(sourceCode.text[wsStart - 1])) wsStart--;
|
|
210
|
-
return [fixer.removeRange([removeStart, removeEnd]), fixer.replaceTextRange([wsStart, openingElement.range[1]], `>${childrenText}</${tagName}>`)];
|
|
211
|
-
}
|
|
212
|
-
const fixes = [fixer.removeRange([removeStart, removeEnd])];
|
|
213
|
-
if (node.closingElement != null) fixes.push(fixer.insertTextBefore(node.closingElement, childrenText));
|
|
214
|
-
return fixes;
|
|
215
|
-
},
|
|
183
|
+
fix: buildFix$1(context, node, childrenProp, childrenText),
|
|
216
184
|
messageId: "moveChildrenToContent"
|
|
217
185
|
}]
|
|
218
186
|
});
|
|
219
187
|
}
|
|
220
188
|
};
|
|
221
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
|
+
}
|
|
222
237
|
|
|
223
238
|
//#endregion
|
|
224
239
|
//#region src/rules/no-comment-textnodes/no-comment-textnodes.ts
|
|
@@ -236,19 +251,493 @@ var no_comment_textnodes_default = createRule({
|
|
|
236
251
|
});
|
|
237
252
|
function create$5(context) {
|
|
238
253
|
function visit(node) {
|
|
239
|
-
if (!
|
|
254
|
+
if (!Check.isJSXElementOrFragment(node.parent)) return;
|
|
240
255
|
if (!/^\s*\/(?:\/|\*)/mu.test(context.sourceCode.getText(node))) return;
|
|
241
256
|
context.report({
|
|
242
257
|
messageId: "default",
|
|
243
258
|
node
|
|
244
259
|
});
|
|
245
260
|
}
|
|
246
|
-
return {
|
|
247
|
-
JSXText: visit,
|
|
248
|
-
Literal: visit
|
|
249
|
-
};
|
|
261
|
+
return { JSXText: visit };
|
|
250
262
|
}
|
|
251
263
|
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region ../../.pkgs/eff/dist/index.js
|
|
266
|
+
function not(predicate) {
|
|
267
|
+
return (data) => !predicate(data);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Applies a `pipe` method's variadic arguments to an initial value from left
|
|
271
|
+
* to right.
|
|
272
|
+
*
|
|
273
|
+
* **When to use**
|
|
274
|
+
*
|
|
275
|
+
* Use to implement a custom `.pipe(...)` method from JavaScript's `arguments`
|
|
276
|
+
* object.
|
|
277
|
+
*
|
|
278
|
+
* **Details**
|
|
279
|
+
*
|
|
280
|
+
* This helper is intended for implementing `Pipeable.pipe` methods that
|
|
281
|
+
* receive JavaScript's `arguments` object. With no functions it returns the
|
|
282
|
+
* original value; otherwise it feeds each result into the next function.
|
|
283
|
+
*
|
|
284
|
+
* **Example** (Implementing a pipe method)
|
|
285
|
+
*
|
|
286
|
+
* ```ts
|
|
287
|
+
* import { Pipeable } from "effect"
|
|
288
|
+
*
|
|
289
|
+
* class NumberBox {
|
|
290
|
+
* constructor(readonly value: number) {}
|
|
291
|
+
*
|
|
292
|
+
* pipe(..._fns: ReadonlyArray<(value: number) => number>): number {
|
|
293
|
+
* return Pipeable.pipeArguments(this.value, arguments) as number
|
|
294
|
+
* }
|
|
295
|
+
* }
|
|
296
|
+
*
|
|
297
|
+
* const result = new NumberBox(5).pipe(
|
|
298
|
+
* (n) => n + 2,
|
|
299
|
+
* (n) => n * 3
|
|
300
|
+
* )
|
|
301
|
+
* console.log(result) // 21
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* @category combinators
|
|
305
|
+
* @since 2.0.0
|
|
306
|
+
*/
|
|
307
|
+
const pipeArguments = (self, args) => {
|
|
308
|
+
switch (args.length) {
|
|
309
|
+
case 0: return self;
|
|
310
|
+
case 1: return args[0](self);
|
|
311
|
+
case 2: return args[1](args[0](self));
|
|
312
|
+
case 3: return args[2](args[1](args[0](self)));
|
|
313
|
+
case 4: return args[3](args[2](args[1](args[0](self))));
|
|
314
|
+
case 5: return args[4](args[3](args[2](args[1](args[0](self)))));
|
|
315
|
+
case 6: return args[5](args[4](args[3](args[2](args[1](args[0](self))))));
|
|
316
|
+
case 7: return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))));
|
|
317
|
+
case 8: return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))));
|
|
318
|
+
case 9: return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))));
|
|
319
|
+
default: {
|
|
320
|
+
let ret = self;
|
|
321
|
+
for (let i = 0, len = args.length; i < len; i++) ret = args[i](ret);
|
|
322
|
+
return ret;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Reusable prototype that implements `Pipeable.pipe`.
|
|
328
|
+
*
|
|
329
|
+
* **When to use**
|
|
330
|
+
*
|
|
331
|
+
* Use when classes or object prototypes can reuse this value when they need the
|
|
332
|
+
* standard pipe implementation backed by `pipeArguments`.
|
|
333
|
+
*
|
|
334
|
+
* @category prototypes
|
|
335
|
+
* @since 3.15.0
|
|
336
|
+
*/
|
|
337
|
+
const Prototype = { pipe() {
|
|
338
|
+
return pipeArguments(this, arguments);
|
|
339
|
+
} };
|
|
340
|
+
/**
|
|
341
|
+
* Provides a base constructor whose instances implement the standard `Pipeable.pipe`
|
|
342
|
+
* method.
|
|
343
|
+
*
|
|
344
|
+
* **When to use**
|
|
345
|
+
*
|
|
346
|
+
* Use when you need to define a class that supports Effect-style method
|
|
347
|
+
* chaining through `.pipe(...)`.
|
|
348
|
+
*
|
|
349
|
+
* @category constructors
|
|
350
|
+
* @since 3.15.0
|
|
351
|
+
*/
|
|
352
|
+
const Class = (function() {
|
|
353
|
+
function PipeableBase() {}
|
|
354
|
+
PipeableBase.prototype = Prototype;
|
|
355
|
+
return PipeableBase;
|
|
356
|
+
})();
|
|
357
|
+
/**
|
|
358
|
+
* Provides small helpers for defining and reusing TypeScript functions.
|
|
359
|
+
*
|
|
360
|
+
* The main helpers are `pipe` and `flow` for left-to-right composition and
|
|
361
|
+
* `dual` for APIs that support both direct and pipe-friendly call styles. The
|
|
362
|
+
* module also contains small identity, constant, tuple, type-level, and
|
|
363
|
+
* memoization helpers used across the library.
|
|
364
|
+
*
|
|
365
|
+
* @since 2.0.0
|
|
366
|
+
*/
|
|
367
|
+
/**
|
|
368
|
+
* Creates a function that can be called in data-first style or data-last
|
|
369
|
+
* (`pipe`-friendly) style.
|
|
370
|
+
*
|
|
371
|
+
* **When to use**
|
|
372
|
+
*
|
|
373
|
+
* Use to expose one implementation through both direct and `pipe`-friendly
|
|
374
|
+
* call styles.
|
|
375
|
+
*
|
|
376
|
+
* **Details**
|
|
377
|
+
*
|
|
378
|
+
* Pass either the arity of the uncurried function or a predicate that decides
|
|
379
|
+
* whether the current call is data-first. Arity is the common case. Use a
|
|
380
|
+
* predicate when optional arguments make arity ambiguous.
|
|
381
|
+
*
|
|
382
|
+
* **Example** (Selecting data-first or data-last style by arity)
|
|
383
|
+
*
|
|
384
|
+
* ```ts
|
|
385
|
+
* import { Function, pipe } from "effect"
|
|
386
|
+
*
|
|
387
|
+
* const sum = Function.dual<
|
|
388
|
+
* (that: number) => (self: number) => number,
|
|
389
|
+
* (self: number, that: number) => number
|
|
390
|
+
* >(2, (self, that) => self + that)
|
|
391
|
+
*
|
|
392
|
+
* console.log(sum(2, 3)) // 5
|
|
393
|
+
* console.log(pipe(2, sum(3))) // 5
|
|
394
|
+
* ```
|
|
395
|
+
*
|
|
396
|
+
* **Example** (Defining overloads with call signatures)
|
|
397
|
+
*
|
|
398
|
+
* ```ts
|
|
399
|
+
* import { Function, pipe } from "effect"
|
|
400
|
+
*
|
|
401
|
+
* const sum: {
|
|
402
|
+
* (that: number): (self: number) => number
|
|
403
|
+
* (self: number, that: number): number
|
|
404
|
+
* } = Function.dual(2, (self: number, that: number): number => self + that)
|
|
405
|
+
*
|
|
406
|
+
* console.log(sum(2, 3)) // 5
|
|
407
|
+
* console.log(pipe(2, sum(3))) // 5
|
|
408
|
+
* ```
|
|
409
|
+
*
|
|
410
|
+
* **Example** (Selecting data-first or data-last style with a predicate)
|
|
411
|
+
*
|
|
412
|
+
* ```ts
|
|
413
|
+
* import { Function, pipe } from "effect"
|
|
414
|
+
*
|
|
415
|
+
* const sum = Function.dual<
|
|
416
|
+
* (that: number) => (self: number) => number,
|
|
417
|
+
* (self: number, that: number) => number
|
|
418
|
+
* >(
|
|
419
|
+
* (args) => args.length === 2,
|
|
420
|
+
* (self, that) => self + that
|
|
421
|
+
* )
|
|
422
|
+
*
|
|
423
|
+
* console.log(sum(2, 3)) // 5
|
|
424
|
+
* console.log(pipe(2, sum(3))) // 5
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* @category combinators
|
|
428
|
+
* @since 2.0.0
|
|
429
|
+
*/
|
|
430
|
+
const dual = function(arity, body) {
|
|
431
|
+
if (typeof arity === "function") return function() {
|
|
432
|
+
return arity(arguments) ? body.apply(this, arguments) : ((self) => body(self, ...arguments));
|
|
433
|
+
};
|
|
434
|
+
switch (arity) {
|
|
435
|
+
case 0:
|
|
436
|
+
case 1: throw new RangeError(`Invalid arity ${arity}`);
|
|
437
|
+
case 2: return function(a, b) {
|
|
438
|
+
if (arguments.length >= 2) return body(a, b);
|
|
439
|
+
return function(self) {
|
|
440
|
+
return body(self, a);
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
case 3: return function(a, b, c) {
|
|
444
|
+
if (arguments.length >= 3) return body(a, b, c);
|
|
445
|
+
return function(self) {
|
|
446
|
+
return body(self, a, b);
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
default: return function() {
|
|
450
|
+
if (arguments.length >= arity) return body.apply(this, arguments);
|
|
451
|
+
const args = arguments;
|
|
452
|
+
return function(self) {
|
|
453
|
+
return body(self, ...args);
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* Returns its input argument unchanged.
|
|
460
|
+
*
|
|
461
|
+
* **When to use**
|
|
462
|
+
*
|
|
463
|
+
* Use to return a value unchanged where a function is required.
|
|
464
|
+
*
|
|
465
|
+
* **Example** (Returning the same value)
|
|
466
|
+
*
|
|
467
|
+
* ```ts
|
|
468
|
+
* import { identity } from "effect"
|
|
469
|
+
* import * as assert from "node:assert"
|
|
470
|
+
*
|
|
471
|
+
* assert.deepStrictEqual(identity(5), 5)
|
|
472
|
+
* ```
|
|
473
|
+
*
|
|
474
|
+
* @category combinators
|
|
475
|
+
* @since 2.0.0
|
|
476
|
+
*/
|
|
477
|
+
const identity = (a) => a;
|
|
478
|
+
/**
|
|
479
|
+
* Returns the input value with a different static type.
|
|
480
|
+
*
|
|
481
|
+
* **When to use**
|
|
482
|
+
*
|
|
483
|
+
* Use when you need an explicit type-level cast and accept that the value is
|
|
484
|
+
* returned unchanged at runtime.
|
|
485
|
+
*
|
|
486
|
+
* **Gotchas**
|
|
487
|
+
*
|
|
488
|
+
* This is a type-level cast only; it performs no runtime validation or
|
|
489
|
+
* conversion.
|
|
490
|
+
*
|
|
491
|
+
* @see {@link satisfies} for checking assignability without changing the resulting type
|
|
492
|
+
*
|
|
493
|
+
* @category utility types
|
|
494
|
+
* @since 4.0.0
|
|
495
|
+
*/
|
|
496
|
+
const cast = identity;
|
|
497
|
+
/**
|
|
498
|
+
* Creates a zero-argument function that always returns the provided value.
|
|
499
|
+
*
|
|
500
|
+
* **When to use**
|
|
501
|
+
*
|
|
502
|
+
* Use when you need a thunk or callback that returns the same value on every
|
|
503
|
+
* invocation.
|
|
504
|
+
*
|
|
505
|
+
* **Example** (Creating a constant thunk)
|
|
506
|
+
*
|
|
507
|
+
* ```ts
|
|
508
|
+
* import { Function } from "effect"
|
|
509
|
+
* import * as assert from "node:assert"
|
|
510
|
+
*
|
|
511
|
+
* const constNull = Function.constant(null)
|
|
512
|
+
*
|
|
513
|
+
* assert.deepStrictEqual(constNull(), null)
|
|
514
|
+
* assert.deepStrictEqual(constNull(), null)
|
|
515
|
+
* ```
|
|
516
|
+
*
|
|
517
|
+
* @category constructors
|
|
518
|
+
* @since 2.0.0
|
|
519
|
+
*/
|
|
520
|
+
const constant = (value) => () => value;
|
|
521
|
+
/**
|
|
522
|
+
* Returns `true` when called.
|
|
523
|
+
*
|
|
524
|
+
* **When to use**
|
|
525
|
+
*
|
|
526
|
+
* Use when you need a thunk that returns `true` on every invocation.
|
|
527
|
+
*
|
|
528
|
+
* **Example** (Returning true from a thunk)
|
|
529
|
+
*
|
|
530
|
+
* ```ts
|
|
531
|
+
* import { Function } from "effect"
|
|
532
|
+
* import * as assert from "node:assert"
|
|
533
|
+
*
|
|
534
|
+
* assert.deepStrictEqual(Function.constTrue(), true)
|
|
535
|
+
* ```
|
|
536
|
+
*
|
|
537
|
+
* @category constants
|
|
538
|
+
* @since 2.0.0
|
|
539
|
+
*/
|
|
540
|
+
const constTrue = constant(true);
|
|
541
|
+
/**
|
|
542
|
+
* Returns `false` when called.
|
|
543
|
+
*
|
|
544
|
+
* **When to use**
|
|
545
|
+
*
|
|
546
|
+
* Use when you need a thunk that returns `false` on every invocation.
|
|
547
|
+
*
|
|
548
|
+
* **Example** (Returning false from a thunk)
|
|
549
|
+
*
|
|
550
|
+
* ```ts
|
|
551
|
+
* import { Function } from "effect"
|
|
552
|
+
* import * as assert from "node:assert"
|
|
553
|
+
*
|
|
554
|
+
* assert.deepStrictEqual(Function.constFalse(), false)
|
|
555
|
+
* ```
|
|
556
|
+
*
|
|
557
|
+
* @category constants
|
|
558
|
+
* @since 2.0.0
|
|
559
|
+
*/
|
|
560
|
+
const constFalse = constant(false);
|
|
561
|
+
/**
|
|
562
|
+
* Returns `null` when called.
|
|
563
|
+
*
|
|
564
|
+
* **When to use**
|
|
565
|
+
*
|
|
566
|
+
* Use when you need a thunk that returns `null` on every invocation.
|
|
567
|
+
*
|
|
568
|
+
* **Example** (Returning null from a thunk)
|
|
569
|
+
*
|
|
570
|
+
* ```ts
|
|
571
|
+
* import { Function } from "effect"
|
|
572
|
+
* import * as assert from "node:assert"
|
|
573
|
+
*
|
|
574
|
+
* assert.deepStrictEqual(Function.constNull(), null)
|
|
575
|
+
* ```
|
|
576
|
+
*
|
|
577
|
+
* @category constants
|
|
578
|
+
* @since 2.0.0
|
|
579
|
+
*/
|
|
580
|
+
const constNull = constant(null);
|
|
581
|
+
/**
|
|
582
|
+
* Returns `undefined` when called.
|
|
583
|
+
*
|
|
584
|
+
* **When to use**
|
|
585
|
+
*
|
|
586
|
+
* Use when you need a thunk that returns `undefined` on every invocation.
|
|
587
|
+
*
|
|
588
|
+
* **Example** (Returning undefined from a thunk)
|
|
589
|
+
*
|
|
590
|
+
* ```ts
|
|
591
|
+
* import { Function } from "effect"
|
|
592
|
+
* import * as assert from "node:assert"
|
|
593
|
+
*
|
|
594
|
+
* assert.deepStrictEqual(Function.constUndefined(), undefined)
|
|
595
|
+
* ```
|
|
596
|
+
*
|
|
597
|
+
* @category constants
|
|
598
|
+
* @since 2.0.0
|
|
599
|
+
*/
|
|
600
|
+
const constUndefined = constant(void 0);
|
|
601
|
+
/**
|
|
602
|
+
* Composes two functions, `ab` and `bc` into a single function that takes in an argument `a` of type `A` and returns a result of type `C`.
|
|
603
|
+
* The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
|
|
604
|
+
*
|
|
605
|
+
* **When to use**
|
|
606
|
+
*
|
|
607
|
+
* Use to compose exactly two unary functions into a reusable unary function.
|
|
608
|
+
*
|
|
609
|
+
* **Example** (Composing two functions)
|
|
610
|
+
*
|
|
611
|
+
* ```ts
|
|
612
|
+
* import { Function } from "effect"
|
|
613
|
+
* import * as assert from "node:assert"
|
|
614
|
+
*
|
|
615
|
+
* const increment = (n: number) => n + 1
|
|
616
|
+
* const square = (n: number) => n * n
|
|
617
|
+
*
|
|
618
|
+
* assert.strictEqual(Function.compose(increment, square)(2), 9)
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* @see {@link flow} for composing a left-to-right sequence of functions
|
|
622
|
+
* @see {@link pipe} for applying a value through a left-to-right sequence immediately
|
|
623
|
+
*
|
|
624
|
+
* @category combinators
|
|
625
|
+
* @since 2.0.0
|
|
626
|
+
*/
|
|
627
|
+
const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
|
|
628
|
+
/**
|
|
629
|
+
* Marks an impossible branch by accepting a `never` value and returning any
|
|
630
|
+
* type.
|
|
631
|
+
*
|
|
632
|
+
* **When to use**
|
|
633
|
+
*
|
|
634
|
+
* Use when you need a return value in a branch that exhaustive checks prove
|
|
635
|
+
* cannot be reached.
|
|
636
|
+
*
|
|
637
|
+
* **Gotchas**
|
|
638
|
+
*
|
|
639
|
+
* Calling `absurd` throws, because a value of type `never` should be
|
|
640
|
+
* impossible at runtime.
|
|
641
|
+
*
|
|
642
|
+
* **Example** (Handling impossible values)
|
|
643
|
+
*
|
|
644
|
+
* ```ts
|
|
645
|
+
* import { absurd } from "effect"
|
|
646
|
+
*
|
|
647
|
+
* const handleNever = (value: never) => {
|
|
648
|
+
* return absurd(value) // This will throw an error if called
|
|
649
|
+
* }
|
|
650
|
+
* ```
|
|
651
|
+
*
|
|
652
|
+
* @category utility types
|
|
653
|
+
* @since 2.0.0
|
|
654
|
+
*/
|
|
655
|
+
const absurd = (_) => {
|
|
656
|
+
throw new Error("Called `absurd` function which should be uncallable");
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* Creates a compile-time placeholder for a value of any type.
|
|
660
|
+
*
|
|
661
|
+
* **When to use**
|
|
662
|
+
*
|
|
663
|
+
* Use as a temporary typed placeholder while developing incomplete code.
|
|
664
|
+
*
|
|
665
|
+
* **Gotchas**
|
|
666
|
+
*
|
|
667
|
+
* `hole` is intended for temporary development use. If the placeholder is
|
|
668
|
+
* evaluated at runtime, it throws.
|
|
669
|
+
*
|
|
670
|
+
* **Example** (Creating a development placeholder)
|
|
671
|
+
*
|
|
672
|
+
* ```ts
|
|
673
|
+
* import { hole } from "effect"
|
|
674
|
+
*
|
|
675
|
+
* // Intentionally not called: `hole` throws if the placeholder is evaluated.
|
|
676
|
+
* const buildUser = (id: number): { readonly id: number; readonly name: string } => ({
|
|
677
|
+
* id,
|
|
678
|
+
* name: hole<string>()
|
|
679
|
+
* })
|
|
680
|
+
*
|
|
681
|
+
* console.log(typeof buildUser) // "function"
|
|
682
|
+
* ```
|
|
683
|
+
*
|
|
684
|
+
* @category utility types
|
|
685
|
+
* @since 2.0.0
|
|
686
|
+
*/
|
|
687
|
+
const hole = cast(absurd);
|
|
688
|
+
/**
|
|
689
|
+
* Drops the longest prefix of elements from an array that satisfy the given predicate.
|
|
690
|
+
*
|
|
691
|
+
* Supports both data-first and data-last (`pipe`-friendly) call styles.
|
|
692
|
+
*
|
|
693
|
+
* @param pred - The predicate to test each element with.
|
|
694
|
+
* @returns A new array without the matching prefix.
|
|
695
|
+
* @example
|
|
696
|
+
* ```ts
|
|
697
|
+
* import * as assert from "node:assert"
|
|
698
|
+
* import { dropWhile, pipe } from "@local/eff"
|
|
699
|
+
*
|
|
700
|
+
* // data-first
|
|
701
|
+
* assert.deepStrictEqual(dropWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [3, 2, 1])
|
|
702
|
+
*
|
|
703
|
+
* // data-last
|
|
704
|
+
* assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], dropWhile((n: number) => n < 3)), [3, 2, 1])
|
|
705
|
+
* ```
|
|
706
|
+
* @category array
|
|
707
|
+
*/
|
|
708
|
+
const dropWhile = dual(2, (xs, pred) => {
|
|
709
|
+
const len = xs.length;
|
|
710
|
+
let idx = 0;
|
|
711
|
+
while (idx < len && pred(xs[idx])) idx++;
|
|
712
|
+
return xs.slice(idx);
|
|
713
|
+
});
|
|
714
|
+
/**
|
|
715
|
+
* Takes the longest prefix of elements from an array that satisfy the given predicate.
|
|
716
|
+
*
|
|
717
|
+
* Supports both data-first and data-last (`pipe`-friendly) call styles.
|
|
718
|
+
*
|
|
719
|
+
* @param pred - The predicate to test each element with.
|
|
720
|
+
* @returns A new array containing only the matching prefix.
|
|
721
|
+
* @example
|
|
722
|
+
* ```ts
|
|
723
|
+
* import * as assert from "node:assert"
|
|
724
|
+
* import { pipe, takeWhile } from "@local/eff"
|
|
725
|
+
*
|
|
726
|
+
* // data-first
|
|
727
|
+
* assert.deepStrictEqual(takeWhile([1, 2, 3, 2, 1], (n: number) => n < 3), [1, 2])
|
|
728
|
+
*
|
|
729
|
+
* // data-last
|
|
730
|
+
* assert.deepStrictEqual(pipe([1, 2, 3, 2, 1], takeWhile((n: number) => n < 3)), [1, 2])
|
|
731
|
+
* ```
|
|
732
|
+
* @category array
|
|
733
|
+
*/
|
|
734
|
+
const takeWhile = dual(2, (xs, pred) => {
|
|
735
|
+
const len = xs.length;
|
|
736
|
+
let idx = 0;
|
|
737
|
+
while (idx < len && pred(xs[idx])) idx++;
|
|
738
|
+
return xs.slice(0, idx);
|
|
739
|
+
});
|
|
740
|
+
|
|
252
741
|
//#endregion
|
|
253
742
|
//#region src/rules/no-key-after-spread/no-key-after-spread.ts
|
|
254
743
|
const RULE_NAME$4 = "no-key-after-spread";
|
|
@@ -256,7 +745,7 @@ var no_key_after_spread_default = createRule({
|
|
|
256
745
|
meta: {
|
|
257
746
|
type: "problem",
|
|
258
747
|
docs: { description: "Prevent patterns that cause deoptimization when using the automatic JSX runtime." },
|
|
259
|
-
messages: {
|
|
748
|
+
messages: { default: "Placing 'key' after spread props causes deoptimization when using the automatic JSX runtime. Put 'key' before any spread props." },
|
|
260
749
|
schema: []
|
|
261
750
|
},
|
|
262
751
|
name: RULE_NAME$4,
|
|
@@ -265,20 +754,12 @@ var no_key_after_spread_default = createRule({
|
|
|
265
754
|
});
|
|
266
755
|
function create$4(context) {
|
|
267
756
|
const { jsx } = core.getJsxConfig(context);
|
|
268
|
-
if (
|
|
757
|
+
if (jsx !== ts.JsxEmit.ReactJSX && jsx !== ts.JsxEmit.ReactJSXDev) return {};
|
|
269
758
|
return { JSXOpeningElement(node) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (firstSpreadPropIndex == null) continue;
|
|
277
|
-
if (prop.name.type === AST_NODE_TYPES.JSXIdentifier && prop.name.name === "key" && index > firstSpreadPropIndex) context.report({
|
|
278
|
-
messageId: "noKeyAfterSpread",
|
|
279
|
-
node: prop
|
|
280
|
-
});
|
|
281
|
-
}
|
|
759
|
+
for (const n of dropWhile(node.attributes, not(Check.is(AST_NODE_TYPES.JSXSpreadAttribute))).filter(isAttribute("key"))) context.report({
|
|
760
|
+
messageId: "default",
|
|
761
|
+
node: n
|
|
762
|
+
});
|
|
282
763
|
} };
|
|
283
764
|
}
|
|
284
765
|
|
|
@@ -302,15 +783,12 @@ var no_leaked_dollar_default = createRule({
|
|
|
302
783
|
defaultOptions: []
|
|
303
784
|
});
|
|
304
785
|
function create$3(context) {
|
|
305
|
-
|
|
306
|
-
* Visitor function for JSXElement and JSXFragment nodes
|
|
307
|
-
* @param node The JSXElement or JSXFragment node to be checked
|
|
308
|
-
*/
|
|
786
|
+
if (!context.sourceCode.text.includes("$")) return {};
|
|
309
787
|
function visit(node) {
|
|
310
788
|
for (const [index, child] of node.children.entries()) {
|
|
311
789
|
if (child.type !== AST_NODE_TYPES.JSXText || !child.value.endsWith("$")) continue;
|
|
312
790
|
if (node.children[index + 1]?.type !== AST_NODE_TYPES.JSXExpressionContainer) continue;
|
|
313
|
-
if (child.value === "$" && node.children.
|
|
791
|
+
if (child.value.trim() === "$" && node.children.every((sibling, siblingIndex) => siblingIndex === index || siblingIndex === index + 1 || isNonSubstantiveChild(sibling))) continue;
|
|
314
792
|
if (!context.sourceCode.getText(child).endsWith("$")) continue;
|
|
315
793
|
const dollarStart = child.range[1] - 1;
|
|
316
794
|
const dollarEnd = child.range[1];
|
|
@@ -335,6 +813,15 @@ function create$3(context) {
|
|
|
335
813
|
JSXFragment: visit
|
|
336
814
|
};
|
|
337
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* Whether a JSX child carries no renderable content: padding whitespace, an
|
|
818
|
+
* empty expression (ex: `{}` or a comment), or an empty string expression.
|
|
819
|
+
* @param child The JSX child node to check.
|
|
820
|
+
*/
|
|
821
|
+
function isNonSubstantiveChild(child) {
|
|
822
|
+
if (isWhitespaceText(child) || isEmptyStringExpression(child)) return true;
|
|
823
|
+
return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
|
|
824
|
+
}
|
|
338
825
|
|
|
339
826
|
//#endregion
|
|
340
827
|
//#region src/rules/no-leaked-semicolon/no-leaked-semicolon.ts
|
|
@@ -343,6 +830,7 @@ var no_leaked_semicolon_default = createRule({
|
|
|
343
830
|
meta: {
|
|
344
831
|
type: "problem",
|
|
345
832
|
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." },
|
|
833
|
+
fixable: "code",
|
|
346
834
|
hasSuggestions: true,
|
|
347
835
|
messages: {
|
|
348
836
|
default: "Leaked ';' in JSX. This ';' will be rendered as text nodes.",
|
|
@@ -354,30 +842,22 @@ var no_leaked_semicolon_default = createRule({
|
|
|
354
842
|
create: create$2,
|
|
355
843
|
defaultOptions: []
|
|
356
844
|
});
|
|
357
|
-
function hasLeakedSemicolon(text) {
|
|
358
|
-
return /^;[ \t]*(?:\r\n|\r|\n)/u.test(text);
|
|
359
|
-
}
|
|
360
845
|
function create$2(context) {
|
|
361
846
|
function visit(node) {
|
|
362
|
-
if (!
|
|
363
|
-
if (
|
|
364
|
-
const semicolonStart = node.range[0];
|
|
365
|
-
const semicolonEnd = node.range[0] + 1;
|
|
847
|
+
if (!Check.isJSXElementOrFragment(node.parent)) return;
|
|
848
|
+
if (!/^;+[ \t]*(?:\r\n|\r|\n)/u.test(context.sourceCode.getText(node))) return;
|
|
366
849
|
context.report({
|
|
367
850
|
messageId: "default",
|
|
368
851
|
node,
|
|
369
852
|
suggest: [{
|
|
370
853
|
fix(fixer) {
|
|
371
|
-
return fixer.removeRange([
|
|
854
|
+
return fixer.removeRange([node.range[0], node.range[0] + 1]);
|
|
372
855
|
},
|
|
373
856
|
messageId: "removeSemicolon"
|
|
374
857
|
}]
|
|
375
858
|
});
|
|
376
859
|
}
|
|
377
|
-
return {
|
|
378
|
-
JSXText: visit,
|
|
379
|
-
Literal: visit
|
|
380
|
-
};
|
|
860
|
+
return { JSXText: visit };
|
|
381
861
|
}
|
|
382
862
|
|
|
383
863
|
//#endregion
|
|
@@ -430,7 +910,6 @@ const schema = [{
|
|
|
430
910
|
var no_useless_fragment_default = createRule({
|
|
431
911
|
meta: {
|
|
432
912
|
type: "suggestion",
|
|
433
|
-
defaultOptions: [...defaultOptions],
|
|
434
913
|
docs: { description: "Disallows useless fragment elements." },
|
|
435
914
|
fixable: "code",
|
|
436
915
|
messages: { default: "A fragment {{reason}} is useless." },
|
|
@@ -441,88 +920,96 @@ var no_useless_fragment_default = createRule({
|
|
|
441
920
|
defaultOptions
|
|
442
921
|
});
|
|
443
922
|
function create(context, [option]) {
|
|
444
|
-
const
|
|
923
|
+
const options = {
|
|
924
|
+
allowEmptyFragment: option.allowEmptyFragment ?? false,
|
|
925
|
+
allowExpressions: option.allowExpressions ?? true
|
|
926
|
+
};
|
|
445
927
|
const jsxConfig = core.getJsxConfig(context);
|
|
446
|
-
/**
|
|
447
|
-
* Whether the fragment has too few meaningful children to justify its
|
|
448
|
-
* existence (the "contains less than two children" reason).
|
|
449
|
-
* @param node The fragment node to check.
|
|
450
|
-
*/
|
|
451
|
-
function isContentUseless(node) {
|
|
452
|
-
if (node.children.length === 0) return !allowEmptyFragment;
|
|
453
|
-
const insideJsx = Check.isJSXElementOrFragment(node.parent);
|
|
454
|
-
if (allowExpressions && !insideJsx && node.children.length === 1 && node.children[0]?.type === AST_NODE_TYPES.JSXText) return false;
|
|
455
|
-
const meaningful = getChildren(node);
|
|
456
|
-
if (meaningful.length === 0) return true;
|
|
457
|
-
if (meaningful.length > 1) return false;
|
|
458
|
-
if (meaningful[0]?.type === AST_NODE_TYPES.JSXExpressionContainer) return !allowExpressions;
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
463
|
-
* @param node The fragment node to check.
|
|
464
|
-
*/
|
|
465
|
-
function isSafeToFix(node) {
|
|
466
|
-
if (node.type === AST_NODE_TYPES.JSXElement && node.openingElement.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute)) return false;
|
|
467
|
-
if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
|
|
468
|
-
if (node.children.length === 0) return false;
|
|
469
|
-
return node.children.every((child) => {
|
|
470
|
-
if (Check.isJSXElementOrFragment(child)) return true;
|
|
471
|
-
return child.type === AST_NODE_TYPES.JSXText && isWhitespaceText(child);
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Build an autofix that unwraps the fragment, replacing it with its
|
|
476
|
-
* meaningful children content. Returns `null` when the fix is unsafe.
|
|
477
|
-
* @param node The fragment node to fix.
|
|
478
|
-
*/
|
|
479
|
-
function buildFix(node) {
|
|
480
|
-
if (!isSafeToFix(node)) return null;
|
|
481
|
-
return (fixer) => {
|
|
482
|
-
let text = "";
|
|
483
|
-
for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
|
|
484
|
-
const cleaned = collapseMultilineText(child.value);
|
|
485
|
-
if (cleaned != null) text += cleaned;
|
|
486
|
-
} else text += context.sourceCode.getText(child);
|
|
487
|
-
let start = node.range[0];
|
|
488
|
-
if (text === "" && node.children.length > 0) {
|
|
489
|
-
const lineStart = Math.max(context.sourceCode.text.lastIndexOf("\n", start - 1), context.sourceCode.text.lastIndexOf("\r", start - 1)) + 1;
|
|
490
|
-
if (/^[ \t]*$/u.test(context.sourceCode.text.slice(lineStart, start))) start = lineStart;
|
|
491
|
-
}
|
|
492
|
-
return fixer.replaceTextRange([start, node.range[1]], text);
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Inspect a fragment node and report if it is useless.
|
|
497
|
-
*
|
|
498
|
-
* A fragment may be reported for **two independent reasons** on the same
|
|
499
|
-
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
500
|
-
* and* "contains less than two children").
|
|
501
|
-
* @param node The fragment node to inspect.
|
|
502
|
-
*/
|
|
503
|
-
function visit(node) {
|
|
504
|
-
if (isHostElement(node.parent)) context.report({
|
|
505
|
-
data: { reason: "placed inside a host component" },
|
|
506
|
-
fix: buildFix(node),
|
|
507
|
-
messageId: "default",
|
|
508
|
-
node
|
|
509
|
-
});
|
|
510
|
-
if (isContentUseless(node)) context.report({
|
|
511
|
-
data: { reason: "contains less than two children" },
|
|
512
|
-
fix: buildFix(node),
|
|
513
|
-
messageId: "default",
|
|
514
|
-
node
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
928
|
return {
|
|
518
929
|
JSXElement(node) {
|
|
519
930
|
if (!isFragmentElement(node, jsxConfig.jsxFragmentFactory)) return;
|
|
520
931
|
if (hasAnyAttribute(context, node, ["key", "ref"])) return;
|
|
521
|
-
|
|
932
|
+
visitFragment(context, node, options);
|
|
522
933
|
},
|
|
523
934
|
JSXFragment(node) {
|
|
524
|
-
|
|
935
|
+
visitFragment(context, node, options);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Inspect a fragment node and report if it is useless.
|
|
941
|
+
*
|
|
942
|
+
* A fragment may be reported for **two independent reasons** on the same
|
|
943
|
+
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
944
|
+
* and "contains less than two children").
|
|
945
|
+
* @param context The rule context.
|
|
946
|
+
* @param node The fragment node to inspect.
|
|
947
|
+
* @param options The resolved rule options.
|
|
948
|
+
*/
|
|
949
|
+
function visitFragment(context, node, options) {
|
|
950
|
+
const reasons = [];
|
|
951
|
+
if (isHostElement(node.parent)) reasons.push("placed inside a host component");
|
|
952
|
+
if (isContentUseless(node, options)) reasons.push("contains less than two children");
|
|
953
|
+
if (reasons.length === 0) return;
|
|
954
|
+
const fix = buildFix(context, node);
|
|
955
|
+
for (const reason of reasons) context.report({
|
|
956
|
+
data: { reason },
|
|
957
|
+
fix,
|
|
958
|
+
messageId: "default",
|
|
959
|
+
node
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Whether the fragment has too few meaningful children to justify its
|
|
964
|
+
* existence (the "contains less than two children" reason).
|
|
965
|
+
* @param node The fragment node to check.
|
|
966
|
+
* @param options The resolved rule options.
|
|
967
|
+
*/
|
|
968
|
+
function isContentUseless(node, options) {
|
|
969
|
+
const { allowEmptyFragment, allowExpressions } = options;
|
|
970
|
+
if (node.children.length === 0) return !allowEmptyFragment;
|
|
971
|
+
const insideJsx = Check.isJSXElementOrFragment(node.parent);
|
|
972
|
+
if (allowExpressions && !insideJsx && node.children.length === 1 && node.children[0]?.type === AST_NODE_TYPES.JSXText) return false;
|
|
973
|
+
const meaningful = getChildren(node);
|
|
974
|
+
if (meaningful.length === 0) return true;
|
|
975
|
+
if (meaningful.length > 1) return false;
|
|
976
|
+
if (meaningful[0]?.type === AST_NODE_TYPES.JSXExpressionContainer) return !allowExpressions;
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
981
|
+
* @param node The fragment node to check.
|
|
982
|
+
*/
|
|
983
|
+
function isSafeToFix(node) {
|
|
984
|
+
if (node.type === AST_NODE_TYPES.JSXElement && node.openingElement.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXSpreadAttribute)) return false;
|
|
985
|
+
if (Check.isJSXElementOrFragment(node.parent)) return isHostElement(node.parent);
|
|
986
|
+
if (node.children.length === 0) return false;
|
|
987
|
+
return node.children.every((child) => {
|
|
988
|
+
if (Check.isJSXElementOrFragment(child)) return true;
|
|
989
|
+
return child.type === AST_NODE_TYPES.JSXText && isWhitespaceText(child);
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Build an autofix that unwraps the fragment, replacing it with its
|
|
994
|
+
* meaningful children content. Returns `null` when the fix is unsafe.
|
|
995
|
+
* @param context The rule context.
|
|
996
|
+
* @param node The fragment node to fix.
|
|
997
|
+
*/
|
|
998
|
+
function buildFix(context, node) {
|
|
999
|
+
if (!isSafeToFix(node)) return null;
|
|
1000
|
+
return (fixer) => {
|
|
1001
|
+
const sourceCode = context.sourceCode;
|
|
1002
|
+
let text = "";
|
|
1003
|
+
for (const child of node.children) if (child.type === AST_NODE_TYPES.JSXText) {
|
|
1004
|
+
const cleaned = collapseMultilineText(child.value);
|
|
1005
|
+
if (cleaned != null) text += cleaned;
|
|
1006
|
+
} else text += sourceCode.getText(child);
|
|
1007
|
+
let start = node.range[0];
|
|
1008
|
+
if (text === "" && node.children.length > 0) {
|
|
1009
|
+
const lineStart = Math.max(sourceCode.text.lastIndexOf("\n", start - 1), sourceCode.text.lastIndexOf("\r", start - 1)) + 1;
|
|
1010
|
+
if (/^[ \t]*$/u.test(sourceCode.text.slice(lineStart, start))) start = lineStart;
|
|
525
1011
|
}
|
|
1012
|
+
return fixer.replaceTextRange([start, node.range[1]], text);
|
|
526
1013
|
};
|
|
527
1014
|
}
|
|
528
1015
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-jsx",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.1",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for React Flavored JSX rules.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -39,18 +39,18 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@typescript-eslint/types": "^8.64.0",
|
|
41
41
|
"@typescript-eslint/utils": "^8.64.0",
|
|
42
|
-
"@eslint-react/ast": "5.
|
|
43
|
-
"@eslint-react/eslint": "5.
|
|
44
|
-
"@eslint-react/
|
|
45
|
-
"@eslint-react/
|
|
46
|
-
"@eslint-react/
|
|
42
|
+
"@eslint-react/ast": "5.17.1",
|
|
43
|
+
"@eslint-react/eslint": "5.17.1",
|
|
44
|
+
"@eslint-react/core": "5.17.1",
|
|
45
|
+
"@eslint-react/jsx": "5.17.1",
|
|
46
|
+
"@eslint-react/shared": "5.17.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.2.17",
|
|
50
50
|
"@types/react-dom": "^19.2.3",
|
|
51
51
|
"dedent": "^1.7.2",
|
|
52
52
|
"eslint": "^10.7.0",
|
|
53
|
-
"tsdown": "^0.22.
|
|
53
|
+
"tsdown": "^0.22.8",
|
|
54
54
|
"typescript": "6.0.3",
|
|
55
55
|
"@local/configs": "0.0.0",
|
|
56
56
|
"@local/eff": "0.0.0"
|