@eslint-react/core 4.2.4-beta.0 → 5.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 +444 -386
- package/dist/index.js +539 -524
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import * as ast from "@eslint-react/ast";
|
|
2
|
+
import { resolveImportSource } from "@eslint-react/var";
|
|
2
3
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
3
|
-
import {
|
|
4
|
-
import { P, match } from "ts-pattern";
|
|
5
|
-
import { JsxDetectionHint, isJsxLike } from "@eslint-react/jsx";
|
|
4
|
+
import { isAPIFromReact as isAPIFromReact$1 } from "@eslint-react/core";
|
|
6
5
|
import { IdGenerator, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE } from "@eslint-react/shared";
|
|
6
|
+
import { JsxDetectionHint, isJsxLike } from "@eslint-react/jsx";
|
|
7
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES$1 } from "@typescript-eslint/utils";
|
|
8
|
+
import { P, isMatching, match } from "ts-pattern";
|
|
9
|
+
import ts from "typescript";
|
|
7
10
|
|
|
8
11
|
//#region ../../.pkgs/eff/dist/index.js
|
|
9
12
|
/**
|
|
10
|
-
* Returns its argument.
|
|
11
|
-
*
|
|
12
|
-
* @param x - The value to return.
|
|
13
|
-
* @returns The input value unchanged.
|
|
14
|
-
*/
|
|
15
|
-
function identity(x) {
|
|
16
|
-
return x;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
13
|
* Creates a function that can be used in a data-last (aka `pipe`able) or
|
|
20
14
|
* data-first style.
|
|
21
15
|
*
|
|
@@ -118,24 +112,6 @@ function constFalse() {
|
|
|
118
112
|
return false;
|
|
119
113
|
}
|
|
120
114
|
/**
|
|
121
|
-
* Reverses the order of arguments for a curried function.
|
|
122
|
-
*
|
|
123
|
-
* @param f - The function to flip.
|
|
124
|
-
* @returns A new function with the argument order reversed.
|
|
125
|
-
* @example
|
|
126
|
-
* ```ts
|
|
127
|
-
* import * as assert from "node:assert"
|
|
128
|
-
* import { flip } from "effect/Function"
|
|
129
|
-
*
|
|
130
|
-
* const f = (a: number) => (b: string) => a - b.length
|
|
131
|
-
*
|
|
132
|
-
* assert.deepStrictEqual(flip(f)('aaa')(2), -1)
|
|
133
|
-
* ```
|
|
134
|
-
*
|
|
135
|
-
* @since 1.0.0
|
|
136
|
-
*/
|
|
137
|
-
const flip = (f) => (...b) => (...a) => f(...a)(...b);
|
|
138
|
-
/**
|
|
139
115
|
* 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`.
|
|
140
116
|
* The result is obtained by first applying the `ab` function to `a` and then applying the `bc` function to the result of `ab`.
|
|
141
117
|
*
|
|
@@ -158,91 +134,13 @@ const flip = (f) => (...b) => (...a) => f(...a)(...b);
|
|
|
158
134
|
const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
|
|
159
135
|
|
|
160
136
|
//#endregion
|
|
161
|
-
//#region src/api
|
|
162
|
-
/**
|
|
163
|
-
* Get the arguments of a require expression
|
|
164
|
-
* @param node The node to match
|
|
165
|
-
* @returns The require expression arguments or null if the node is not a require expression
|
|
166
|
-
* @internal
|
|
167
|
-
*/
|
|
168
|
-
function getRequireExpressionArguments(node) {
|
|
169
|
-
return match(node).with({
|
|
170
|
-
type: AST_NODE_TYPES.CallExpression,
|
|
171
|
-
arguments: P.select(),
|
|
172
|
-
callee: {
|
|
173
|
-
type: AST_NODE_TYPES.Identifier,
|
|
174
|
-
name: "require"
|
|
175
|
-
}
|
|
176
|
-
}, identity).with({
|
|
177
|
-
type: AST_NODE_TYPES.MemberExpression,
|
|
178
|
-
object: P.select()
|
|
179
|
-
}, getRequireExpressionArguments).otherwise(() => null);
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Find the import source of a variable
|
|
183
|
-
* @param name The variable name
|
|
184
|
-
* @param initialScope The initial scope to search
|
|
185
|
-
* @returns The import source or null if not found
|
|
186
|
-
*/
|
|
187
|
-
function findImportSource(name, initialScope) {
|
|
188
|
-
return findImportSourceImpl(name, initialScope, /* @__PURE__ */ new Set());
|
|
189
|
-
}
|
|
190
|
-
function findImportSourceImpl(name, initialScope, visited) {
|
|
191
|
-
if (visited.has(name)) return null;
|
|
192
|
-
visited.add(name);
|
|
193
|
-
const latestDef = findVariable(initialScope, name)?.defs.at(-1);
|
|
194
|
-
if (latestDef == null) return null;
|
|
195
|
-
const { node, parent } = latestDef;
|
|
196
|
-
if (node.type === AST_NODE_TYPES.VariableDeclarator && node.init != null) {
|
|
197
|
-
const { init } = node;
|
|
198
|
-
if (init.type === AST_NODE_TYPES.MemberExpression && init.object.type === AST_NODE_TYPES.Identifier) return findImportSourceImpl(init.object.name, initialScope, visited);
|
|
199
|
-
if (init.type === AST_NODE_TYPES.Identifier) return findImportSourceImpl(init.name, initialScope, visited);
|
|
200
|
-
const arg0 = getRequireExpressionArguments(init)?.[0];
|
|
201
|
-
if (arg0 == null || !ast.isLiteral(arg0, "string")) return null;
|
|
202
|
-
return arg0.value;
|
|
203
|
-
}
|
|
204
|
-
if (parent?.type === AST_NODE_TYPES.ImportDeclaration) return parent.source.value;
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
//#endregion
|
|
209
|
-
//#region src/api/is-from-react.ts
|
|
210
|
-
/**
|
|
211
|
-
* Check if a variable is initialized from React import
|
|
212
|
-
* @param name The variable name
|
|
213
|
-
* @param initialScope The initial scope
|
|
214
|
-
* @param importSource Alternative import source of React (ex: "preact/compat")
|
|
215
|
-
* @returns True if the variable is initialized or derived from React import
|
|
216
|
-
*/
|
|
217
|
-
function isInitializedFromReact(name, initialScope, importSource = "react") {
|
|
218
|
-
return name.toLowerCase() === "react" || Boolean(findImportSource(name, initialScope)?.startsWith(importSource));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
//#endregion
|
|
222
|
-
//#region src/api/is-from-react-native.ts
|
|
223
|
-
/**
|
|
224
|
-
* if a variable is initialized from React Native import
|
|
225
|
-
* @param name The variable name
|
|
226
|
-
* @param initialScope The initial scope
|
|
227
|
-
* @param importSource Alternative import source of React Native (ex: "react-native-web")
|
|
228
|
-
* @returns True if the variable is initialized from React Native import
|
|
229
|
-
*/
|
|
230
|
-
function isInitializedFromReactNative(name, initialScope, importSource = "react-native") {
|
|
231
|
-
return [
|
|
232
|
-
"react_native",
|
|
233
|
-
"reactnative",
|
|
234
|
-
"rn"
|
|
235
|
-
].includes(name.toLowerCase()) || Boolean(findImportSource(name, initialScope)?.startsWith(importSource));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
//#endregion
|
|
239
|
-
//#region src/api/is-react-api.ts
|
|
137
|
+
//#region src/api.ts
|
|
240
138
|
/**
|
|
241
139
|
* Check if the node is a React API identifier or member expression
|
|
242
140
|
* @param api The React API name to check against (ex: "useState", "React.memo")
|
|
243
141
|
* @returns A predicate function to check if a node matches the API
|
|
244
142
|
*/
|
|
245
|
-
function
|
|
143
|
+
function isAPI(api) {
|
|
246
144
|
const func = (context, node) => {
|
|
247
145
|
if (node == null) return false;
|
|
248
146
|
const getText = (n) => context.sourceCode.getText(n);
|
|
@@ -258,260 +156,105 @@ function isReactAPI(api) {
|
|
|
258
156
|
* @param api The React API name to check against
|
|
259
157
|
* @returns A predicate function to check if a node is a call to the API
|
|
260
158
|
*/
|
|
261
|
-
function
|
|
159
|
+
function isAPICall(api) {
|
|
262
160
|
const func = (context, node) => {
|
|
263
161
|
if (node == null) return false;
|
|
264
162
|
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
265
|
-
return
|
|
163
|
+
return isAPI(api)(context, ast.getUnderlyingExpression(node.callee));
|
|
266
164
|
};
|
|
267
165
|
return dual(2, func);
|
|
268
166
|
}
|
|
269
|
-
const isCaptureOwnerStack = isReactAPI("captureOwnerStack");
|
|
270
|
-
const isChildrenCount = isReactAPI("Children.count");
|
|
271
|
-
const isChildrenForEach = isReactAPI("Children.forEach");
|
|
272
|
-
const isChildrenMap = isReactAPI("Children.map");
|
|
273
|
-
const isChildrenOnly = isReactAPI("Children.only");
|
|
274
|
-
const isChildrenToArray = isReactAPI("Children.toArray");
|
|
275
|
-
const isCloneElement = isReactAPI("cloneElement");
|
|
276
|
-
const isCreateContext = isReactAPI("createContext");
|
|
277
|
-
const isCreateElement = isReactAPI("createElement");
|
|
278
|
-
const isCreateRef = isReactAPI("createRef");
|
|
279
|
-
const isForwardRef = isReactAPI("forwardRef");
|
|
280
|
-
const isMemo = isReactAPI("memo");
|
|
281
|
-
const isLazy = isReactAPI("lazy");
|
|
282
|
-
const isCaptureOwnerStackCall = isReactAPICall("captureOwnerStack");
|
|
283
|
-
const isChildrenCountCall = isReactAPICall("Children.count");
|
|
284
|
-
const isChildrenForEachCall = isReactAPICall("Children.forEach");
|
|
285
|
-
const isChildrenMapCall = isReactAPICall("Children.map");
|
|
286
|
-
const isChildrenOnlyCall = isReactAPICall("Children.only");
|
|
287
|
-
const isChildrenToArrayCall = isReactAPICall("Children.toArray");
|
|
288
|
-
const isCloneElementCall = isReactAPICall("cloneElement");
|
|
289
|
-
const isCreateContextCall = isReactAPICall("createContext");
|
|
290
|
-
const isCreateElementCall = isReactAPICall("createElement");
|
|
291
|
-
const isCreateRefCall = isReactAPICall("createRef");
|
|
292
|
-
const isForwardRefCall = isReactAPICall("forwardRef");
|
|
293
|
-
const isMemoCall = isReactAPICall("memo");
|
|
294
|
-
const isLazyCall = isReactAPICall("lazy");
|
|
295
|
-
|
|
296
|
-
//#endregion
|
|
297
|
-
//#region src/hook/hook-name.ts
|
|
298
|
-
const REACT_BUILTIN_HOOK_NAMES = [
|
|
299
|
-
"use",
|
|
300
|
-
"useActionState",
|
|
301
|
-
"useCallback",
|
|
302
|
-
"useContext",
|
|
303
|
-
"useDebugValue",
|
|
304
|
-
"useDeferredValue",
|
|
305
|
-
"useEffect",
|
|
306
|
-
"useFormStatus",
|
|
307
|
-
"useId",
|
|
308
|
-
"useImperativeHandle",
|
|
309
|
-
"useInsertionEffect",
|
|
310
|
-
"useLayoutEffect",
|
|
311
|
-
"useMemo",
|
|
312
|
-
"useOptimistic",
|
|
313
|
-
"useReducer",
|
|
314
|
-
"useRef",
|
|
315
|
-
"useState",
|
|
316
|
-
"useSyncExternalStore",
|
|
317
|
-
"useTransition"
|
|
318
|
-
];
|
|
319
|
-
/**
|
|
320
|
-
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
|
321
|
-
* character to exclude identifiers like "user".
|
|
322
|
-
* @param name The name of the identifier to check.
|
|
323
|
-
* @see https://github.com/facebook/react/blob/1d6c8168db1d82713202e842df3167787ffa00ed/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts#L16
|
|
324
|
-
*/
|
|
325
|
-
function isHookName(name) {
|
|
326
|
-
return name === "use" || /^use[A-Z0-9]/.test(name);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
//#endregion
|
|
330
|
-
//#region src/hook/hook-is.ts
|
|
331
|
-
/**
|
|
332
|
-
* Determine if a function node is a React Hook based on its name.
|
|
333
|
-
* @param node The function node to check
|
|
334
|
-
* @returns True if the function is a React Hook, false otherwise
|
|
335
|
-
*/
|
|
336
|
-
function isHookDefinition(node) {
|
|
337
|
-
if (node == null) return false;
|
|
338
|
-
const id = ast.getFunctionId(node);
|
|
339
|
-
switch (id?.type) {
|
|
340
|
-
case AST_NODE_TYPES.Identifier: return isHookName(id.name);
|
|
341
|
-
case AST_NODE_TYPES.MemberExpression: return "name" in id.property && isHookName(id.property.name);
|
|
342
|
-
default: return false;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Check if the given node is a React Hook call by its name.
|
|
347
|
-
* @param node The node to check.
|
|
348
|
-
* @returns `true` if the node is a React Hook call, `false` otherwise.
|
|
349
|
-
*/
|
|
350
|
-
function isHookCall(node) {
|
|
351
|
-
if (node == null) return false;
|
|
352
|
-
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
353
|
-
if (node.callee.type === AST_NODE_TYPES.Identifier) return isHookName(node.callee.name);
|
|
354
|
-
if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.property.type === AST_NODE_TYPES.Identifier && isHookName(node.callee.property.name);
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Check if a node is a call to a specific React hook.
|
|
359
|
-
* Returns a function that accepts a hook name to check against.
|
|
360
|
-
* @param node The AST node to check
|
|
361
|
-
* @returns A function that takes a hook name and returns boolean
|
|
362
|
-
*/
|
|
363
|
-
function isHookCallWithName(node) {
|
|
364
|
-
if (node == null || node.type !== AST_NODE_TYPES.CallExpression) return constFalse;
|
|
365
|
-
return (name) => {
|
|
366
|
-
switch (node.callee.type) {
|
|
367
|
-
case AST_NODE_TYPES.Identifier: return node.callee.name === name;
|
|
368
|
-
case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === name;
|
|
369
|
-
default: return false;
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Detect useEffect calls and variations (useLayoutEffect, etc.) using a regex pattern
|
|
375
|
-
* @param node The AST node to check
|
|
376
|
-
* @param additionalEffectHooks Regex pattern matching custom hooks that should be treated as effect hooks
|
|
377
|
-
* @returns True if the node is a useEffect-like call
|
|
378
|
-
*/
|
|
379
|
-
function isUseEffectLikeCall(node, additionalEffectHooks = { test: constFalse }) {
|
|
380
|
-
if (node == null) return false;
|
|
381
|
-
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
382
|
-
return [/^use\w*Effect$/u, additionalEffectHooks].some((regexp) => {
|
|
383
|
-
if (node.callee.type === AST_NODE_TYPES.Identifier) return regexp.test(node.callee.name);
|
|
384
|
-
if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.property.type === AST_NODE_TYPES.Identifier && regexp.test(node.callee.property.name);
|
|
385
|
-
return false;
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Detect useState calls and variations (useCustomState, etc.) using a regex pattern
|
|
390
|
-
* @param node The AST node to check
|
|
391
|
-
* @param additionalStateHooks Regex pattern matching custom hooks that should be treated as state hooks
|
|
392
|
-
* @returns True if the node is a useState-like call
|
|
393
|
-
*/
|
|
394
|
-
function isUseStateLikeCall(node, additionalStateHooks = { test: constFalse }) {
|
|
395
|
-
if (node == null) return false;
|
|
396
|
-
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
397
|
-
switch (true) {
|
|
398
|
-
case node.callee.type === AST_NODE_TYPES.Identifier: return node.callee.name === "useState" || additionalStateHooks.test(node.callee.name);
|
|
399
|
-
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier: return ast.getPropertyName(node.callee.property) === "useState" || additionalStateHooks.test(node.callee.property.name);
|
|
400
|
-
}
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
const isUseCall = flip(isHookCallWithName)("use");
|
|
404
|
-
const isUseActionStateCall = flip(isHookCallWithName)("useActionState");
|
|
405
|
-
const isUseCallbackCall = flip(isHookCallWithName)("useCallback");
|
|
406
|
-
const isUseContextCall = flip(isHookCallWithName)("useContext");
|
|
407
|
-
const isUseDebugValueCall = flip(isHookCallWithName)("useDebugValue");
|
|
408
|
-
const isUseDeferredValueCall = flip(isHookCallWithName)("useDeferredValue");
|
|
409
|
-
const isUseEffectCall = flip(isHookCallWithName)("useEffect");
|
|
410
|
-
const isUseFormStatusCall = flip(isHookCallWithName)("useFormStatus");
|
|
411
|
-
const isUseIdCall = flip(isHookCallWithName)("useId");
|
|
412
|
-
const isUseImperativeHandleCall = flip(isHookCallWithName)("useImperativeHandle");
|
|
413
|
-
const isUseInsertionEffectCall = flip(isHookCallWithName)("useInsertionEffect");
|
|
414
|
-
const isUseLayoutEffectCall = flip(isHookCallWithName)("useLayoutEffect");
|
|
415
|
-
const isUseMemoCall = flip(isHookCallWithName)("useMemo");
|
|
416
|
-
const isUseOptimisticCall = flip(isHookCallWithName)("useOptimistic");
|
|
417
|
-
const isUseReducerCall = flip(isHookCallWithName)("useReducer");
|
|
418
|
-
const isUseRefCall = flip(isHookCallWithName)("useRef");
|
|
419
|
-
const isUseStateCall = flip(isHookCallWithName)("useState");
|
|
420
|
-
const isUseSyncExternalStoreCall = flip(isHookCallWithName)("useSyncExternalStore");
|
|
421
|
-
const isUseTransitionCall = flip(isHookCallWithName)("useTransition");
|
|
422
|
-
|
|
423
|
-
//#endregion
|
|
424
|
-
//#region src/hook/hook-callback.ts
|
|
425
|
-
/**
|
|
426
|
-
* Determine if a node is the setup function passed to a useEffect-like hook
|
|
427
|
-
* @param node The AST node to check
|
|
428
|
-
*/
|
|
429
|
-
function isUseEffectSetupCallback(node) {
|
|
430
|
-
if (node == null) return false;
|
|
431
|
-
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.arguments.at(0) === node && isUseEffectLikeCall(node.parent);
|
|
432
|
-
}
|
|
433
167
|
/**
|
|
434
|
-
*
|
|
435
|
-
* @param
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const returnStatement = ast.findParent(node, ast.is(AST_NODE_TYPES.ReturnStatement));
|
|
440
|
-
const enclosingFunction = ast.findParent(node, ast.isFunction);
|
|
441
|
-
if (enclosingFunction !== ast.findParent(returnStatement, ast.isFunction)) return false;
|
|
442
|
-
return isUseEffectSetupCallback(enclosingFunction);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
//#endregion
|
|
446
|
-
//#region src/hook/hook-id.ts
|
|
447
|
-
/**
|
|
448
|
-
* Checks if the given node is a hook identifier
|
|
449
|
-
* @param id The AST node to check
|
|
450
|
-
* @returns `true` if the node is a hook identifier or member expression with hook name, `false` otherwise
|
|
168
|
+
* Check if a variable is initialized from React import
|
|
169
|
+
* @param name The variable name
|
|
170
|
+
* @param initialScope The initial scope
|
|
171
|
+
* @param importSource Alternative import source of React (ex: "preact/compat")
|
|
172
|
+
* @returns True if the variable is initialized or derived from React import
|
|
451
173
|
*/
|
|
452
|
-
function
|
|
453
|
-
|
|
454
|
-
case AST_NODE_TYPES.Identifier: return isHookName(id.name);
|
|
455
|
-
case AST_NODE_TYPES.MemberExpression: return "name" in id.property && isHookName(id.property.name);
|
|
456
|
-
default: return false;
|
|
457
|
-
}
|
|
174
|
+
function isAPIFromReact(name, initialScope, importSource = "react") {
|
|
175
|
+
return name.toLowerCase() === "react" || Boolean(resolveImportSource(name, initialScope)?.startsWith(importSource));
|
|
458
176
|
}
|
|
459
|
-
|
|
460
|
-
//#endregion
|
|
461
|
-
//#region src/hook/hook-collector.ts
|
|
462
|
-
const idGen$2 = new IdGenerator("hook:");
|
|
463
177
|
/**
|
|
464
|
-
*
|
|
465
|
-
* @param
|
|
466
|
-
* @
|
|
178
|
+
* if a variable is initialized from React Native import
|
|
179
|
+
* @param name The variable name
|
|
180
|
+
* @param initialScope The initial scope
|
|
181
|
+
* @param importSource Alternative import source of React Native (ex: "react-native-web")
|
|
182
|
+
* @returns True if the variable is initialized from React Native import
|
|
467
183
|
*/
|
|
468
|
-
function
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
184
|
+
function isAPIFromReactNative(name, initialScope, importSource = "react-native") {
|
|
185
|
+
return [
|
|
186
|
+
"react_native",
|
|
187
|
+
"reactnative",
|
|
188
|
+
"rn"
|
|
189
|
+
].includes(name.toLowerCase()) || Boolean(resolveImportSource(name, initialScope)?.startsWith(importSource));
|
|
190
|
+
}
|
|
191
|
+
const isCaptureOwnerStack = isAPI("captureOwnerStack");
|
|
192
|
+
const isChildrenCount = isAPI("Children.count");
|
|
193
|
+
const isChildrenForEach = isAPI("Children.forEach");
|
|
194
|
+
const isChildrenMap = isAPI("Children.map");
|
|
195
|
+
const isChildrenOnly = isAPI("Children.only");
|
|
196
|
+
const isChildrenToArray = isAPI("Children.toArray");
|
|
197
|
+
const isCloneElement = isAPI("cloneElement");
|
|
198
|
+
const isCreateContext = isAPI("createContext");
|
|
199
|
+
const isCreateElement = isAPI("createElement");
|
|
200
|
+
const isCreateRef = isAPI("createRef");
|
|
201
|
+
const isForwardRef = isAPI("forwardRef");
|
|
202
|
+
const isMemo = isAPI("memo");
|
|
203
|
+
const isLazy = isAPI("lazy");
|
|
204
|
+
const isCaptureOwnerStackCall = isAPICall("captureOwnerStack");
|
|
205
|
+
const isChildrenCountCall = isAPICall("Children.count");
|
|
206
|
+
const isChildrenForEachCall = isAPICall("Children.forEach");
|
|
207
|
+
const isChildrenMapCall = isAPICall("Children.map");
|
|
208
|
+
const isChildrenOnlyCall = isAPICall("Children.only");
|
|
209
|
+
const isChildrenToArrayCall = isAPICall("Children.toArray");
|
|
210
|
+
const isCloneElementCall = isAPICall("cloneElement");
|
|
211
|
+
const isCreateContextCall = isAPICall("createContext");
|
|
212
|
+
const isCreateElementCall = isAPICall("createElement");
|
|
213
|
+
const isCreateRefCall = isAPICall("createRef");
|
|
214
|
+
const isForwardRefCall = isAPICall("forwardRef");
|
|
215
|
+
const isMemoCall = isAPICall("memo");
|
|
216
|
+
const isLazyCall = isAPICall("lazy");
|
|
217
|
+
const isUse = isAPI("use");
|
|
218
|
+
const isUseActionState = isAPI("useActionState");
|
|
219
|
+
const isUseCallback = isAPI("useCallback");
|
|
220
|
+
const isUseContext = isAPI("useContext");
|
|
221
|
+
const isUseDebugValue = isAPI("useDebugValue");
|
|
222
|
+
const isUseDeferredValue = isAPI("useDeferredValue");
|
|
223
|
+
const isUseEffect = isAPI("useEffect");
|
|
224
|
+
const isUseFormStatus = isAPI("useFormStatus");
|
|
225
|
+
const isUseId = isAPI("useId");
|
|
226
|
+
const isUseImperativeHandle = isAPI("useImperativeHandle");
|
|
227
|
+
const isUseInsertionEffect = isAPI("useInsertionEffect");
|
|
228
|
+
const isUseLayoutEffect = isAPI("useLayoutEffect");
|
|
229
|
+
const isUseMemo = isAPI("useMemo");
|
|
230
|
+
const isUseOptimistic = isAPI("useOptimistic");
|
|
231
|
+
const isUseReducer = isAPI("useReducer");
|
|
232
|
+
const isUseRef = isAPI("useRef");
|
|
233
|
+
const isUseState = isAPI("useState");
|
|
234
|
+
const isUseSyncExternalStore = isAPI("useSyncExternalStore");
|
|
235
|
+
const isUseTransition = isAPI("useTransition");
|
|
236
|
+
const isUseCall = isAPICall("use");
|
|
237
|
+
const isUseActionStateCall = isAPICall("useActionState");
|
|
238
|
+
const isUseCallbackCall = isAPICall("useCallback");
|
|
239
|
+
const isUseContextCall = isAPICall("useContext");
|
|
240
|
+
const isUseDebugValueCall = isAPICall("useDebugValue");
|
|
241
|
+
const isUseDeferredValueCall = isAPICall("useDeferredValue");
|
|
242
|
+
const isUseEffectCall = isAPICall("useEffect");
|
|
243
|
+
const isUseFormStatusCall = isAPICall("useFormStatus");
|
|
244
|
+
const isUseIdCall = isAPICall("useId");
|
|
245
|
+
const isUseImperativeHandleCall = isAPICall("useImperativeHandle");
|
|
246
|
+
const isUseInsertionEffectCall = isAPICall("useInsertionEffect");
|
|
247
|
+
const isUseLayoutEffectCall = isAPICall("useLayoutEffect");
|
|
248
|
+
const isUseMemoCall = isAPICall("useMemo");
|
|
249
|
+
const isUseOptimisticCall = isAPICall("useOptimistic");
|
|
250
|
+
const isUseReducerCall = isAPICall("useReducer");
|
|
251
|
+
const isUseRefCall = isAPICall("useRef");
|
|
252
|
+
const isUseStateCall = isAPICall("useState");
|
|
253
|
+
const isUseSyncExternalStoreCall = isAPICall("useSyncExternalStore");
|
|
254
|
+
const isUseTransitionCall = isAPICall("useTransition");
|
|
512
255
|
|
|
513
256
|
//#endregion
|
|
514
|
-
//#region src/component
|
|
257
|
+
//#region src/class-component.ts
|
|
515
258
|
function isClassComponent(node, context) {
|
|
516
259
|
if ("superClass" in node && node.superClass != null) {
|
|
517
260
|
const re = /^(?:Pure)?Component$/u;
|
|
@@ -519,20 +262,29 @@ function isClassComponent(node, context) {
|
|
|
519
262
|
case node.superClass.type === AST_NODE_TYPES.Identifier:
|
|
520
263
|
if (!re.test(node.superClass.name)) return false;
|
|
521
264
|
if (context == null) return true;
|
|
522
|
-
return
|
|
265
|
+
return isAPIFromReact$1(node.superClass.name, context.sourceCode.getScope(node), "react");
|
|
523
266
|
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier:
|
|
524
267
|
if (!re.test(node.superClass.property.name)) return false;
|
|
525
268
|
if (context == null) return true;
|
|
526
|
-
if (node.superClass.object.type === AST_NODE_TYPES.Identifier) return
|
|
269
|
+
if (node.superClass.object.type === AST_NODE_TYPES.Identifier) return isAPIFromReact$1(node.superClass.object.name, context.sourceCode.getScope(node), "react");
|
|
527
270
|
return true;
|
|
528
271
|
}
|
|
529
272
|
}
|
|
530
273
|
return false;
|
|
531
274
|
}
|
|
275
|
+
function isClassComponentLoose(node) {
|
|
276
|
+
if ("superClass" in node && node.superClass != null) {
|
|
277
|
+
const re = /^(?:Pure)?Component$/u;
|
|
278
|
+
switch (true) {
|
|
279
|
+
case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
|
|
280
|
+
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
532
285
|
/**
|
|
533
|
-
*
|
|
534
|
-
* @
|
|
535
|
-
* @returns `true` if the node is a PureComponent, `false` otherwise
|
|
286
|
+
* @param node The AST node to check.
|
|
287
|
+
* @deprecated Class components are legacy. This function exists only to support legacy rules.
|
|
536
288
|
*/
|
|
537
289
|
function isPureComponent(node) {
|
|
538
290
|
if ("superClass" in node && node.superClass != null) {
|
|
@@ -544,109 +296,144 @@ function isPureComponent(node) {
|
|
|
544
296
|
}
|
|
545
297
|
return false;
|
|
546
298
|
}
|
|
547
|
-
/**
|
|
548
|
-
* Create a lifecycle method checker function
|
|
549
|
-
* @param methodName The lifecycle method name
|
|
550
|
-
* @param isStatic Whether the method is static
|
|
551
|
-
*/
|
|
552
299
|
function createLifecycleChecker(methodName, isStatic = false) {
|
|
553
300
|
return (node) => ast.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
|
|
554
301
|
}
|
|
302
|
+
/** @deprecated Class components are legacy. */
|
|
555
303
|
const isRender = createLifecycleChecker("render");
|
|
304
|
+
/** @deprecated Class components are legacy. */
|
|
556
305
|
const isComponentDidCatch = createLifecycleChecker("componentDidCatch");
|
|
306
|
+
/** @deprecated Class components are legacy. */
|
|
557
307
|
const isComponentDidMount = createLifecycleChecker("componentDidMount");
|
|
308
|
+
/** @deprecated Class components are legacy. */
|
|
558
309
|
const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate");
|
|
310
|
+
/** @deprecated Class components are legacy. */
|
|
559
311
|
const isComponentWillMount = createLifecycleChecker("componentWillMount");
|
|
312
|
+
/** @deprecated Class components are legacy. */
|
|
560
313
|
const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps");
|
|
314
|
+
/** @deprecated Class components are legacy. */
|
|
561
315
|
const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount");
|
|
316
|
+
/** @deprecated Class components are legacy. */
|
|
562
317
|
const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate");
|
|
318
|
+
/** @deprecated Class components are legacy. */
|
|
563
319
|
const isGetChildContext = createLifecycleChecker("getChildContext");
|
|
320
|
+
/** @deprecated Class components are legacy. */
|
|
564
321
|
const isGetInitialState = createLifecycleChecker("getInitialState");
|
|
322
|
+
/** @deprecated Class components are legacy. */
|
|
565
323
|
const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate");
|
|
324
|
+
/** @deprecated Class components are legacy. */
|
|
566
325
|
const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate");
|
|
326
|
+
/** @deprecated Class components are legacy. */
|
|
567
327
|
const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount");
|
|
328
|
+
/** @deprecated Class components are legacy. */
|
|
568
329
|
const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps");
|
|
330
|
+
/** @deprecated Class components are legacy. */
|
|
569
331
|
const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate");
|
|
332
|
+
/** @deprecated Class components are legacy. */
|
|
570
333
|
const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
|
|
334
|
+
/** @deprecated Class components are legacy. */
|
|
571
335
|
const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
|
|
336
|
+
/** @deprecated Class components are legacy. */
|
|
572
337
|
const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
|
|
573
338
|
/**
|
|
574
|
-
*
|
|
575
|
-
* @
|
|
576
|
-
* @returns True if the node is a componentDidMount callback, false otherwise
|
|
339
|
+
* @param node The AST node to check.
|
|
340
|
+
* @deprecated Class components are legacy. This function exists only to support legacy rules.
|
|
577
341
|
*/
|
|
578
|
-
function
|
|
579
|
-
return ast.
|
|
342
|
+
function isRenderMethodLike(node) {
|
|
343
|
+
return ast.isMethodOrProperty(node) && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render") && ast.isOneOf([AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.ClassExpression])(node.parent.parent);
|
|
344
|
+
}
|
|
345
|
+
function isRenderMethodCallback(node) {
|
|
346
|
+
const parent = node.parent;
|
|
347
|
+
const greatGrandparent = parent.parent?.parent;
|
|
348
|
+
return greatGrandparent != null && isRenderMethodLike(parent) && isClassComponentLoose(greatGrandparent);
|
|
580
349
|
}
|
|
581
350
|
/**
|
|
582
|
-
*
|
|
583
|
-
* @
|
|
584
|
-
* @returns True if the node is a componentWillUnmount callback, false otherwise
|
|
351
|
+
* @param node The call expression node to check.
|
|
352
|
+
* @deprecated Class components are legacy. This function exists only to support legacy rules.
|
|
585
353
|
*/
|
|
586
|
-
function
|
|
587
|
-
|
|
354
|
+
function isThisSetStateCall(node) {
|
|
355
|
+
const { callee } = node;
|
|
356
|
+
return callee.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(callee.object) && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "setState";
|
|
588
357
|
}
|
|
589
358
|
/**
|
|
590
|
-
*
|
|
591
|
-
* @
|
|
592
|
-
* ```tsx
|
|
593
|
-
* class Component extends React.Component {
|
|
594
|
-
* renderHeader = () => <div />;
|
|
595
|
-
* renderFooter = () => <div />;
|
|
596
|
-
* }
|
|
597
|
-
* ```
|
|
598
|
-
* @param node The AST node to check
|
|
599
|
-
* @returns `true` if node is a render function, `false` if not
|
|
359
|
+
* @param node The assignment expression node to check.
|
|
360
|
+
* @deprecated Class components are legacy. This function exists only to support legacy rules.
|
|
600
361
|
*/
|
|
601
|
-
function
|
|
602
|
-
|
|
362
|
+
function isAssignmentToThisState(node) {
|
|
363
|
+
const { left } = node;
|
|
364
|
+
return left.type === AST_NODE_TYPES.MemberExpression && ast.isThisExpressionLoose(left.object) && ast.getPropertyName(left.property) === "state";
|
|
603
365
|
}
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/class-component-collector.ts
|
|
369
|
+
const idGen$2 = new IdGenerator("class-component:");
|
|
604
370
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
* @param node The AST node to check
|
|
608
|
-
* @returns `true` if the node is a render function inside a class component
|
|
609
|
-
*
|
|
610
|
-
* @example
|
|
611
|
-
* ```tsx
|
|
612
|
-
* class Component extends React.Component {
|
|
613
|
-
* renderHeader = () => <div />; // Returns true
|
|
614
|
-
* }
|
|
615
|
-
* ```
|
|
371
|
+
* @param context The rule context.
|
|
372
|
+
* @deprecated Class components are legacy. This function exists only to support legacy rules.
|
|
616
373
|
*/
|
|
617
|
-
function
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
-
|
|
374
|
+
function getClassComponentCollector(context) {
|
|
375
|
+
const components = /* @__PURE__ */ new Map();
|
|
376
|
+
const api = { getAllComponents(node) {
|
|
377
|
+
return [...components.values()];
|
|
378
|
+
} };
|
|
379
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
380
|
+
const collect = (node) => {
|
|
381
|
+
if (!isClassComponent(node)) return;
|
|
382
|
+
const id = ast.getClassId(node);
|
|
383
|
+
const key = idGen$2.next();
|
|
384
|
+
const name = id == null ? null : ast.getFullyQualifiedName(id, getText);
|
|
385
|
+
components.set(key, {
|
|
386
|
+
id,
|
|
387
|
+
key,
|
|
388
|
+
kind: "class-component",
|
|
389
|
+
name,
|
|
390
|
+
displayName: null,
|
|
391
|
+
flag: 0n,
|
|
392
|
+
hint: 0n,
|
|
393
|
+
methods: [],
|
|
394
|
+
node
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
api,
|
|
399
|
+
visitor: {
|
|
400
|
+
ClassDeclaration: collect,
|
|
401
|
+
ClassExpression: collect
|
|
402
|
+
}
|
|
403
|
+
};
|
|
621
404
|
}
|
|
405
|
+
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/function-component.ts
|
|
622
408
|
/**
|
|
623
|
-
*
|
|
624
|
-
* @param node The node to check
|
|
625
|
-
* @internal
|
|
409
|
+
* Component flag constants
|
|
626
410
|
*/
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
411
|
+
const FunctionComponentFlag = {
|
|
412
|
+
None: 0n,
|
|
413
|
+
PureComponent: 1n << 0n,
|
|
414
|
+
CreateElement: 1n << 1n,
|
|
415
|
+
Memo: 1n << 2n,
|
|
416
|
+
ForwardRef: 1n << 3n
|
|
417
|
+
};
|
|
631
418
|
/**
|
|
632
|
-
*
|
|
633
|
-
* @param
|
|
419
|
+
* Get component flag from init path
|
|
420
|
+
* @param initPath The init path of the function component
|
|
421
|
+
* @returns The component flag
|
|
634
422
|
* @internal
|
|
635
423
|
*/
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
|
|
424
|
+
function getFunctionComponentFlagFromInitPath(initPath) {
|
|
425
|
+
let flag = FunctionComponentFlag.None;
|
|
426
|
+
if (initPath != null && ast.hasCallInFunctionInitPath("memo", initPath)) flag |= FunctionComponentFlag.Memo;
|
|
427
|
+
if (initPath != null && ast.hasCallInFunctionInitPath("forwardRef", initPath)) flag |= FunctionComponentFlag.ForwardRef;
|
|
428
|
+
return flag;
|
|
639
429
|
}
|
|
640
|
-
|
|
641
|
-
//#endregion
|
|
642
|
-
//#region src/component/component-wrapper.ts
|
|
643
430
|
/**
|
|
644
431
|
* Check if the node is a call expression for a component wrapper
|
|
645
432
|
* @param context The ESLint rule context
|
|
646
433
|
* @param node The node to check
|
|
647
434
|
* @returns `true` if the node is a call expression for a component wrapper
|
|
648
435
|
*/
|
|
649
|
-
function
|
|
436
|
+
function isFunctionComponentWrapperCall(context, node) {
|
|
650
437
|
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
651
438
|
return isMemoCall(context, node) || isForwardRefCall(context, node);
|
|
652
439
|
}
|
|
@@ -656,75 +443,40 @@ function isComponentWrapperCall(context, node) {
|
|
|
656
443
|
* @param node The node to check
|
|
657
444
|
* @returns `true` if the node is a callback function passed to a component wrapper
|
|
658
445
|
*/
|
|
659
|
-
|
|
660
|
-
* Check if the node is a call expression for a component wrapper loosely
|
|
661
|
-
* @param context The ESLint rule context
|
|
662
|
-
* @param node The node to check
|
|
663
|
-
* @returns `true` if the node is a call expression for a component wrapper loosely
|
|
664
|
-
*/
|
|
665
|
-
function isComponentWrapperCallLoose(context, node) {
|
|
666
|
-
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
667
|
-
return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Check if the node is a callback function passed to a component wrapper
|
|
671
|
-
* @param context The ESLint rule context
|
|
672
|
-
* @param node The node to check
|
|
673
|
-
* @returns `true` if the node is a callback function passed to a component wrapper
|
|
674
|
-
*/
|
|
675
|
-
function isComponentWrapperCallback(context, node) {
|
|
676
|
-
if (!ast.isFunction(node)) return false;
|
|
677
|
-
let parent = node.parent;
|
|
678
|
-
while (ast.isTypeExpression(parent)) parent = parent.parent;
|
|
679
|
-
if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
680
|
-
return isComponentWrapperCall(context, parent);
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Check if the node is a callback function passed to a component wrapper loosely
|
|
684
|
-
* @param context The ESLint rule context
|
|
685
|
-
* @param node The node to check
|
|
686
|
-
* @returns `true` if the node is a callback function passed to a component wrapper loosely
|
|
687
|
-
*/
|
|
688
|
-
function isComponentWrapperCallbackLoose(context, node) {
|
|
446
|
+
function isFunctionComponentWrapperCallback(context, node) {
|
|
689
447
|
if (!ast.isFunction(node)) return false;
|
|
690
448
|
let parent = node.parent;
|
|
691
449
|
while (ast.isTypeExpression(parent)) parent = parent.parent;
|
|
692
450
|
if (parent.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
693
|
-
return
|
|
451
|
+
return isFunctionComponentWrapperCall(context, parent);
|
|
694
452
|
}
|
|
695
|
-
|
|
696
|
-
//#endregion
|
|
697
|
-
//#region src/component/component-id.ts
|
|
698
453
|
/**
|
|
699
454
|
* Get function component identifier from `const Component = memo(() => {});`
|
|
700
455
|
* @param context The rule context
|
|
701
|
-
* @param node The
|
|
702
|
-
* @
|
|
456
|
+
* @param node The AST node to get the function component identifier from
|
|
457
|
+
* @internal
|
|
703
458
|
*/
|
|
704
459
|
function getFunctionComponentId(context, node) {
|
|
705
460
|
const functionId = ast.getFunctionId(node);
|
|
706
461
|
if (functionId != null) return functionId;
|
|
707
462
|
let parent = node.parent;
|
|
708
463
|
while (ast.isTypeExpression(parent)) parent = parent.parent;
|
|
709
|
-
if (parent.type === AST_NODE_TYPES.CallExpression &&
|
|
710
|
-
if (parent.type === AST_NODE_TYPES.CallExpression &&
|
|
464
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && isFunctionComponentWrapperCall(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.id;
|
|
465
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && isFunctionComponentWrapperCall(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isFunctionComponentWrapperCall(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator) return parent.parent.parent.id;
|
|
711
466
|
return null;
|
|
712
467
|
}
|
|
713
|
-
|
|
714
|
-
//#endregion
|
|
715
|
-
//#region src/component/component-name.ts
|
|
716
468
|
/**
|
|
717
469
|
* Check if a string matches the strict component name pattern
|
|
718
470
|
* @param name The name to check
|
|
719
471
|
*/
|
|
720
|
-
function
|
|
472
|
+
function isFunctionComponentName(name) {
|
|
721
473
|
return RE_COMPONENT_NAME.test(name);
|
|
722
474
|
}
|
|
723
475
|
/**
|
|
724
476
|
* Check if a string matches the loose component name pattern
|
|
725
477
|
* @param name The name to check
|
|
726
478
|
*/
|
|
727
|
-
function
|
|
479
|
+
function isFunctionComponentNameLoose(name) {
|
|
728
480
|
return RE_COMPONENT_NAME_LOOSE.test(name);
|
|
729
481
|
}
|
|
730
482
|
/**
|
|
@@ -737,17 +489,14 @@ function isComponentNameLoose(name) {
|
|
|
737
489
|
function isFunctionWithLooseComponentName(context, fn, allowNone = false) {
|
|
738
490
|
const id = getFunctionComponentId(context, fn);
|
|
739
491
|
if (id == null) return allowNone;
|
|
740
|
-
if (id.type === AST_NODE_TYPES.Identifier) return
|
|
741
|
-
if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return
|
|
492
|
+
if (id.type === AST_NODE_TYPES.Identifier) return isFunctionComponentNameLoose(id.name);
|
|
493
|
+
if (id.type === AST_NODE_TYPES.MemberExpression && id.property.type === AST_NODE_TYPES.Identifier) return isFunctionComponentNameLoose(id.property.name);
|
|
742
494
|
return false;
|
|
743
495
|
}
|
|
744
|
-
|
|
745
|
-
//#endregion
|
|
746
|
-
//#region src/component/component-detection.ts
|
|
747
496
|
/**
|
|
748
497
|
* Hints for component collector
|
|
749
498
|
*/
|
|
750
|
-
const
|
|
499
|
+
const FunctionComponentDetectionHint = {
|
|
751
500
|
...JsxDetectionHint,
|
|
752
501
|
DoNotIncludeFunctionDefinedAsClassMethod: 1n << 11n,
|
|
753
502
|
DoNotIncludeFunctionDefinedAsClassProperty: 1n << 12n,
|
|
@@ -761,7 +510,7 @@ const ComponentDetectionHint = {
|
|
|
761
510
|
/**
|
|
762
511
|
* Default component detection hint
|
|
763
512
|
*/
|
|
764
|
-
const DEFAULT_COMPONENT_DETECTION_HINT = 0n |
|
|
513
|
+
const DEFAULT_COMPONENT_DETECTION_HINT = 0n | FunctionComponentDetectionHint.DoNotIncludeJsxWithBigIntValue | FunctionComponentDetectionHint.DoNotIncludeJsxWithBooleanValue | FunctionComponentDetectionHint.DoNotIncludeJsxWithNumberValue | FunctionComponentDetectionHint.DoNotIncludeJsxWithStringValue | FunctionComponentDetectionHint.DoNotIncludeJsxWithUndefinedValue | FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArbitraryCallExpressionCallback | FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayExpressionElement | FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback | FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback | FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayPatternElement | FunctionComponentDetectionHint.RequireAllArrayElementsToBeJsx | FunctionComponentDetectionHint.RequireBothBranchesOfConditionalExpressionToBeJsx | FunctionComponentDetectionHint.RequireBothSidesOfLogicalExpressionToBeJsx;
|
|
765
514
|
/**
|
|
766
515
|
* Determine if a function node represents a valid React component definition
|
|
767
516
|
*
|
|
@@ -770,7 +519,7 @@ const DEFAULT_COMPONENT_DETECTION_HINT = 0n | ComponentDetectionHint.DoNotInclud
|
|
|
770
519
|
* @param hint Component detection hints (bit flags) to customize detection logic
|
|
771
520
|
* @returns `true` if the node is considered a component definition
|
|
772
521
|
*/
|
|
773
|
-
function
|
|
522
|
+
function isFunctionComponentDefinition(context, node, hint) {
|
|
774
523
|
if (!isFunctionWithLooseComponentName(context, node, true)) return false;
|
|
775
524
|
switch (true) {
|
|
776
525
|
case node.parent.type === AST_NODE_TYPES.CallExpression && isCreateElementCall(context, node.parent) && node.parent.arguments.slice(2).some((arg) => arg === node): return false;
|
|
@@ -780,28 +529,28 @@ function isComponentDefinition(context, node, hint) {
|
|
|
780
529
|
while (ast.isTypeExpression(parent)) parent = parent.parent;
|
|
781
530
|
switch (true) {
|
|
782
531
|
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && parent.type === AST_NODE_TYPES.Property && parent.parent.type === AST_NODE_TYPES.ObjectExpression:
|
|
783
|
-
if (hint &
|
|
532
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsObjectMethod) return false;
|
|
784
533
|
break;
|
|
785
534
|
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && parent.type === AST_NODE_TYPES.MethodDefinition:
|
|
786
|
-
if (hint &
|
|
535
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsClassMethod) return false;
|
|
787
536
|
break;
|
|
788
537
|
case ast.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(node) && parent.type === AST_NODE_TYPES.Property:
|
|
789
|
-
if (hint &
|
|
538
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsClassProperty) return false;
|
|
790
539
|
break;
|
|
791
540
|
case parent.type === AST_NODE_TYPES.ArrayPattern:
|
|
792
|
-
if (hint &
|
|
541
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayPatternElement) return false;
|
|
793
542
|
break;
|
|
794
543
|
case parent.type === AST_NODE_TYPES.ArrayExpression:
|
|
795
|
-
if (hint &
|
|
544
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayExpressionElement) return false;
|
|
796
545
|
break;
|
|
797
546
|
case parent.type === AST_NODE_TYPES.CallExpression && parent.callee.type === AST_NODE_TYPES.MemberExpression && parent.callee.property.type === AST_NODE_TYPES.Identifier && parent.callee.property.name === "map":
|
|
798
|
-
if (hint &
|
|
547
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayMapCallback) return false;
|
|
799
548
|
break;
|
|
800
549
|
case parent.type === AST_NODE_TYPES.CallExpression && parent.callee.type === AST_NODE_TYPES.MemberExpression && parent.callee.property.type === AST_NODE_TYPES.Identifier && parent.callee.property.name === "flatMap":
|
|
801
|
-
if (hint &
|
|
550
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArrayFlatMapCallback) return false;
|
|
802
551
|
break;
|
|
803
|
-
case parent.type === AST_NODE_TYPES.CallExpression && ast.getFunctionId(node) == null && !
|
|
804
|
-
if (hint &
|
|
552
|
+
case parent.type === AST_NODE_TYPES.CallExpression && ast.getFunctionId(node) == null && !isFunctionComponentWrapperCall(context, parent) && !isCreateElementCall(context, parent):
|
|
553
|
+
if (hint & FunctionComponentDetectionHint.DoNotIncludeFunctionDefinedAsArbitraryCallExpressionCallback) return false;
|
|
805
554
|
break;
|
|
806
555
|
}
|
|
807
556
|
const significantParent = ast.findParent(node, ast.isOneOf([
|
|
@@ -817,31 +566,127 @@ function isComponentDefinition(context, node, hint) {
|
|
|
817
566
|
}
|
|
818
567
|
|
|
819
568
|
//#endregion
|
|
820
|
-
//#region src/
|
|
569
|
+
//#region src/hook.ts
|
|
570
|
+
const REACT_BUILTIN_HOOK_NAMES = [
|
|
571
|
+
"use",
|
|
572
|
+
"useActionState",
|
|
573
|
+
"useCallback",
|
|
574
|
+
"useContext",
|
|
575
|
+
"useDebugValue",
|
|
576
|
+
"useDeferredValue",
|
|
577
|
+
"useEffect",
|
|
578
|
+
"useFormStatus",
|
|
579
|
+
"useId",
|
|
580
|
+
"useImperativeHandle",
|
|
581
|
+
"useInsertionEffect",
|
|
582
|
+
"useLayoutEffect",
|
|
583
|
+
"useMemo",
|
|
584
|
+
"useOptimistic",
|
|
585
|
+
"useReducer",
|
|
586
|
+
"useRef",
|
|
587
|
+
"useState",
|
|
588
|
+
"useSyncExternalStore",
|
|
589
|
+
"useTransition"
|
|
590
|
+
];
|
|
821
591
|
/**
|
|
822
|
-
*
|
|
592
|
+
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
|
593
|
+
* character to exclude identifiers like "user".
|
|
594
|
+
* @param name The name of the identifier to check.
|
|
595
|
+
* @see https://github.com/facebook/react/blob/1d6c8168db1d82713202e842df3167787ffa00ed/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts#L16
|
|
823
596
|
*/
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
CreateElement: 1n << 1n,
|
|
828
|
-
Memo: 1n << 2n,
|
|
829
|
-
ForwardRef: 1n << 3n
|
|
830
|
-
};
|
|
597
|
+
function isHookName(name) {
|
|
598
|
+
return name === "use" || /^use[A-Z0-9]/.test(name);
|
|
599
|
+
}
|
|
831
600
|
/**
|
|
832
|
-
*
|
|
833
|
-
* @param
|
|
834
|
-
* @returns
|
|
601
|
+
* Checks if the given node is a hook identifier
|
|
602
|
+
* @param id The AST node to check
|
|
603
|
+
* @returns `true` if the node is a hook identifier or member expression with hook name, `false` otherwise
|
|
835
604
|
*/
|
|
836
|
-
function
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
605
|
+
function isHookId(id) {
|
|
606
|
+
switch (id.type) {
|
|
607
|
+
case AST_NODE_TYPES.Identifier: return isHookName(id.name);
|
|
608
|
+
case AST_NODE_TYPES.MemberExpression: return "name" in id.property && isHookName(id.property.name);
|
|
609
|
+
default: return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Determine if a function node is a React Hook based on its name.
|
|
614
|
+
* @param node The function node to check
|
|
615
|
+
* @returns True if the function is a React Hook, false otherwise
|
|
616
|
+
*/
|
|
617
|
+
function isHookDefinition(node) {
|
|
618
|
+
if (node == null) return false;
|
|
619
|
+
const id = ast.getFunctionId(node);
|
|
620
|
+
switch (id?.type) {
|
|
621
|
+
case AST_NODE_TYPES.Identifier: return isHookName(id.name);
|
|
622
|
+
case AST_NODE_TYPES.MemberExpression: return "name" in id.property && isHookName(id.property.name);
|
|
623
|
+
default: return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Check if the given node is a React Hook call by its name.
|
|
628
|
+
* @param node The node to check.
|
|
629
|
+
* @returns `true` if the node is a React Hook call, `false` otherwise.
|
|
630
|
+
*/
|
|
631
|
+
function isHookCall(node) {
|
|
632
|
+
if (node == null) return false;
|
|
633
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
634
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) return isHookName(node.callee.name);
|
|
635
|
+
if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.property.type === AST_NODE_TYPES.Identifier && isHookName(node.callee.property.name);
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Detect useEffect calls and variations (useLayoutEffect, etc.) using a regex pattern
|
|
640
|
+
* @param node The AST node to check
|
|
641
|
+
* @param additionalEffectHooks Regex pattern matching custom hooks that should be treated as effect hooks
|
|
642
|
+
* @returns True if the node is a useEffect-like call
|
|
643
|
+
*/
|
|
644
|
+
function isUseEffectLikeCall(node, additionalEffectHooks = { test: constFalse }) {
|
|
645
|
+
if (node == null) return false;
|
|
646
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
647
|
+
return [/^use\w*Effect$/u, additionalEffectHooks].some((regexp) => {
|
|
648
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) return regexp.test(node.callee.name);
|
|
649
|
+
if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.property.type === AST_NODE_TYPES.Identifier && regexp.test(node.callee.property.name);
|
|
650
|
+
return false;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Detect useState calls and variations using a regex pattern
|
|
655
|
+
* @param node The AST node to check
|
|
656
|
+
* @param additionalStateHooks Regex pattern matching custom hooks that should be treated as state hooks
|
|
657
|
+
* @returns True if the node is a useState-like call
|
|
658
|
+
*/
|
|
659
|
+
function isUseStateLikeCall(node, additionalStateHooks = { test: constFalse }) {
|
|
660
|
+
if (node == null) return false;
|
|
661
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
662
|
+
switch (true) {
|
|
663
|
+
case node.callee.type === AST_NODE_TYPES.Identifier: return node.callee.name === "useState" || additionalStateHooks.test(node.callee.name);
|
|
664
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier: return ast.getPropertyName(node.callee.property) === "useState" || additionalStateHooks.test(node.callee.property.name);
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Determine if a node is the setup function passed to a useEffect-like hook
|
|
670
|
+
* @param node The AST node to check
|
|
671
|
+
*/
|
|
672
|
+
function isUseEffectSetupCallback(node) {
|
|
673
|
+
if (node == null) return false;
|
|
674
|
+
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.arguments.at(0) === node && isUseEffectLikeCall(node.parent);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Determine if a node is the cleanup function returned by a useEffect-like hook's setup function
|
|
678
|
+
* @param node The AST node to check
|
|
679
|
+
*/
|
|
680
|
+
function isUseEffectCleanupCallback(node) {
|
|
681
|
+
if (node == null) return false;
|
|
682
|
+
const returnStatement = ast.findParent(node, ast.is(AST_NODE_TYPES.ReturnStatement));
|
|
683
|
+
const enclosingFunction = ast.findParent(node, ast.isFunction);
|
|
684
|
+
if (enclosingFunction !== ast.findParent(returnStatement, ast.isFunction)) return false;
|
|
685
|
+
return isUseEffectSetupCallback(enclosingFunction);
|
|
841
686
|
}
|
|
842
687
|
|
|
843
688
|
//#endregion
|
|
844
|
-
//#region src/component
|
|
689
|
+
//#region src/function-component-collector.ts
|
|
845
690
|
const idGen$1 = new IdGenerator("component:");
|
|
846
691
|
/**
|
|
847
692
|
* Get an api and visitor object for the rule to collect function components
|
|
@@ -849,7 +694,7 @@ const idGen$1 = new IdGenerator("component:");
|
|
|
849
694
|
* @param options The options to use
|
|
850
695
|
* @returns The api and visitor of the collector
|
|
851
696
|
*/
|
|
852
|
-
function
|
|
697
|
+
function getFunctionComponentCollector(context, options = {}) {
|
|
853
698
|
const { collectDisplayName = false, hint = DEFAULT_COMPONENT_DETECTION_HINT } = options;
|
|
854
699
|
const functionEntries = [];
|
|
855
700
|
const components = /* @__PURE__ */ new Map();
|
|
@@ -871,18 +716,18 @@ function getComponentCollector(context, options = {}) {
|
|
|
871
716
|
name,
|
|
872
717
|
directives,
|
|
873
718
|
displayName: null,
|
|
874
|
-
flag:
|
|
719
|
+
flag: getFunctionComponentFlagFromInitPath(initPath),
|
|
875
720
|
hint,
|
|
876
721
|
hookCalls: [],
|
|
877
722
|
initPath,
|
|
878
|
-
|
|
723
|
+
isFunctionComponentDefinition: isFunctionComponentDefinition(context, node, hint),
|
|
879
724
|
isExportDefault,
|
|
880
725
|
isExportDefaultDeclaration,
|
|
881
726
|
node,
|
|
882
727
|
rets: []
|
|
883
728
|
};
|
|
884
729
|
functionEntries.push(entry);
|
|
885
|
-
if (!entry.
|
|
730
|
+
if (!entry.isFunctionComponentDefinition || !isFunctionWithLooseComponentName(context, node, false)) return;
|
|
886
731
|
if (directives.some((d) => d.directive === "use memo" || d.directive === "use no memo")) components.set(entry.key, entry);
|
|
887
732
|
};
|
|
888
733
|
const onFunctionExit = () => {
|
|
@@ -901,7 +746,7 @@ function getComponentCollector(context, options = {}) {
|
|
|
901
746
|
const { body } = entry.node;
|
|
902
747
|
if (body.type === AST_NODE_TYPES.BlockStatement) return;
|
|
903
748
|
entry.rets.push(body);
|
|
904
|
-
if (!entry.
|
|
749
|
+
if (!entry.isFunctionComponentDefinition) return;
|
|
905
750
|
if (!components.has(entry.key) && !isJsxLike(context, body, hint)) return;
|
|
906
751
|
components.set(entry.key, entry);
|
|
907
752
|
},
|
|
@@ -918,14 +763,14 @@ function getComponentCollector(context, options = {}) {
|
|
|
918
763
|
const entry = getCurrentEntry();
|
|
919
764
|
if (entry == null) return;
|
|
920
765
|
entry.hookCalls.push(node);
|
|
921
|
-
if (!entry.
|
|
766
|
+
if (!entry.isFunctionComponentDefinition) return;
|
|
922
767
|
components.set(entry.key, entry);
|
|
923
768
|
},
|
|
924
769
|
ReturnStatement(node) {
|
|
925
770
|
const entry = getCurrentEntry();
|
|
926
771
|
if (entry == null) return;
|
|
927
772
|
entry.rets.push(node.argument);
|
|
928
|
-
if (!entry.
|
|
773
|
+
if (!entry.isFunctionComponentDefinition) return;
|
|
929
774
|
const { argument } = node;
|
|
930
775
|
if (!components.has(entry.key) && !isJsxLike(context, argument, hint)) return;
|
|
931
776
|
components.set(entry.key, entry);
|
|
@@ -935,45 +780,215 @@ function getComponentCollector(context, options = {}) {
|
|
|
935
780
|
}
|
|
936
781
|
|
|
937
782
|
//#endregion
|
|
938
|
-
//#region src/
|
|
939
|
-
const idGen = new IdGenerator("
|
|
783
|
+
//#region src/hook-collector.ts
|
|
784
|
+
const idGen = new IdGenerator("hook:");
|
|
940
785
|
/**
|
|
941
|
-
* Get an api and visitor object for the rule to collect
|
|
786
|
+
* Get an api and visitor object for the rule to collect hooks
|
|
942
787
|
* @param context The ESLint rule context
|
|
943
788
|
* @returns The api and visitor of the collector
|
|
944
789
|
*/
|
|
945
|
-
function
|
|
946
|
-
const
|
|
947
|
-
const
|
|
948
|
-
return [...components.values()];
|
|
949
|
-
} };
|
|
790
|
+
function getHookCollector(context) {
|
|
791
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
792
|
+
const functionEntries = [];
|
|
950
793
|
const getText = (n) => context.sourceCode.getText(n);
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
const id = ast.
|
|
794
|
+
const getCurrentEntry = () => functionEntries.at(-1) ?? null;
|
|
795
|
+
const onFunctionEnter = (node) => {
|
|
796
|
+
const id = ast.getFunctionId(node);
|
|
954
797
|
const key = idGen.next();
|
|
955
|
-
const
|
|
956
|
-
const flag = isPureComponent(node) ? ComponentFlag.PureComponent : ComponentFlag.None;
|
|
957
|
-
components.set(key, {
|
|
798
|
+
const entry = {
|
|
958
799
|
id,
|
|
959
800
|
key,
|
|
960
|
-
kind: "
|
|
961
|
-
name,
|
|
962
|
-
|
|
963
|
-
flag,
|
|
801
|
+
kind: "hook",
|
|
802
|
+
name: id == null ? null : ast.getFullyQualifiedName(id, getText),
|
|
803
|
+
directives: [],
|
|
804
|
+
flag: 0n,
|
|
964
805
|
hint: 0n,
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
806
|
+
hookCalls: [],
|
|
807
|
+
isHookDefinition: id != null && isHookId(id),
|
|
808
|
+
node,
|
|
809
|
+
rets: []
|
|
810
|
+
};
|
|
811
|
+
functionEntries.push(entry);
|
|
812
|
+
if (!entry.isHookDefinition) return;
|
|
813
|
+
hooks.set(key, entry);
|
|
814
|
+
};
|
|
815
|
+
const onFunctionExit = () => {
|
|
816
|
+
functionEntries.pop();
|
|
968
817
|
};
|
|
969
818
|
return {
|
|
970
|
-
api
|
|
819
|
+
api: { getAllHooks(node) {
|
|
820
|
+
return [...hooks.values()];
|
|
821
|
+
} },
|
|
971
822
|
visitor: {
|
|
972
|
-
|
|
973
|
-
|
|
823
|
+
":function": onFunctionEnter,
|
|
824
|
+
":function:exit": onFunctionExit,
|
|
825
|
+
"ArrowFunctionExpression[body.type!='BlockStatement']"() {
|
|
826
|
+
const entry = getCurrentEntry();
|
|
827
|
+
if (entry == null) return;
|
|
828
|
+
const { body } = entry.node;
|
|
829
|
+
if (body.type === AST_NODE_TYPES$1.BlockStatement) return;
|
|
830
|
+
entry.rets.push(body);
|
|
831
|
+
},
|
|
832
|
+
CallExpression(node) {
|
|
833
|
+
if (!isHookCall(node)) return;
|
|
834
|
+
const entry = getCurrentEntry();
|
|
835
|
+
if (entry == null) return;
|
|
836
|
+
entry.hookCalls.push(node);
|
|
837
|
+
},
|
|
838
|
+
ReturnStatement(node) {
|
|
839
|
+
const entry = getCurrentEntry();
|
|
840
|
+
if (entry == null) return;
|
|
841
|
+
entry.rets.push(node.argument);
|
|
842
|
+
}
|
|
974
843
|
}
|
|
975
844
|
};
|
|
976
845
|
}
|
|
977
846
|
|
|
978
847
|
//#endregion
|
|
979
|
-
|
|
848
|
+
//#region src/type-is.ts
|
|
849
|
+
function isFlagSet(allFlags, flag) {
|
|
850
|
+
return (allFlags & flag) !== 0;
|
|
851
|
+
}
|
|
852
|
+
function isFlagSetOnObject(obj, flag) {
|
|
853
|
+
return isFlagSet(obj.flags, flag);
|
|
854
|
+
}
|
|
855
|
+
const isTypeFlagSet = isFlagSetOnObject;
|
|
856
|
+
function isBooleanLiteralType(type) {
|
|
857
|
+
return isTypeFlagSet(type, ts.TypeFlags.BooleanLiteral);
|
|
858
|
+
}
|
|
859
|
+
/** @internal */
|
|
860
|
+
const isFalseLiteralType = (type) => isBooleanLiteralType(type) && type.intrinsicName === "false";
|
|
861
|
+
/** @internal */
|
|
862
|
+
const isTrueLiteralType = (type) => isBooleanLiteralType(type) && type.intrinsicName === "true";
|
|
863
|
+
/** @internal */
|
|
864
|
+
const isAnyType = (type) => isTypeFlagSet(type, ts.TypeFlags.TypeParameter | ts.TypeFlags.Any);
|
|
865
|
+
/** @internal */
|
|
866
|
+
const isBigIntType = (type) => isTypeFlagSet(type, ts.TypeFlags.BigIntLike);
|
|
867
|
+
/** @internal */
|
|
868
|
+
const isBooleanType = (type) => isTypeFlagSet(type, ts.TypeFlags.BooleanLike);
|
|
869
|
+
/** @internal */
|
|
870
|
+
const isEnumType = (type) => isTypeFlagSet(type, ts.TypeFlags.EnumLike);
|
|
871
|
+
/** @internal */
|
|
872
|
+
const isFalsyBigIntType = (type) => type.isLiteral() && isMatching({ value: { base10Value: "0" } }, type);
|
|
873
|
+
/** @internal */
|
|
874
|
+
const isFalsyNumberType = (type) => type.isNumberLiteral() && type.value === 0;
|
|
875
|
+
/** @internal */
|
|
876
|
+
const isFalsyStringType = (type) => type.isStringLiteral() && type.value === "";
|
|
877
|
+
/** @internal */
|
|
878
|
+
const isNeverType = (type) => isTypeFlagSet(type, ts.TypeFlags.Never);
|
|
879
|
+
/** @internal */
|
|
880
|
+
const isNullishType = (type) => isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike);
|
|
881
|
+
/** @internal */
|
|
882
|
+
const isNumberType = (type) => isTypeFlagSet(type, ts.TypeFlags.NumberLike);
|
|
883
|
+
/** @internal */
|
|
884
|
+
const isObjectType = (type) => !isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.TypeParameter | ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never);
|
|
885
|
+
/** @internal */
|
|
886
|
+
const isStringType = (type) => isTypeFlagSet(type, ts.TypeFlags.StringLike);
|
|
887
|
+
/** @internal */
|
|
888
|
+
const isTruthyBigIntType = (type) => type.isLiteral() && isMatching({ value: { base10Value: P.not("0") } }, type);
|
|
889
|
+
/** @internal */
|
|
890
|
+
const isTruthyNumberType = (type) => type.isNumberLiteral() && type.value !== 0;
|
|
891
|
+
/** @internal */
|
|
892
|
+
const isTruthyStringType = (type) => type.isStringLiteral() && type.value !== "";
|
|
893
|
+
/** @internal */
|
|
894
|
+
const isUnknownType = (type) => isTypeFlagSet(type, ts.TypeFlags.Unknown);
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/type-name.ts
|
|
898
|
+
/**
|
|
899
|
+
* An enhanced version of getFullyQualifiedName that handles cases that original function does not handle
|
|
900
|
+
* @param checker TypeScript type checker
|
|
901
|
+
* @param symbol Symbol to get fully qualified name for
|
|
902
|
+
* @returns Fully qualified name of the symbol
|
|
903
|
+
*/
|
|
904
|
+
function getFullyQualifiedNameEx(checker, symbol) {
|
|
905
|
+
let name = symbol.name;
|
|
906
|
+
let parent = symbol.declarations?.at(0)?.parent;
|
|
907
|
+
if (parent == null) return checker.getFullyQualifiedName(symbol);
|
|
908
|
+
while (parent.kind !== ts.SyntaxKind.SourceFile) {
|
|
909
|
+
switch (true) {
|
|
910
|
+
case ts.isInterfaceDeclaration(parent):
|
|
911
|
+
case ts.isTypeAliasDeclaration(parent):
|
|
912
|
+
case ts.isEnumDeclaration(parent):
|
|
913
|
+
case ts.isModuleDeclaration(parent):
|
|
914
|
+
case ts.isNamespaceImport(parent):
|
|
915
|
+
case ts.isNamespaceExport(parent):
|
|
916
|
+
case ts.isNamespaceExportDeclaration(parent):
|
|
917
|
+
name = `${parent.name.text}.${name}`;
|
|
918
|
+
break;
|
|
919
|
+
case ts.isPropertySignature(parent) && ts.isIdentifier(parent.name):
|
|
920
|
+
case ts.isPropertyDeclaration(parent) && ts.isIdentifier(parent.name):
|
|
921
|
+
case ts.isMethodDeclaration(parent) && ts.isIdentifier(parent.name):
|
|
922
|
+
case ts.isMethodSignature(parent) && ts.isIdentifier(parent.name):
|
|
923
|
+
case ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name):
|
|
924
|
+
name = `${parent.name.text}.${name}`;
|
|
925
|
+
break;
|
|
926
|
+
case ts.isFunctionDeclaration(parent) && parent.name != null:
|
|
927
|
+
case ts.isClassExpression(parent) && parent.name != null:
|
|
928
|
+
case ts.isClassDeclaration(parent) && parent.name != null:
|
|
929
|
+
name = `${parent.name.text}.${name}`;
|
|
930
|
+
break;
|
|
931
|
+
case ts.isEnumMember(parent):
|
|
932
|
+
name = `${parent.name.getText()}.${name}`;
|
|
933
|
+
break;
|
|
934
|
+
case ts.isTypeLiteralNode(parent):
|
|
935
|
+
case ts.isMappedTypeNode(parent):
|
|
936
|
+
case ts.isObjectLiteralExpression(parent):
|
|
937
|
+
case ts.isIntersectionTypeNode(parent):
|
|
938
|
+
case ts.isUnionTypeNode(parent): break;
|
|
939
|
+
default: break;
|
|
940
|
+
}
|
|
941
|
+
parent = parent.parent;
|
|
942
|
+
}
|
|
943
|
+
const namespace = parent.getSourceFile().statements.find((n) => ts.isNamespaceExportDeclaration(n))?.name.text;
|
|
944
|
+
if (namespace == null) return name;
|
|
945
|
+
if (name.startsWith(`${namespace}.`)) return name;
|
|
946
|
+
return `${namespace}.${name}`;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/type-variant.ts
|
|
951
|
+
/**
|
|
952
|
+
* Ported from https://github.com/typescript-eslint/typescript-eslint/blob/eb736bbfc22554694400e6a4f97051d845d32e0b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts#L826 with some enhancements
|
|
953
|
+
* Get the variants of an array of types.
|
|
954
|
+
* @param types The types to get the variants of
|
|
955
|
+
* @returns The variants of the types
|
|
956
|
+
* @internal
|
|
957
|
+
*/
|
|
958
|
+
function getTypeVariants(types) {
|
|
959
|
+
const variants = /* @__PURE__ */ new Set();
|
|
960
|
+
if (types.some(isUnknownType)) {
|
|
961
|
+
variants.add("unknown");
|
|
962
|
+
return variants;
|
|
963
|
+
}
|
|
964
|
+
if (types.some(isNullishType)) variants.add("nullish");
|
|
965
|
+
const booleans = types.filter(isBooleanType);
|
|
966
|
+
const boolean0 = booleans[0];
|
|
967
|
+
if (booleans.length === 1 && boolean0 != null) {
|
|
968
|
+
if (isFalseLiteralType(boolean0)) variants.add("falsy boolean");
|
|
969
|
+
else if (isTrueLiteralType(boolean0)) variants.add("truthy boolean");
|
|
970
|
+
} else if (booleans.length === 2) variants.add("boolean");
|
|
971
|
+
const strings = types.filter(isStringType);
|
|
972
|
+
if (strings.length > 0) {
|
|
973
|
+
const evaluated = match(strings).when((types) => types.every(isTruthyStringType), () => "truthy string").when((types) => types.every(isFalsyStringType), () => "falsy string").otherwise(() => "string");
|
|
974
|
+
variants.add(evaluated);
|
|
975
|
+
}
|
|
976
|
+
const bigints = types.filter(isBigIntType);
|
|
977
|
+
if (bigints.length > 0) {
|
|
978
|
+
const evaluated = match(bigints).when((types) => types.every(isTruthyBigIntType), () => "truthy bigint").when((types) => types.every(isFalsyBigIntType), () => "falsy bigint").otherwise(() => "bigint");
|
|
979
|
+
variants.add(evaluated);
|
|
980
|
+
}
|
|
981
|
+
const numbers = types.filter(isNumberType);
|
|
982
|
+
if (numbers.length > 0) {
|
|
983
|
+
const evaluated = match(numbers).when((types) => types.every(isTruthyNumberType), () => "truthy number").when((types) => types.every(isFalsyNumberType), () => "falsy number").otherwise(() => "number");
|
|
984
|
+
variants.add(evaluated);
|
|
985
|
+
}
|
|
986
|
+
if (types.some(isEnumType)) variants.add("enum");
|
|
987
|
+
if (types.some(isObjectType)) variants.add("object");
|
|
988
|
+
if (types.some(isAnyType)) variants.add("any");
|
|
989
|
+
if (types.some(isNeverType)) variants.add("never");
|
|
990
|
+
return variants;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
//#endregion
|
|
994
|
+
export { DEFAULT_COMPONENT_DETECTION_HINT, FunctionComponentDetectionHint, FunctionComponentFlag, REACT_BUILTIN_HOOK_NAMES, getClassComponentCollector, getFullyQualifiedNameEx, getFunctionComponentCollector, getFunctionComponentFlagFromInitPath, getFunctionComponentId, getHookCollector, getTypeVariants, isAPI, isAPICall, isAPIFromReact, isAPIFromReactNative, isAnyType, isAssignmentToThisState, isBigIntType, isBooleanLiteralType, isBooleanType, isCaptureOwnerStack, isCaptureOwnerStackCall, isChildrenCount, isChildrenCountCall, isChildrenForEach, isChildrenForEachCall, isChildrenMap, isChildrenMapCall, isChildrenOnly, isChildrenOnlyCall, isChildrenToArray, isChildrenToArrayCall, isClassComponent, isClassComponentLoose, isCloneElement, isCloneElementCall, isComponentDidCatch, isComponentDidMount, isComponentDidUpdate, isComponentWillMount, isComponentWillReceiveProps, isComponentWillUnmount, isComponentWillUpdate, isCreateContext, isCreateContextCall, isCreateElement, isCreateElementCall, isCreateRef, isCreateRefCall, isEnumType, isFalseLiteralType, isFalsyBigIntType, isFalsyNumberType, isFalsyStringType, isForwardRef, isForwardRefCall, isFunctionComponentDefinition, isFunctionComponentName, isFunctionComponentNameLoose, isFunctionComponentWrapperCall, isFunctionComponentWrapperCallback, isFunctionWithLooseComponentName, isGetChildContext, isGetDefaultProps, isGetDerivedStateFromError, isGetDerivedStateFromProps, isGetInitialState, isGetSnapshotBeforeUpdate, isHookCall, isHookDefinition, isHookId, isHookName, isLazy, isLazyCall, isMemo, isMemoCall, isNeverType, isNullishType, isNumberType, isObjectType, isPureComponent, isRender, isRenderMethodCallback, isRenderMethodLike, isShouldComponentUpdate, isStringType, isThisSetStateCall, isTrueLiteralType, isTruthyBigIntType, isTruthyNumberType, isTruthyStringType, isUnknownType, isUnsafeComponentWillMount, isUnsafeComponentWillReceiveProps, isUnsafeComponentWillUpdate, isUse, isUseActionState, isUseActionStateCall, isUseCall, isUseCallback, isUseCallbackCall, isUseContext, isUseContextCall, isUseDebugValue, isUseDebugValueCall, isUseDeferredValue, isUseDeferredValueCall, isUseEffect, isUseEffectCall, isUseEffectCleanupCallback, isUseEffectLikeCall, isUseEffectSetupCallback, isUseFormStatus, isUseFormStatusCall, isUseId, isUseIdCall, isUseImperativeHandle, isUseImperativeHandleCall, isUseInsertionEffect, isUseInsertionEffectCall, isUseLayoutEffect, isUseLayoutEffectCall, isUseMemo, isUseMemoCall, isUseOptimistic, isUseOptimisticCall, isUseReducer, isUseReducerCall, isUseRef, isUseRefCall, isUseState, isUseStateCall, isUseStateLikeCall, isUseSyncExternalStore, isUseSyncExternalStoreCall, isUseTransition, isUseTransitionCall };
|