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