@eslint-react/core 2.0.0-next.18 → 2.0.0-next.180
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 +579 -288
- package/dist/index.js +1192 -886
- package/package.json +14 -14
package/dist/index.js
CHANGED
|
@@ -1,958 +1,1264 @@
|
|
|
1
|
-
import { AST_NODE_TYPES } from
|
|
2
|
-
import * as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import * as
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
+
import * as AST from "@eslint-react/ast";
|
|
3
|
+
import { constFalse, constTrue, dual, flip, getOrElseUpdate, identity, unit } from "@eslint-react/eff";
|
|
4
|
+
import { RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE, SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION } from "@eslint-react/kit";
|
|
5
|
+
import { getId } from "@eslint-react/shared";
|
|
6
|
+
import * as VAR from "@eslint-react/var";
|
|
7
|
+
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
8
|
+
import { P, isMatching, match } from "ts-pattern";
|
|
9
|
+
import birecord from "birecord";
|
|
10
|
+
import { isFalseLiteralType, isTrueLiteralType, isTypeFlagSet } from "ts-api-utils";
|
|
11
|
+
import ts from "typescript";
|
|
12
|
+
|
|
13
|
+
//#region src/utils/is-react-api.ts
|
|
11
14
|
function isReactAPI(api) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
const func = (context, node) => {
|
|
16
|
+
if (node == null) return false;
|
|
17
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
18
|
+
const name = AST.toStringFormat(node, getText);
|
|
19
|
+
if (name === api) return true;
|
|
20
|
+
if (name.substring(name.indexOf(".") + 1) === api) return true;
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
return dual(2, func);
|
|
21
24
|
}
|
|
22
25
|
function isReactAPICall(api) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
26
|
+
const func = (context, node) => {
|
|
27
|
+
if (node == null) return false;
|
|
28
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
29
|
+
return isReactAPI(api)(context, node.callee);
|
|
30
|
+
};
|
|
31
|
+
return dual(2, func);
|
|
32
|
+
}
|
|
33
|
+
const isCaptureOwnerStack = isReactAPI("captureOwnerStack");
|
|
34
|
+
const isChildrenCount = isReactAPI("Children.count");
|
|
35
|
+
const isChildrenForEach = isReactAPI("Children.forEach");
|
|
36
|
+
const isChildrenMap = isReactAPI("Children.map");
|
|
37
|
+
const isChildrenOnly = isReactAPI("Children.only");
|
|
38
|
+
const isChildrenToArray = isReactAPI("Children.toArray");
|
|
39
|
+
const isCloneElement = isReactAPI("cloneElement");
|
|
40
|
+
const isCreateContext = isReactAPI("createContext");
|
|
41
|
+
const isCreateElement = isReactAPI("createElement");
|
|
42
|
+
const isCreateRef = isReactAPI("createRef");
|
|
43
|
+
const isForwardRef = isReactAPI("forwardRef");
|
|
44
|
+
const isMemo = isReactAPI("memo");
|
|
45
|
+
const isLazy = isReactAPI("lazy");
|
|
46
|
+
const isCaptureOwnerStackCall = isReactAPICall("captureOwnerStack");
|
|
47
|
+
const isChildrenCountCall = isReactAPICall("Children.count");
|
|
48
|
+
const isChildrenForEachCall = isReactAPICall("Children.forEach");
|
|
49
|
+
const isChildrenMapCall = isReactAPICall("Children.map");
|
|
50
|
+
const isChildrenOnlyCall = isReactAPICall("Children.only");
|
|
51
|
+
const isChildrenToArrayCall = isReactAPICall("Children.toArray");
|
|
52
|
+
const isCloneElementCall = isReactAPICall("cloneElement");
|
|
53
|
+
const isCreateContextCall = isReactAPICall("createContext");
|
|
54
|
+
const isCreateElementCall = isReactAPICall("createElement");
|
|
55
|
+
const isCreateRefCall = isReactAPICall("createRef");
|
|
56
|
+
const isForwardRefCall = isReactAPICall("forwardRef");
|
|
57
|
+
const isMemoCall = isReactAPICall("memo");
|
|
58
|
+
const isLazyCall = isReactAPICall("lazy");
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/component/component-children.ts
|
|
62
|
+
/**
|
|
63
|
+
* Determines whether inside `createElement`'s children.
|
|
64
|
+
* @param context The rule context
|
|
65
|
+
* @param node The AST node to check
|
|
66
|
+
* @returns `true` if the node is inside createElement's children
|
|
67
|
+
*/
|
|
58
68
|
function isChildrenOfCreateElement(context, node) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const parent = node.parent;
|
|
70
|
+
if (parent == null || parent.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
71
|
+
if (!isCreateElementCall(context, parent)) return false;
|
|
72
|
+
return parent.arguments.slice(2).some((arg) => arg === node);
|
|
63
73
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const { init } = node;
|
|
88
|
-
if (init.type === AST_NODE_TYPES.MemberExpression && init.object.type === AST_NODE_TYPES.Identifier) {
|
|
89
|
-
return isInitializedFromReact(init.object.name, importSource, initialScope);
|
|
90
|
-
}
|
|
91
|
-
if (init.type === AST_NODE_TYPES.Identifier) {
|
|
92
|
-
return isInitializedFromReact(init.name, importSource, initialScope);
|
|
93
|
-
}
|
|
94
|
-
const args = getRequireExpressionArguments(init);
|
|
95
|
-
const arg0 = args?.[0];
|
|
96
|
-
if (arg0 == null || !AST14.isLiteral(arg0, "string")) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return arg0.value === importSource || arg0.value.startsWith(`${importSource}/`);
|
|
100
|
-
}
|
|
101
|
-
return parent?.type === AST_NODE_TYPES.ImportDeclaration && parent.source.value === importSource;
|
|
102
|
-
}
|
|
103
|
-
function isInstanceIdEqual(context, a, b) {
|
|
104
|
-
return AST14.isNodeEqual(a, b) || VAR3.isNodeValueEqual(a, b, [
|
|
105
|
-
context.sourceCode.getScope(a),
|
|
106
|
-
context.sourceCode.getScope(b)
|
|
107
|
-
]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// src/hook/hook-name.ts
|
|
111
|
-
var REACT_BUILTIN_HOOK_NAMES = [
|
|
112
|
-
"use",
|
|
113
|
-
"useActionState",
|
|
114
|
-
"useCallback",
|
|
115
|
-
"useContext",
|
|
116
|
-
"useDebugValue",
|
|
117
|
-
"useDeferredValue",
|
|
118
|
-
"useEffect",
|
|
119
|
-
"useFormStatus",
|
|
120
|
-
"useId",
|
|
121
|
-
"useImperativeHandle",
|
|
122
|
-
"useInsertionEffect",
|
|
123
|
-
"useLayoutEffect",
|
|
124
|
-
"useMemo",
|
|
125
|
-
"useOptimistic",
|
|
126
|
-
"useReducer",
|
|
127
|
-
"useRef",
|
|
128
|
-
"useState",
|
|
129
|
-
"useSyncExternalStore",
|
|
130
|
-
"useTransition"
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/hook/hook-name.ts
|
|
77
|
+
const REACT_BUILTIN_HOOK_NAMES = [
|
|
78
|
+
"use",
|
|
79
|
+
"useActionState",
|
|
80
|
+
"useCallback",
|
|
81
|
+
"useContext",
|
|
82
|
+
"useDebugValue",
|
|
83
|
+
"useDeferredValue",
|
|
84
|
+
"useEffect",
|
|
85
|
+
"useFormStatus",
|
|
86
|
+
"useId",
|
|
87
|
+
"useImperativeHandle",
|
|
88
|
+
"useInsertionEffect",
|
|
89
|
+
"useLayoutEffect",
|
|
90
|
+
"useMemo",
|
|
91
|
+
"useOptimistic",
|
|
92
|
+
"useReducer",
|
|
93
|
+
"useRef",
|
|
94
|
+
"useState",
|
|
95
|
+
"useSyncExternalStore",
|
|
96
|
+
"useTransition"
|
|
131
97
|
];
|
|
98
|
+
/**
|
|
99
|
+
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
|
100
|
+
* character to exclude identifiers like "user".
|
|
101
|
+
* @param name The name of the identifier to check.
|
|
102
|
+
* @see https://github.com/facebook/react/blob/1d6c8168db1d82713202e842df3167787ffa00ed/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts#L16
|
|
103
|
+
*/
|
|
132
104
|
function isReactHookName(name) {
|
|
133
|
-
|
|
105
|
+
return name === "use" || /^use[A-Z0-9]/.test(name);
|
|
134
106
|
}
|
|
135
107
|
|
|
136
|
-
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/hook/hook-is.ts
|
|
110
|
+
/**
|
|
111
|
+
* Determines if a function node is a React Hook based on its name.
|
|
112
|
+
* @param node The function node to check
|
|
113
|
+
* @returns True if the function is a React Hook, false otherwise
|
|
114
|
+
*/
|
|
137
115
|
function isReactHook(node) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
116
|
+
if (node == null) return false;
|
|
117
|
+
const id = AST.getFunctionId(node);
|
|
118
|
+
return id?.name != null && isReactHookName(id.name);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if the given node is a React Hook call by its name.
|
|
122
|
+
* @param node The node to check.
|
|
123
|
+
* @returns `true` if the node is a React Hook call, `false` otherwise.
|
|
124
|
+
*/
|
|
142
125
|
function isReactHookCall(node) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
function isReactHookCallWithName(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
var isUseCall = flip(isReactHookCallWithName)("use");
|
|
218
|
-
var isUseActionStateCall = flip(isReactHookCallWithName)("useActionState");
|
|
219
|
-
var isUseCallbackCall = flip(isReactHookCallWithName)("useCallback");
|
|
220
|
-
var isUseContextCall = flip(isReactHookCallWithName)("useContext");
|
|
221
|
-
var isUseDebugValueCall = flip(isReactHookCallWithName)("useDebugValue");
|
|
222
|
-
var isUseDeferredValueCall = flip(isReactHookCallWithName)("useDeferredValue");
|
|
223
|
-
var isUseEffectCall = flip(isReactHookCallWithName)("useEffect");
|
|
224
|
-
var isUseFormStatusCall = flip(isReactHookCallWithName)("useFormStatus");
|
|
225
|
-
var isUseIdCall = flip(isReactHookCallWithName)("useId");
|
|
226
|
-
var isUseImperativeHandleCall = flip(isReactHookCallWithName)("useImperativeHandle");
|
|
227
|
-
var isUseInsertionEffectCall = flip(isReactHookCallWithName)("useInsertionEffect");
|
|
228
|
-
var isUseLayoutEffectCall = flip(isReactHookCallWithName)("useLayoutEffect");
|
|
229
|
-
var isUseMemoCall = flip(isReactHookCallWithName)("useMemo");
|
|
230
|
-
var isUseOptimisticCall = flip(isReactHookCallWithName)("useOptimistic");
|
|
231
|
-
var isUseReducerCall = flip(isReactHookCallWithName)("useReducer");
|
|
232
|
-
var isUseRefCall = flip(isReactHookCallWithName)("useRef");
|
|
233
|
-
var isUseStateCall = flip(isReactHookCallWithName)("useState");
|
|
234
|
-
var isUseSyncExternalStoreCall = flip(isReactHookCallWithName)("useSyncExternalStore");
|
|
235
|
-
var isUseTransitionCall = flip(isReactHookCallWithName)("useTransition");
|
|
236
|
-
|
|
237
|
-
// src/hook/hook-collector.ts
|
|
126
|
+
if (node == null) return false;
|
|
127
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
128
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) return isReactHookName(node.callee.name);
|
|
129
|
+
if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.property.type === AST_NODE_TYPES.Identifier && isReactHookName(node.callee.property.name);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Checks if a node is a call to a specific React hook.
|
|
134
|
+
* Returns a function that accepts a hook name to check against.
|
|
135
|
+
* @param node The AST node to check
|
|
136
|
+
* @returns A function that takes a hook name and returns boolean
|
|
137
|
+
*/
|
|
138
|
+
function isReactHookCallWithName(node) {
|
|
139
|
+
if (node == null || node.type !== AST_NODE_TYPES.CallExpression) return constFalse;
|
|
140
|
+
return (name) => {
|
|
141
|
+
switch (node.callee.type) {
|
|
142
|
+
case AST_NODE_TYPES.Identifier: return node.callee.name === name;
|
|
143
|
+
case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === name;
|
|
144
|
+
default: return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Checks if a node is a call to a specific React hook or one of its aliases.
|
|
150
|
+
* @param name The primary hook name to check
|
|
151
|
+
* @param alias Optional array of alias names to also accept
|
|
152
|
+
* @returns Function that checks if a node matches the hook name or aliases
|
|
153
|
+
*/
|
|
154
|
+
function isReactHookCallWithNameAlias(name, alias = []) {
|
|
155
|
+
return (node) => {
|
|
156
|
+
switch (true) {
|
|
157
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === name: return true;
|
|
158
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === name && "name" in node.callee.object: return true;
|
|
159
|
+
default: return alias.some(isReactHookCallWithName(node));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Detects useEffect calls and variations (useLayoutEffect, etc.) using regex pattern.
|
|
165
|
+
* @param node The AST node to check
|
|
166
|
+
* @returns True if the node is a useEffect-like call
|
|
167
|
+
*/
|
|
168
|
+
function isUseEffectLikeCall(node) {
|
|
169
|
+
if (node == null) return false;
|
|
170
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
171
|
+
switch (node.callee.type) {
|
|
172
|
+
case AST_NODE_TYPES.Identifier: return /^use\w*Effect$/u.test(node.callee.name);
|
|
173
|
+
case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && /^use\w*Effect$/u.test(node.callee.property.name);
|
|
174
|
+
default: return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const isUseCall = flip(isReactHookCallWithName)("use");
|
|
178
|
+
const isUseActionStateCall = flip(isReactHookCallWithName)("useActionState");
|
|
179
|
+
const isUseCallbackCall = flip(isReactHookCallWithName)("useCallback");
|
|
180
|
+
const isUseContextCall = flip(isReactHookCallWithName)("useContext");
|
|
181
|
+
const isUseDebugValueCall = flip(isReactHookCallWithName)("useDebugValue");
|
|
182
|
+
const isUseDeferredValueCall = flip(isReactHookCallWithName)("useDeferredValue");
|
|
183
|
+
const isUseEffectCall = flip(isReactHookCallWithName)("useEffect");
|
|
184
|
+
const isUseFormStatusCall = flip(isReactHookCallWithName)("useFormStatus");
|
|
185
|
+
const isUseIdCall = flip(isReactHookCallWithName)("useId");
|
|
186
|
+
const isUseImperativeHandleCall = flip(isReactHookCallWithName)("useImperativeHandle");
|
|
187
|
+
const isUseInsertionEffectCall = flip(isReactHookCallWithName)("useInsertionEffect");
|
|
188
|
+
const isUseLayoutEffectCall = flip(isReactHookCallWithName)("useLayoutEffect");
|
|
189
|
+
const isUseMemoCall = flip(isReactHookCallWithName)("useMemo");
|
|
190
|
+
const isUseOptimisticCall = flip(isReactHookCallWithName)("useOptimistic");
|
|
191
|
+
const isUseReducerCall = flip(isReactHookCallWithName)("useReducer");
|
|
192
|
+
const isUseRefCall = flip(isReactHookCallWithName)("useRef");
|
|
193
|
+
const isUseStateCall = flip(isReactHookCallWithName)("useState");
|
|
194
|
+
const isUseSyncExternalStoreCall = flip(isReactHookCallWithName)("useSyncExternalStore");
|
|
195
|
+
const isUseTransitionCall = flip(isReactHookCallWithName)("useTransition");
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/hook/hook-collector.ts
|
|
238
199
|
function useHookCollector() {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
200
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
201
|
+
const functionEntries = [];
|
|
202
|
+
const onFunctionEnter = (node) => {
|
|
203
|
+
const id = AST.getFunctionId(node);
|
|
204
|
+
const key = getId();
|
|
205
|
+
const name = id?.name;
|
|
206
|
+
if (name != null && isReactHookName(name)) {
|
|
207
|
+
functionEntries.push({
|
|
208
|
+
key,
|
|
209
|
+
node,
|
|
210
|
+
isHook: true
|
|
211
|
+
});
|
|
212
|
+
hooks.set(key, {
|
|
213
|
+
id,
|
|
214
|
+
key,
|
|
215
|
+
kind: "function",
|
|
216
|
+
name,
|
|
217
|
+
node,
|
|
218
|
+
flag: 0n,
|
|
219
|
+
hint: 0n,
|
|
220
|
+
hookCalls: []
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
functionEntries.push({
|
|
225
|
+
key,
|
|
226
|
+
node,
|
|
227
|
+
isHook: false
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
const onFunctionExit = () => {
|
|
231
|
+
functionEntries.pop();
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
ctx: { getAllHooks(node) {
|
|
235
|
+
return hooks;
|
|
236
|
+
} },
|
|
237
|
+
listeners: {
|
|
238
|
+
":function[type]": onFunctionEnter,
|
|
239
|
+
":function[type]:exit": onFunctionExit,
|
|
240
|
+
"CallExpression[type]"(node) {
|
|
241
|
+
if (!isReactHookCall(node)) return;
|
|
242
|
+
const fEntry = functionEntries.at(-1);
|
|
243
|
+
if (fEntry?.key == null) return;
|
|
244
|
+
const hook = hooks.get(fEntry.key);
|
|
245
|
+
if (hook == null) return;
|
|
246
|
+
hook.hookCalls.push(node);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
289
250
|
}
|
|
251
|
+
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/hook/hook-id.ts
|
|
290
254
|
function isReactHookId(id) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
default:
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
255
|
+
switch (id.type) {
|
|
256
|
+
case AST_NODE_TYPES.Identifier: return isReactHookName(id.name);
|
|
257
|
+
case AST_NODE_TYPES.MemberExpression: return "name" in id.property && isReactHookName(id.property.name);
|
|
258
|
+
default: return false;
|
|
259
|
+
}
|
|
299
260
|
}
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/jsx/jsx-stringify.ts
|
|
264
|
+
/**
|
|
265
|
+
* Incomplete but sufficient stringification of JSX nodes for common use cases
|
|
266
|
+
*
|
|
267
|
+
* @param node - JSX node from TypeScript ESTree
|
|
268
|
+
* @returns String representation of the JSX node
|
|
269
|
+
*/
|
|
300
270
|
function stringifyJsx(node) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// src/jsx/jsx-attribute-name.ts
|
|
271
|
+
switch (node.type) {
|
|
272
|
+
case AST_NODE_TYPES.JSXIdentifier: return node.name;
|
|
273
|
+
case AST_NODE_TYPES.JSXNamespacedName: return `${node.namespace.name}:${node.name.name}`;
|
|
274
|
+
case AST_NODE_TYPES.JSXMemberExpression: return `${stringifyJsx(node.object)}.${stringifyJsx(node.property)}`;
|
|
275
|
+
case AST_NODE_TYPES.JSXText: return node.value;
|
|
276
|
+
case AST_NODE_TYPES.JSXOpeningElement: return `<${stringifyJsx(node.name)}>`;
|
|
277
|
+
case AST_NODE_TYPES.JSXClosingElement: return `</${stringifyJsx(node.name)}>`;
|
|
278
|
+
case AST_NODE_TYPES.JSXOpeningFragment: return "<>";
|
|
279
|
+
case AST_NODE_TYPES.JSXClosingFragment: return "</>";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/jsx/jsx-attribute-name.ts
|
|
285
|
+
/**
|
|
286
|
+
* Get the stringified name of a JSX attribute
|
|
287
|
+
* @param context The ESLint rule context
|
|
288
|
+
* @param node The JSX attribute node
|
|
289
|
+
* @returns The name of the attribute
|
|
290
|
+
*/
|
|
322
291
|
function getAttributeName(context, node) {
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
292
|
+
return stringifyJsx(node.name);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/jsx/jsx-attribute.ts
|
|
297
|
+
function getAttribute(context, attributes, initialScope) {
|
|
298
|
+
return (name) => {
|
|
299
|
+
return attributes.findLast((attr) => {
|
|
300
|
+
if (attr.type === AST_NODE_TYPES.JSXAttribute) return getAttributeName(context, attr) === name;
|
|
301
|
+
if (initialScope == null) return false;
|
|
302
|
+
switch (attr.argument.type) {
|
|
303
|
+
case AST_NODE_TYPES.Identifier: {
|
|
304
|
+
const variable = VAR.findVariable(attr.argument.name, initialScope);
|
|
305
|
+
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
306
|
+
if (variableNode?.type === AST_NODE_TYPES.ObjectExpression) return VAR.findProperty(name, variableNode.properties, initialScope) != null;
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
case AST_NODE_TYPES.ObjectExpression: return VAR.findProperty(name, attr.argument.properties, initialScope) != null;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/jsx/jsx-attribute-value.ts
|
|
318
|
+
function resolveAttributeValue(context, attribute) {
|
|
319
|
+
const initialScope = context.sourceCode.getScope(attribute);
|
|
320
|
+
function handleJsxAttribute(node) {
|
|
321
|
+
if (node.value == null) return {
|
|
322
|
+
kind: "boolean",
|
|
323
|
+
toStatic() {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
switch (node.value.type) {
|
|
328
|
+
case AST_NODE_TYPES.Literal: {
|
|
329
|
+
const staticValue = node.value.value;
|
|
330
|
+
return {
|
|
331
|
+
kind: "literal",
|
|
332
|
+
node: node.value,
|
|
333
|
+
toStatic() {
|
|
334
|
+
return staticValue;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
case AST_NODE_TYPES.JSXExpressionContainer: {
|
|
339
|
+
const expr = node.value.expression;
|
|
340
|
+
return {
|
|
341
|
+
kind: "expression",
|
|
342
|
+
node: expr,
|
|
343
|
+
toStatic() {
|
|
344
|
+
return getStaticValue(expr, initialScope)?.value;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
case AST_NODE_TYPES.JSXElement: return {
|
|
349
|
+
kind: "element",
|
|
350
|
+
node: node.value,
|
|
351
|
+
toStatic() {
|
|
352
|
+
return unit;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
case AST_NODE_TYPES.JSXSpreadChild: return {
|
|
356
|
+
kind: "spreadChild",
|
|
357
|
+
node: node.value.expression,
|
|
358
|
+
toStatic() {
|
|
359
|
+
return unit;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function handleJsxSpreadAttribute(node) {
|
|
365
|
+
return {
|
|
366
|
+
kind: "spreadProps",
|
|
367
|
+
node: node.argument,
|
|
368
|
+
toStatic(name) {
|
|
369
|
+
if (name == null) return unit;
|
|
370
|
+
return match(getStaticValue(node.argument, initialScope)?.value).with({ [name]: P.select(P.any) }, identity).otherwise(() => unit);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
switch (attribute.type) {
|
|
375
|
+
case AST_NODE_TYPES.JSXAttribute: return handleJsxAttribute(attribute);
|
|
376
|
+
case AST_NODE_TYPES.JSXSpreadAttribute: return handleJsxSpreadAttribute(attribute);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/jsx/jsx-config.ts
|
|
382
|
+
const JsxEmit = {
|
|
383
|
+
None: 0,
|
|
384
|
+
Preserve: 1,
|
|
385
|
+
React: 2,
|
|
386
|
+
ReactNative: 3,
|
|
387
|
+
ReactJSX: 4,
|
|
388
|
+
ReactJSXDev: 5
|
|
403
389
|
};
|
|
404
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Get JsxConfig from the rule context by reading compiler options.
|
|
392
|
+
* @param context The RuleContext.
|
|
393
|
+
* @returns JsxConfig derived from compiler options.
|
|
394
|
+
*/
|
|
395
|
+
function getJsxConfigFromContext(context) {
|
|
396
|
+
const options = context.sourceCode.parserServices?.program?.getCompilerOptions() ?? {};
|
|
397
|
+
return {
|
|
398
|
+
jsx: options.jsx ?? JsxEmit.ReactJSX,
|
|
399
|
+
jsxFactory: options.jsxFactory ?? "React.createElement",
|
|
400
|
+
jsxFragmentFactory: options.jsxFragmentFactory ?? "React.Fragment",
|
|
401
|
+
jsxImportSource: options.jsxImportSource ?? "react"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
405
|
+
/**
|
|
406
|
+
* Get JsxConfig from pragma comments (annotations) in the source code.
|
|
407
|
+
* @param context The RuleContext.
|
|
408
|
+
* @returns JsxConfig derived from pragma comments.
|
|
409
|
+
*/
|
|
410
|
+
function getJsxConfigFromAnnotation(context) {
|
|
411
|
+
return getOrElseUpdate(cache, context.sourceCode, () => {
|
|
412
|
+
const options = {};
|
|
413
|
+
if (!context.sourceCode.text.includes("@jsx")) return options;
|
|
414
|
+
let jsx, jsxFrag, jsxRuntime, jsxImportSource;
|
|
415
|
+
for (const comment of context.sourceCode.getAllComments().reverse()) {
|
|
416
|
+
const value = comment.value;
|
|
417
|
+
jsx ??= value.match(RE_ANNOTATION_JSX)?.[1];
|
|
418
|
+
jsxFrag ??= value.match(RE_ANNOTATION_JSX_FRAG)?.[1];
|
|
419
|
+
jsxRuntime ??= value.match(RE_ANNOTATION_JSX_RUNTIME)?.[1];
|
|
420
|
+
jsxImportSource ??= value.match(RE_ANNOTATION_JSX_IMPORT_SOURCE)?.[1];
|
|
421
|
+
}
|
|
422
|
+
if (jsx != null) options.jsxFactory = jsx;
|
|
423
|
+
if (jsxFrag != null) options.jsxFragmentFactory = jsxFrag;
|
|
424
|
+
if (jsxRuntime != null) options.jsx = jsxRuntime === "classic" ? JsxEmit.React : JsxEmit.ReactJSX;
|
|
425
|
+
if (jsxImportSource != null) options.jsxImportSource = jsxImportSource;
|
|
426
|
+
return options;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
405
429
|
|
|
406
|
-
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/jsx/jsx-detection.ts
|
|
432
|
+
/**
|
|
433
|
+
* Flags to control JSX detection behavior:
|
|
434
|
+
* - Skip* flags: Ignore specific node types when detecting JSX
|
|
435
|
+
* - Strict* flags: Enforce stricter rules for container types
|
|
436
|
+
*/
|
|
437
|
+
const JSXDetectionHint = {
|
|
438
|
+
None: 0n,
|
|
439
|
+
SkipUndefined: 1n << 0n,
|
|
440
|
+
SkipNullLiteral: 1n << 1n,
|
|
441
|
+
SkipBooleanLiteral: 1n << 2n,
|
|
442
|
+
SkipStringLiteral: 1n << 3n,
|
|
443
|
+
SkipNumberLiteral: 1n << 4n,
|
|
444
|
+
SkipBigIntLiteral: 1n << 5n,
|
|
445
|
+
SkipEmptyArray: 1n << 6n,
|
|
446
|
+
SkipCreateElement: 1n << 7n,
|
|
447
|
+
StrictArray: 1n << 8n,
|
|
448
|
+
StrictLogical: 1n << 9n,
|
|
449
|
+
StrictConditional: 1n << 10n
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* Default JSX detection configuration
|
|
453
|
+
* Skips undefined and boolean literals (common in React)
|
|
454
|
+
*/
|
|
455
|
+
const DEFAULT_JSX_DETECTION_HINT = 0n | JSXDetectionHint.SkipUndefined | JSXDetectionHint.SkipBooleanLiteral;
|
|
456
|
+
/**
|
|
457
|
+
* Checks if a node is a `JSXText` or a `Literal` node
|
|
458
|
+
* @param node The AST node to check
|
|
459
|
+
* @returns `true` if the node is a `JSXText` or a `Literal` node
|
|
460
|
+
*/
|
|
407
461
|
function isJsxText(node) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
462
|
+
if (node == null) return false;
|
|
463
|
+
return node.type === AST_NODE_TYPES.JSXText || node.type === AST_NODE_TYPES.Literal;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Determines if a node represents JSX-like content based on heuristics
|
|
467
|
+
* Supports configuration through hint flags to customize detection behavior
|
|
468
|
+
*
|
|
469
|
+
* @param code The source code with scope lookup capability
|
|
470
|
+
* @param code.getScope The function to get the scope of a node
|
|
471
|
+
* @param node The AST node to analyze
|
|
472
|
+
* @param hint The configuration flags to adjust detection behavior
|
|
473
|
+
* @returns boolean Whether the node is considered JSX-like
|
|
474
|
+
*/
|
|
411
475
|
function isJsxLike(code, node, hint = DEFAULT_JSX_DETECTION_HINT) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return isJsxLike(code, exp, hint);
|
|
469
|
-
}
|
|
470
|
-
case AST_NODE_TYPES.CallExpression: {
|
|
471
|
-
if (hint & JSXDetectionHint.SkipCreateElement) {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
switch (node.callee.type) {
|
|
475
|
-
case AST_NODE_TYPES.Identifier:
|
|
476
|
-
return node.callee.name === "createElement";
|
|
477
|
-
case AST_NODE_TYPES.MemberExpression:
|
|
478
|
-
return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "createElement";
|
|
479
|
-
}
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
case AST_NODE_TYPES.Identifier: {
|
|
483
|
-
const { name } = node;
|
|
484
|
-
if (name === "undefined") {
|
|
485
|
-
return !(hint & JSXDetectionHint.SkipUndefined);
|
|
486
|
-
}
|
|
487
|
-
if (AST14.isJSXTagNameExpression(node)) {
|
|
488
|
-
return true;
|
|
489
|
-
}
|
|
490
|
-
const variable = VAR3.findVariable(name, code.getScope(node));
|
|
491
|
-
const variableNode = variable && VAR3.getVariableInitNode(variable, 0);
|
|
492
|
-
return !!variableNode && isJsxLike(code, variableNode, hint);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return false;
|
|
476
|
+
if (node == null) return false;
|
|
477
|
+
if (AST.isJSX(node)) return true;
|
|
478
|
+
switch (node.type) {
|
|
479
|
+
case AST_NODE_TYPES.Literal:
|
|
480
|
+
switch (typeof node.value) {
|
|
481
|
+
case "boolean": return !(hint & JSXDetectionHint.SkipBooleanLiteral);
|
|
482
|
+
case "string": return !(hint & JSXDetectionHint.SkipStringLiteral);
|
|
483
|
+
case "number": return !(hint & JSXDetectionHint.SkipNumberLiteral);
|
|
484
|
+
case "bigint": return !(hint & JSXDetectionHint.SkipBigIntLiteral);
|
|
485
|
+
}
|
|
486
|
+
if (node.value == null) return !(hint & JSXDetectionHint.SkipNullLiteral);
|
|
487
|
+
return false;
|
|
488
|
+
case AST_NODE_TYPES.TemplateLiteral: return !(hint & JSXDetectionHint.SkipStringLiteral);
|
|
489
|
+
case AST_NODE_TYPES.ArrayExpression:
|
|
490
|
+
if (node.elements.length === 0) return !(hint & JSXDetectionHint.SkipEmptyArray);
|
|
491
|
+
if (hint & JSXDetectionHint.StrictArray) return node.elements.every((n) => isJsxLike(code, n, hint));
|
|
492
|
+
return node.elements.some((n) => isJsxLike(code, n, hint));
|
|
493
|
+
case AST_NODE_TYPES.LogicalExpression:
|
|
494
|
+
if (hint & JSXDetectionHint.StrictLogical) return isJsxLike(code, node.left, hint) && isJsxLike(code, node.right, hint);
|
|
495
|
+
return isJsxLike(code, node.left, hint) || isJsxLike(code, node.right, hint);
|
|
496
|
+
case AST_NODE_TYPES.ConditionalExpression: {
|
|
497
|
+
function leftHasJSX(node$1) {
|
|
498
|
+
if (Array.isArray(node$1.consequent)) {
|
|
499
|
+
if (node$1.consequent.length === 0) return !(hint & JSXDetectionHint.SkipEmptyArray);
|
|
500
|
+
if (hint & JSXDetectionHint.StrictArray) return node$1.consequent.every((n) => isJsxLike(code, n, hint));
|
|
501
|
+
return node$1.consequent.some((n) => isJsxLike(code, n, hint));
|
|
502
|
+
}
|
|
503
|
+
return isJsxLike(code, node$1.consequent, hint);
|
|
504
|
+
}
|
|
505
|
+
function rightHasJSX(node$1) {
|
|
506
|
+
return isJsxLike(code, node$1.alternate, hint);
|
|
507
|
+
}
|
|
508
|
+
if (hint & JSXDetectionHint.StrictConditional) return leftHasJSX(node) && rightHasJSX(node);
|
|
509
|
+
return leftHasJSX(node) || rightHasJSX(node);
|
|
510
|
+
}
|
|
511
|
+
case AST_NODE_TYPES.SequenceExpression: {
|
|
512
|
+
const exp = node.expressions.at(-1);
|
|
513
|
+
return isJsxLike(code, exp, hint);
|
|
514
|
+
}
|
|
515
|
+
case AST_NODE_TYPES.CallExpression:
|
|
516
|
+
if (hint & JSXDetectionHint.SkipCreateElement) return false;
|
|
517
|
+
switch (node.callee.type) {
|
|
518
|
+
case AST_NODE_TYPES.Identifier: return node.callee.name === "createElement";
|
|
519
|
+
case AST_NODE_TYPES.MemberExpression: return node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "createElement";
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
case AST_NODE_TYPES.Identifier: {
|
|
523
|
+
const { name } = node;
|
|
524
|
+
if (name === "undefined") return !(hint & JSXDetectionHint.SkipUndefined);
|
|
525
|
+
if (AST.isJSXTagNameExpression(node)) return true;
|
|
526
|
+
const variable = VAR.findVariable(name, code.getScope(node));
|
|
527
|
+
const variableNode = variable && VAR.getVariableDefinitionNode(variable, 0);
|
|
528
|
+
return !!variableNode && isJsxLike(code, variableNode, hint);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
496
532
|
}
|
|
533
|
+
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/jsx/jsx-element-type.ts
|
|
536
|
+
/**
|
|
537
|
+
* Extracts the element type name from a JSX element or fragment
|
|
538
|
+
* For JSX elements, returns the stringified name (e.g., "div", "Button", "React.Fragment")
|
|
539
|
+
* For JSX fragments, returns an empty string
|
|
540
|
+
*
|
|
541
|
+
* @param context - ESLint rule context
|
|
542
|
+
* @param node - JSX element or fragment node
|
|
543
|
+
* @returns String representation of the element type
|
|
544
|
+
*/
|
|
497
545
|
function getElementType(context, node) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
return stringifyJsx(node.openingElement.name);
|
|
546
|
+
if (node.type === AST_NODE_TYPES.JSXFragment) return "";
|
|
547
|
+
return stringifyJsx(node.openingElement.name);
|
|
502
548
|
}
|
|
503
549
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/jsx/jsx-element-is.ts
|
|
552
|
+
/**
|
|
553
|
+
* Determines if a JSX element is a host element
|
|
554
|
+
* Host elements in React start with lowercase letters (e.g., div, span)
|
|
555
|
+
*
|
|
556
|
+
* @param context - ESLint rule context
|
|
557
|
+
* @param node - AST node to check
|
|
558
|
+
* @returns boolean indicating if the element is a host element
|
|
559
|
+
*/
|
|
560
|
+
function isHostElement(context, node) {
|
|
561
|
+
return node.type === AST_NODE_TYPES.JSXElement && node.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && /^[a-z]/u.test(node.openingElement.name.name);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Determines if a JSX element is a React Fragment
|
|
565
|
+
* Fragments can be imported from React and used like <Fragment> or <React.Fragment>
|
|
566
|
+
*
|
|
567
|
+
* @param context - ESLint rule context
|
|
568
|
+
* @param node - AST node to check
|
|
569
|
+
* @returns boolean indicating if the element is a Fragment with type narrowing
|
|
570
|
+
*/
|
|
571
|
+
function isFragmentElement(context, node) {
|
|
572
|
+
if (node.type !== AST_NODE_TYPES.JSXElement) return false;
|
|
573
|
+
return getElementType(context, node).split(".").at(-1) === "Fragment";
|
|
507
574
|
}
|
|
575
|
+
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/jsx/jsx-has.ts
|
|
578
|
+
/**
|
|
579
|
+
* Checks if a JSX element has a specific attribute
|
|
580
|
+
*
|
|
581
|
+
* @param context - ESLint rule context
|
|
582
|
+
* @param name - Name of the attribute to check for
|
|
583
|
+
* @param attributes - List of JSX attributes from opening element
|
|
584
|
+
* @param initialScope - Optional scope for resolving variables in spread attributes
|
|
585
|
+
* @returns boolean indicating whether the attribute exists
|
|
586
|
+
*/
|
|
587
|
+
function hasAttribute(context, name, attributes, initialScope) {
|
|
588
|
+
return getAttribute(context, attributes, initialScope)(name) != null;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Checks if a JSX element has at least one of the specified attributes
|
|
592
|
+
*
|
|
593
|
+
* @param context - ESLint rule context
|
|
594
|
+
* @param names - Array of attribute names to check for
|
|
595
|
+
* @param attributes - List of JSX attributes from opening element
|
|
596
|
+
* @param initialScope - Optional scope for resolving variables in spread attributes
|
|
597
|
+
* @returns boolean indicating whether any of the attributes exist
|
|
598
|
+
*/
|
|
508
599
|
function hasAnyAttribute(context, names, attributes, initialScope) {
|
|
509
|
-
|
|
510
|
-
}
|
|
600
|
+
return names.some((n) => hasAttribute(context, n, attributes, initialScope));
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Checks if a JSX element has all of the specified attributes
|
|
604
|
+
*
|
|
605
|
+
* @param context - ESLint rule context
|
|
606
|
+
* @param names - Array of attribute names to check for
|
|
607
|
+
* @param attributes - List of JSX attributes from opening element
|
|
608
|
+
* @param initialScope - Optional scope for resolving variables in spread attributes
|
|
609
|
+
* @returns boolean indicating whether all of the attributes exist
|
|
610
|
+
*/
|
|
511
611
|
function hasEveryAttribute(context, names, attributes, initialScope) {
|
|
512
|
-
|
|
612
|
+
return names.every((n) => hasAttribute(context, n, attributes, initialScope));
|
|
513
613
|
}
|
|
614
|
+
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/jsx/jsx-hierarchy.ts
|
|
617
|
+
/**
|
|
618
|
+
* Traverses up the AST to find a parent JSX attribute node that matches a given test
|
|
619
|
+
*
|
|
620
|
+
* @param node - The starting AST node
|
|
621
|
+
* @param test - Optional predicate function to test if the attribute meets criteria
|
|
622
|
+
* Defaults to always returning true (matches any attribute)
|
|
623
|
+
* @returns The first matching JSX attribute node found when traversing upwards, or undefined
|
|
624
|
+
*/
|
|
514
625
|
function findParentAttribute(node, test = constTrue) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
626
|
+
const guard = (node$1) => {
|
|
627
|
+
return node$1.type === AST_NODE_TYPES.JSXAttribute && test(node$1);
|
|
628
|
+
};
|
|
629
|
+
return AST.findParentNode(node, guard);
|
|
519
630
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
var ComponentDetectionHint = {
|
|
535
|
-
/**
|
|
536
|
-
* 1n << 0n - 1n << 63n are reserved for JSXDetectionHint
|
|
537
|
-
*/
|
|
538
|
-
...JSXDetectionHint,
|
|
539
|
-
/**
|
|
540
|
-
* Skip function component created by React.memo
|
|
541
|
-
*/
|
|
542
|
-
SkipMemo: 1n << 64n,
|
|
543
|
-
/**
|
|
544
|
-
* Skip function component created by React.forwardRef
|
|
545
|
-
*/
|
|
546
|
-
SkipForwardRef: 1n << 65n,
|
|
547
|
-
/**
|
|
548
|
-
* Skip function component defined as array map argument
|
|
549
|
-
*/
|
|
550
|
-
SkipArrayMapArgument: 1n << 66n,
|
|
551
|
-
/**
|
|
552
|
-
* Skip function component defined on object method
|
|
553
|
-
*/
|
|
554
|
-
SkipObjectMethod: 1n << 67n,
|
|
555
|
-
/**
|
|
556
|
-
* Skip function component defined on class method
|
|
557
|
-
*/
|
|
558
|
-
SkipClassMethod: 1n << 68n,
|
|
559
|
-
/**
|
|
560
|
-
* Skip function component defined on class property
|
|
561
|
-
*/
|
|
562
|
-
SkipClassProperty: 1n << 69n
|
|
631
|
+
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/component/component-detection-hint.ts
|
|
634
|
+
/**
|
|
635
|
+
* Hints for component collector
|
|
636
|
+
*/
|
|
637
|
+
const ComponentDetectionHint = {
|
|
638
|
+
...JSXDetectionHint,
|
|
639
|
+
SkipMemo: 1n << 64n,
|
|
640
|
+
SkipForwardRef: 1n << 65n,
|
|
641
|
+
SkipArrayMapArgument: 1n << 66n,
|
|
642
|
+
SkipObjectMethod: 1n << 67n,
|
|
643
|
+
SkipClassMethod: 1n << 68n,
|
|
644
|
+
SkipClassProperty: 1n << 69n
|
|
563
645
|
};
|
|
564
|
-
|
|
646
|
+
/**
|
|
647
|
+
* Default component detection hint
|
|
648
|
+
*/
|
|
649
|
+
const DEFAULT_COMPONENT_DETECTION_HINT = 0n | ComponentDetectionHint.SkipBooleanLiteral | ComponentDetectionHint.SkipEmptyArray | ComponentDetectionHint.SkipArrayMapArgument | ComponentDetectionHint.SkipNumberLiteral | ComponentDetectionHint.SkipStringLiteral | ComponentDetectionHint.SkipUndefined | ComponentDetectionHint.StrictArray | ComponentDetectionHint.StrictConditional | ComponentDetectionHint.StrictLogical;
|
|
650
|
+
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/component/component-is.ts
|
|
653
|
+
/**
|
|
654
|
+
* Check if a node is a React class component
|
|
655
|
+
* @param node The AST node to check
|
|
656
|
+
* @returns `true` if the node is a class component, `false` otherwise
|
|
657
|
+
*/
|
|
565
658
|
function isClassComponent(node) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
659
|
+
if ("superClass" in node && node.superClass != null) {
|
|
660
|
+
const re = /^(?:Pure)?Component$/u;
|
|
661
|
+
switch (true) {
|
|
662
|
+
case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
|
|
663
|
+
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Check if a node is a React PureComponent
|
|
670
|
+
* @param node The AST node to check
|
|
671
|
+
* @returns `true` if the node is a pure component, `false` otherwise
|
|
672
|
+
*/
|
|
577
673
|
function isPureComponent(node) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
return false;
|
|
674
|
+
if ("superClass" in node && node.superClass != null) {
|
|
675
|
+
const re = /^PureComponent$/u;
|
|
676
|
+
switch (true) {
|
|
677
|
+
case node.superClass.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.name);
|
|
678
|
+
case node.superClass.type === AST_NODE_TYPES.MemberExpression && node.superClass.property.type === AST_NODE_TYPES.Identifier: return re.test(node.superClass.property.name);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
588
682
|
}
|
|
683
|
+
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/component/component-render-method.ts
|
|
686
|
+
/**
|
|
687
|
+
* Check whether given node is a render method of a class component
|
|
688
|
+
* @example
|
|
689
|
+
* ```tsx
|
|
690
|
+
* class Component extends React.Component {
|
|
691
|
+
* renderHeader = () => <div />;
|
|
692
|
+
* renderFooter = () => <div />;
|
|
693
|
+
* }
|
|
694
|
+
* ```
|
|
695
|
+
* @param node The AST node to check
|
|
696
|
+
* @returns `true` if node is a render function, `false` if not
|
|
697
|
+
*/
|
|
589
698
|
function isRenderMethodLike(node) {
|
|
590
|
-
|
|
699
|
+
return AST.isMethodOrProperty(node) && node.key.type === AST_NODE_TYPES.Identifier && node.key.name.startsWith("render") && node.parent.parent.type === AST_NODE_TYPES.ClassDeclaration;
|
|
591
700
|
}
|
|
592
701
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/component/component-definition.ts
|
|
704
|
+
/**
|
|
705
|
+
* Function pattern matchers for different contexts
|
|
706
|
+
*/
|
|
707
|
+
const functionPatterns = {
|
|
708
|
+
classMethod: {
|
|
709
|
+
type: P.union(AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression),
|
|
710
|
+
parent: AST_NODE_TYPES.MethodDefinition
|
|
711
|
+
},
|
|
712
|
+
classProperty: {
|
|
713
|
+
type: P.union(AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression),
|
|
714
|
+
parent: AST_NODE_TYPES.Property
|
|
715
|
+
},
|
|
716
|
+
objectMethod: {
|
|
717
|
+
type: P.union(AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression),
|
|
718
|
+
parent: {
|
|
719
|
+
type: AST_NODE_TYPES.Property,
|
|
720
|
+
parent: { type: AST_NODE_TYPES.ObjectExpression }
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
/**
|
|
725
|
+
* Check whether given node is a function of a render method of a class component
|
|
726
|
+
* @example
|
|
727
|
+
* ```tsx
|
|
728
|
+
* class Component extends React.Component {
|
|
729
|
+
* renderHeader = () => <div />;
|
|
730
|
+
* renderFooter = () => <div />;
|
|
731
|
+
* }
|
|
732
|
+
* ```
|
|
733
|
+
* @param node The AST node to check
|
|
734
|
+
* @returns `true` if node is a render function, `false` if not
|
|
735
|
+
*/
|
|
611
736
|
function isFunctionOfRenderMethod(node) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
737
|
+
return isRenderMethodLike(node.parent) && isClassComponent(node.parent.parent.parent);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Checks if a function node should be excluded based on detection hints
|
|
741
|
+
* @param node The function node to check
|
|
742
|
+
* @param hint Component detection hints as bit flags
|
|
743
|
+
* @returns `true` if the function should be excluded, `false` otherwise
|
|
744
|
+
*/
|
|
745
|
+
function shouldExcludeBasedOnHint(node, hint) {
|
|
746
|
+
if (hint & ComponentDetectionHint.SkipObjectMethod && isMatching(functionPatterns.objectMethod)(node)) return true;
|
|
747
|
+
if (hint & ComponentDetectionHint.SkipClassMethod && isMatching(functionPatterns.classMethod)(node)) return true;
|
|
748
|
+
if (hint & ComponentDetectionHint.SkipClassProperty && isMatching(functionPatterns.classProperty)(node)) return true;
|
|
749
|
+
if (hint & ComponentDetectionHint.SkipArrayMapArgument && AST.isArrayMapCall(node.parent)) return true;
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Determines if a function node represents a valid React component definition
|
|
754
|
+
* @param context The rule context
|
|
755
|
+
* @param node The function node to check
|
|
756
|
+
* @param hint Component detection hints as bit flags
|
|
757
|
+
* @returns `true` if the node is a valid component definition, `false` otherwise
|
|
758
|
+
*/
|
|
759
|
+
function isComponentDefinition(context, node, hint) {
|
|
760
|
+
if (isChildrenOfCreateElement(context, node) || isFunctionOfRenderMethod(node)) return false;
|
|
761
|
+
if (shouldExcludeBasedOnHint(node, hint)) return false;
|
|
762
|
+
const significantParent = AST.findParentNode(node, AST.isOneOf([
|
|
763
|
+
AST_NODE_TYPES.JSXExpressionContainer,
|
|
764
|
+
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
765
|
+
AST_NODE_TYPES.FunctionExpression,
|
|
766
|
+
AST_NODE_TYPES.Property,
|
|
767
|
+
AST_NODE_TYPES.ClassBody
|
|
768
|
+
]));
|
|
769
|
+
return significantParent == null || significantParent.type !== AST_NODE_TYPES.JSXExpressionContainer;
|
|
644
770
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
771
|
+
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/utils/get-instance-id.ts
|
|
774
|
+
/**
|
|
775
|
+
* Gets the identifier node of an instance based on AST node relationships.
|
|
776
|
+
* Used for tracking where hooks or components are being assigned in the code.
|
|
777
|
+
* @param node The current AST node to evaluate
|
|
778
|
+
* @param prev The previous AST node in the traversal (used for context)
|
|
779
|
+
* @internal
|
|
780
|
+
*/
|
|
781
|
+
function getInstanceId(node, prev) {
|
|
782
|
+
switch (true) {
|
|
783
|
+
case node.type === AST_NODE_TYPES.VariableDeclarator && node.init === prev: return node.id;
|
|
784
|
+
case node.type === AST_NODE_TYPES.AssignmentExpression && node.right === prev: return node.left;
|
|
785
|
+
case node.type === AST_NODE_TYPES.PropertyDefinition && node.value === prev: return node.key;
|
|
786
|
+
case node.type === AST_NODE_TYPES.BlockStatement || node.type === AST_NODE_TYPES.Program || node.parent === node: return unit;
|
|
787
|
+
default: return getInstanceId(node.parent, node);
|
|
788
|
+
}
|
|
648
789
|
}
|
|
790
|
+
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/utils/is-from-react.ts
|
|
793
|
+
/**
|
|
794
|
+
* Get the arguments of a require expression
|
|
795
|
+
* @param node The node to match
|
|
796
|
+
* @returns The require expression arguments or undefined if the node is not a require expression
|
|
797
|
+
*/
|
|
798
|
+
function getRequireExpressionArguments(node) {
|
|
799
|
+
return match(node).with({
|
|
800
|
+
type: AST_NODE_TYPES.CallExpression,
|
|
801
|
+
arguments: P.select(),
|
|
802
|
+
callee: {
|
|
803
|
+
type: AST_NODE_TYPES.Identifier,
|
|
804
|
+
name: "require"
|
|
805
|
+
}
|
|
806
|
+
}, identity).with({
|
|
807
|
+
type: AST_NODE_TYPES.MemberExpression,
|
|
808
|
+
object: P.select()
|
|
809
|
+
}, getRequireExpressionArguments).otherwise(() => null);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Check if an identifier name is initialized from react
|
|
813
|
+
* @param name The top-level identifier's name
|
|
814
|
+
* @param importSource The import source to check against
|
|
815
|
+
* @param initialScope Initial scope to search for the identifier
|
|
816
|
+
* @returns Whether the identifier name is initialized from react
|
|
817
|
+
* @internal
|
|
818
|
+
*/
|
|
819
|
+
function isInitializedFromReact(name, importSource, initialScope) {
|
|
820
|
+
if (name.toLowerCase() === "react") return true;
|
|
821
|
+
const latestDef = VAR.findVariable(name, initialScope)?.defs.at(-1);
|
|
822
|
+
if (latestDef == null) return false;
|
|
823
|
+
const { node, parent } = latestDef;
|
|
824
|
+
if (node.type === AST_NODE_TYPES.VariableDeclarator && node.init != null) {
|
|
825
|
+
const { init } = node;
|
|
826
|
+
if (init.type === AST_NODE_TYPES.MemberExpression && init.object.type === AST_NODE_TYPES.Identifier) return isInitializedFromReact(init.object.name, importSource, initialScope);
|
|
827
|
+
if (init.type === AST_NODE_TYPES.Identifier) return isInitializedFromReact(init.name, importSource, initialScope);
|
|
828
|
+
const arg0 = getRequireExpressionArguments(init)?.[0];
|
|
829
|
+
if (arg0 == null || !AST.isLiteral(arg0, "string")) return false;
|
|
830
|
+
return arg0.value === importSource || arg0.value.startsWith(`${importSource}/`);
|
|
831
|
+
}
|
|
832
|
+
return parent?.type === AST_NODE_TYPES.ImportDeclaration && parent.source.value === importSource;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/utils/is-instance-id-equal.ts
|
|
837
|
+
/** @internal */
|
|
838
|
+
function isInstanceIdEqual(context, a, b) {
|
|
839
|
+
return AST.isNodeEqual(a, b) || VAR.isNodeValueEqual(a, b, [context.sourceCode.getScope(a), context.sourceCode.getScope(b)]);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
//#endregion
|
|
843
|
+
//#region src/component/component-wrapper.ts
|
|
844
|
+
/**
|
|
845
|
+
* Check if the node is a call expression for a component wrapper
|
|
846
|
+
* @param context The ESLint rule context
|
|
847
|
+
* @param node The node to check
|
|
848
|
+
* @returns `true` if the node is a call expression for a component wrapper
|
|
849
|
+
*/
|
|
850
|
+
function isComponentWrapperCall(context, node) {
|
|
851
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
852
|
+
return isMemoCall(context, node) || isForwardRefCall(context, node);
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Check if the node is a call expression for a component wrapper loosely
|
|
856
|
+
* @param context The ESLint rule context
|
|
857
|
+
* @param node The node to check
|
|
858
|
+
* @returns `true` if the node is a call expression for a component wrapper loosely
|
|
859
|
+
*/
|
|
649
860
|
function isComponentWrapperCallLoose(context, node) {
|
|
650
|
-
|
|
651
|
-
|
|
861
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
862
|
+
return isComponentWrapperCall(context, node) || isUseCallbackCall(node);
|
|
652
863
|
}
|
|
653
864
|
|
|
654
|
-
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/component/component-id.ts
|
|
655
867
|
function getFunctionComponentId(context, node) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
PureComponent: 1n << 0n,
|
|
674
|
-
CreateElement: 1n << 1n,
|
|
675
|
-
Memo: 1n << 2n,
|
|
676
|
-
ForwardRef: 1n << 3n,
|
|
677
|
-
Async: 1n << 4n
|
|
868
|
+
const functionId = AST.getFunctionId(node);
|
|
869
|
+
if (functionId != null) return functionId;
|
|
870
|
+
const { parent } = node;
|
|
871
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.VariableDeclarator && parent.parent.id.type === AST_NODE_TYPES.Identifier) return parent.parent.id;
|
|
872
|
+
if (parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent) && parent.parent.type === AST_NODE_TYPES.CallExpression && isComponentWrapperCallLoose(context, parent.parent) && parent.parent.parent.type === AST_NODE_TYPES.VariableDeclarator && parent.parent.parent.id.type === AST_NODE_TYPES.Identifier) return parent.parent.parent.id;
|
|
873
|
+
return unit;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/component/component-flag.ts
|
|
878
|
+
const ComponentFlag = {
|
|
879
|
+
None: 0n,
|
|
880
|
+
PureComponent: 1n << 0n,
|
|
881
|
+
CreateElement: 1n << 1n,
|
|
882
|
+
Memo: 1n << 2n,
|
|
883
|
+
ForwardRef: 1n << 3n,
|
|
884
|
+
Async: 1n << 4n
|
|
678
885
|
};
|
|
679
886
|
|
|
680
|
-
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/component/component-init-path.ts
|
|
681
889
|
function getComponentFlagFromInitPath(initPath) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (initPath != null && AST14.hasCallInFunctionInitPath("forwardRef", initPath)) {
|
|
687
|
-
flag |= ComponentFlag.ForwardRef;
|
|
688
|
-
}
|
|
689
|
-
return flag;
|
|
890
|
+
let flag = ComponentFlag.None;
|
|
891
|
+
if (initPath != null && AST.hasCallInFunctionInitPath("memo", initPath)) flag |= ComponentFlag.Memo;
|
|
892
|
+
if (initPath != null && AST.hasCallInFunctionInitPath("forwardRef", initPath)) flag |= ComponentFlag.ForwardRef;
|
|
893
|
+
return flag;
|
|
690
894
|
}
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/component/component-name.ts
|
|
691
898
|
function isComponentName(name) {
|
|
692
|
-
|
|
899
|
+
return RE_COMPONENT_NAME.test(name);
|
|
693
900
|
}
|
|
694
901
|
function isComponentNameLoose(name) {
|
|
695
|
-
|
|
902
|
+
return RE_COMPONENT_NAME_LOOSE.test(name);
|
|
696
903
|
}
|
|
697
904
|
function getComponentNameFromId(id) {
|
|
698
|
-
|
|
699
|
-
|
|
905
|
+
if (id == null) return unit;
|
|
906
|
+
return Array.isArray(id) ? id.map((n) => n.name).join(".") : id.name;
|
|
700
907
|
}
|
|
701
908
|
function hasNoneOrLooseComponentName(context, fn) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
909
|
+
const id = getFunctionComponentId(context, fn);
|
|
910
|
+
if (id == null) return true;
|
|
911
|
+
const name = Array.isArray(id) ? id.at(-1)?.name : id.name;
|
|
912
|
+
return name != null && isComponentNameLoose(name);
|
|
706
913
|
}
|
|
707
914
|
|
|
708
|
-
|
|
915
|
+
//#endregion
|
|
916
|
+
//#region src/component/component-collector.ts
|
|
917
|
+
/**
|
|
918
|
+
* Get a ctx and listeners for the rule to collect function components
|
|
919
|
+
* @param context The ESLint rule context
|
|
920
|
+
* @param options The options to use
|
|
921
|
+
* @returns The component collector
|
|
922
|
+
*/
|
|
709
923
|
function useComponentCollector(context, options = {}) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
924
|
+
const { collectDisplayName = false, collectHookCalls = false, hint = DEFAULT_COMPONENT_DETECTION_HINT } = options;
|
|
925
|
+
const components = /* @__PURE__ */ new Map();
|
|
926
|
+
const functionEntries = [];
|
|
927
|
+
const getCurrentEntry = () => functionEntries.at(-1);
|
|
928
|
+
const onFunctionEnter = (node) => {
|
|
929
|
+
const key = getId();
|
|
930
|
+
functionEntries.push({
|
|
931
|
+
key,
|
|
932
|
+
node,
|
|
933
|
+
hookCalls: [],
|
|
934
|
+
isComponent: false
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
const onFunctionExit = () => {
|
|
938
|
+
const entry = functionEntries.at(-1);
|
|
939
|
+
if (entry == null) return;
|
|
940
|
+
if (!entry.isComponent) return functionEntries.pop();
|
|
941
|
+
const rets = AST.getNestedReturnStatements(entry.node.body);
|
|
942
|
+
for (let i = rets.length - 1; i >= 0; i--) {
|
|
943
|
+
const ret = rets[i];
|
|
944
|
+
if (ret == null) continue;
|
|
945
|
+
if (context.sourceCode.getScope(ret).block === entry.node && ret.argument != null && !isJsxLike(context.sourceCode, ret.argument, hint)) {
|
|
946
|
+
components.delete(entry.key);
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return functionEntries.pop();
|
|
951
|
+
};
|
|
952
|
+
const ctx = {
|
|
953
|
+
getAllComponents(node) {
|
|
954
|
+
return components;
|
|
955
|
+
},
|
|
956
|
+
getCurrentEntries() {
|
|
957
|
+
return [...functionEntries];
|
|
958
|
+
},
|
|
959
|
+
getCurrentEntry
|
|
960
|
+
};
|
|
961
|
+
const listeners = {
|
|
962
|
+
":function[type]": onFunctionEnter,
|
|
963
|
+
":function[type]:exit": onFunctionExit,
|
|
964
|
+
"ArrowFunctionExpression[body.type!='BlockStatement']"() {
|
|
965
|
+
const entry = getCurrentEntry();
|
|
966
|
+
if (entry == null) return;
|
|
967
|
+
const { body } = entry.node;
|
|
968
|
+
if (!(hasNoneOrLooseComponentName(context, entry.node) && isJsxLike(context.sourceCode, body, hint) && isComponentDefinition(context, entry.node, hint))) return;
|
|
969
|
+
const initPath = AST.getFunctionInitPath(entry.node);
|
|
970
|
+
const id = getFunctionComponentId(context, entry.node);
|
|
971
|
+
const name = getComponentNameFromId(id);
|
|
972
|
+
const key = getId();
|
|
973
|
+
components.set(key, {
|
|
974
|
+
id,
|
|
975
|
+
key,
|
|
976
|
+
kind: "function",
|
|
977
|
+
name,
|
|
978
|
+
node: entry.node,
|
|
979
|
+
displayName: unit,
|
|
980
|
+
flag: getComponentFlagFromInitPath(initPath),
|
|
981
|
+
hint,
|
|
982
|
+
hookCalls: entry.hookCalls,
|
|
983
|
+
initPath
|
|
984
|
+
});
|
|
985
|
+
},
|
|
986
|
+
...collectDisplayName ? { [SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node) {
|
|
987
|
+
const { left, right } = node;
|
|
988
|
+
if (left.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
989
|
+
const componentName = left.object.type === AST_NODE_TYPES.Identifier ? left.object.name : unit;
|
|
990
|
+
const component = [...components.values()].findLast(({ name }) => name != null && name === componentName);
|
|
991
|
+
if (component == null) return;
|
|
992
|
+
component.displayName = right;
|
|
993
|
+
} } : {},
|
|
994
|
+
...collectHookCalls ? { "CallExpression[type]:exit"(node) {
|
|
995
|
+
if (!isReactHookCall(node)) return;
|
|
996
|
+
const entry = getCurrentEntry();
|
|
997
|
+
if (entry == null) return;
|
|
998
|
+
entry.hookCalls.push(node);
|
|
999
|
+
} } : {},
|
|
1000
|
+
"ReturnStatement[type]"(node) {
|
|
1001
|
+
const entry = getCurrentEntry();
|
|
1002
|
+
if (entry == null) return;
|
|
1003
|
+
if (!(hasNoneOrLooseComponentName(context, entry.node) && isJsxLike(context.sourceCode, node.argument, hint) && isComponentDefinition(context, entry.node, hint))) return;
|
|
1004
|
+
entry.isComponent = true;
|
|
1005
|
+
const initPath = AST.getFunctionInitPath(entry.node);
|
|
1006
|
+
const id = getFunctionComponentId(context, entry.node);
|
|
1007
|
+
const name = getComponentNameFromId(id);
|
|
1008
|
+
components.set(entry.key, {
|
|
1009
|
+
id,
|
|
1010
|
+
key: entry.key,
|
|
1011
|
+
kind: "function",
|
|
1012
|
+
name,
|
|
1013
|
+
node: entry.node,
|
|
1014
|
+
displayName: unit,
|
|
1015
|
+
flag: getComponentFlagFromInitPath(initPath),
|
|
1016
|
+
hint,
|
|
1017
|
+
hookCalls: entry.hookCalls,
|
|
1018
|
+
initPath
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
return {
|
|
1023
|
+
ctx,
|
|
1024
|
+
listeners
|
|
1025
|
+
};
|
|
812
1026
|
}
|
|
1027
|
+
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/component/component-collector-legacy.ts
|
|
1030
|
+
/**
|
|
1031
|
+
* Get a ctx and listeners object for the rule to collect class components
|
|
1032
|
+
* @returns The context and listeners for the rule
|
|
1033
|
+
*/
|
|
813
1034
|
function useComponentCollectorLegacy() {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
);
|
|
844
|
-
};
|
|
845
|
-
const listeners = {
|
|
846
|
-
"ClassDeclaration[type]": collect,
|
|
847
|
-
"ClassExpression[type]": collect
|
|
848
|
-
};
|
|
849
|
-
return { ctx, listeners };
|
|
850
|
-
}
|
|
851
|
-
function isComponentDidCatch(node) {
|
|
852
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentDidCatch";
|
|
853
|
-
}
|
|
854
|
-
function isComponentDidMount(node) {
|
|
855
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentDidMount";
|
|
856
|
-
}
|
|
857
|
-
function isComponentDidUpdate(node) {
|
|
858
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentDidUpdate";
|
|
859
|
-
}
|
|
860
|
-
function isComponentWillMount(node) {
|
|
861
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentWillMount";
|
|
862
|
-
}
|
|
863
|
-
function isComponentWillReceiveProps(node) {
|
|
864
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentWillReceiveProps";
|
|
865
|
-
}
|
|
866
|
-
function isComponentWillUnmount(node) {
|
|
867
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentWillUnmount";
|
|
868
|
-
}
|
|
869
|
-
function isComponentWillUpdate(node) {
|
|
870
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "componentWillUpdate";
|
|
871
|
-
}
|
|
872
|
-
function isGetChildContext(node) {
|
|
873
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "getChildContext";
|
|
874
|
-
}
|
|
875
|
-
function isGetDefaultProps(node) {
|
|
876
|
-
return AST14.isMethodOrProperty(node) && node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "getDefaultProps";
|
|
877
|
-
}
|
|
878
|
-
function isGetInitialState(node) {
|
|
879
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "getInitialState";
|
|
880
|
-
}
|
|
881
|
-
function isGetSnapshotBeforeUpdate(node) {
|
|
882
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "getSnapshotBeforeUpdate";
|
|
883
|
-
}
|
|
884
|
-
function isShouldComponentUpdate(node) {
|
|
885
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "shouldComponentUpdate";
|
|
886
|
-
}
|
|
887
|
-
function isUnsafeComponentWillMount(node) {
|
|
888
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "UNSAFE_componentWillMount";
|
|
889
|
-
}
|
|
890
|
-
function isUnsafeComponentWillReceiveProps(node) {
|
|
891
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "UNSAFE_componentWillReceiveProps";
|
|
892
|
-
}
|
|
893
|
-
function isUnsafeComponentWillUpdate(node) {
|
|
894
|
-
return AST14.isMethodOrProperty(node) && !node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "UNSAFE_componentWillUpdate";
|
|
895
|
-
}
|
|
896
|
-
function isGetDerivedStateFromProps(node) {
|
|
897
|
-
return AST14.isMethodOrProperty(node) && node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "getDerivedStateFromProps";
|
|
898
|
-
}
|
|
899
|
-
function isGetDerivedStateFromError(node) {
|
|
900
|
-
return AST14.isMethodOrProperty(node) && node.static && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "getDerivedStateFromError";
|
|
901
|
-
}
|
|
902
|
-
var ComponentPhaseRelevance = birecord({
|
|
903
|
-
mount: "unmount",
|
|
904
|
-
setup: "cleanup"
|
|
905
|
-
});
|
|
906
|
-
var isInversePhase = dual(2, (a, b) => ComponentPhaseRelevance.get(a) === b);
|
|
907
|
-
function isRenderLike(node) {
|
|
908
|
-
return AST14.isMethodOrProperty(node) && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "render" && node.parent.parent.type === AST_NODE_TYPES.ClassDeclaration;
|
|
909
|
-
}
|
|
910
|
-
function isFunctionOfRender(node) {
|
|
911
|
-
if (!isRenderLike(node.parent)) {
|
|
912
|
-
return false;
|
|
913
|
-
}
|
|
914
|
-
return isClassComponent(node.parent.parent.parent);
|
|
1035
|
+
const components = /* @__PURE__ */ new Map();
|
|
1036
|
+
const ctx = { getAllComponents(node) {
|
|
1037
|
+
return components;
|
|
1038
|
+
} };
|
|
1039
|
+
const collect = (node) => {
|
|
1040
|
+
if (!isClassComponent(node)) return;
|
|
1041
|
+
const id = AST.getClassId(node);
|
|
1042
|
+
const key = getId();
|
|
1043
|
+
const flag = isPureComponent(node) ? ComponentFlag.PureComponent : ComponentFlag.None;
|
|
1044
|
+
components.set(key, {
|
|
1045
|
+
id,
|
|
1046
|
+
key,
|
|
1047
|
+
kind: "class",
|
|
1048
|
+
name: id?.name,
|
|
1049
|
+
node,
|
|
1050
|
+
displayName: unit,
|
|
1051
|
+
flag,
|
|
1052
|
+
hint: 0n,
|
|
1053
|
+
methods: []
|
|
1054
|
+
});
|
|
1055
|
+
};
|
|
1056
|
+
return {
|
|
1057
|
+
ctx,
|
|
1058
|
+
listeners: {
|
|
1059
|
+
"ClassDeclaration[type]": collect,
|
|
1060
|
+
"ClassExpression[type]": collect
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
915
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
//#endregion
|
|
1066
|
+
//#region src/component/component-method.ts
|
|
1067
|
+
/**
|
|
1068
|
+
* Create a lifecycle method checker function
|
|
1069
|
+
* @param methodName The lifecycle method name
|
|
1070
|
+
* @param isStatic Whether the method is static
|
|
1071
|
+
*/
|
|
1072
|
+
function createLifecycleChecker(methodName, isStatic) {
|
|
1073
|
+
return function(node) {
|
|
1074
|
+
return AST.isMethodOrProperty(node) && node.static === isStatic && node.key.type === AST_NODE_TYPES.Identifier && node.key.name === methodName;
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
const isRender = createLifecycleChecker("render", false);
|
|
1078
|
+
const isComponentDidCatch = createLifecycleChecker("componentDidCatch", false);
|
|
1079
|
+
const isComponentDidMount = createLifecycleChecker("componentDidMount", false);
|
|
1080
|
+
const isComponentDidUpdate = createLifecycleChecker("componentDidUpdate", false);
|
|
1081
|
+
const isComponentWillMount = createLifecycleChecker("componentWillMount", false);
|
|
1082
|
+
const isComponentWillReceiveProps = createLifecycleChecker("componentWillReceiveProps", false);
|
|
1083
|
+
const isComponentWillUnmount = createLifecycleChecker("componentWillUnmount", false);
|
|
1084
|
+
const isComponentWillUpdate = createLifecycleChecker("componentWillUpdate", false);
|
|
1085
|
+
const isGetChildContext = createLifecycleChecker("getChildContext", false);
|
|
1086
|
+
const isGetInitialState = createLifecycleChecker("getInitialState", false);
|
|
1087
|
+
const isGetSnapshotBeforeUpdate = createLifecycleChecker("getSnapshotBeforeUpdate", false);
|
|
1088
|
+
const isShouldComponentUpdate = createLifecycleChecker("shouldComponentUpdate", false);
|
|
1089
|
+
const isUnsafeComponentWillMount = createLifecycleChecker("UNSAFE_componentWillMount", false);
|
|
1090
|
+
const isUnsafeComponentWillReceiveProps = createLifecycleChecker("UNSAFE_componentWillReceiveProps", false);
|
|
1091
|
+
const isUnsafeComponentWillUpdate = createLifecycleChecker("UNSAFE_componentWillUpdate", false);
|
|
1092
|
+
const isGetDefaultProps = createLifecycleChecker("getDefaultProps", true);
|
|
1093
|
+
const isGetDerivedStateFromProps = createLifecycleChecker("getDerivedStateFromProps", true);
|
|
1094
|
+
const isGetDerivedStateFromError = createLifecycleChecker("getDerivedStateFromError", true);
|
|
1095
|
+
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/component/component-phase.ts
|
|
1098
|
+
const ComponentPhaseRelevance = birecord({
|
|
1099
|
+
mount: "unmount",
|
|
1100
|
+
setup: "cleanup"
|
|
1101
|
+
});
|
|
1102
|
+
const isInversePhase = dual(2, (a, b) => ComponentPhaseRelevance.get(a) === b);
|
|
1103
|
+
|
|
1104
|
+
//#endregion
|
|
1105
|
+
//#region src/component/component-render-prop.ts
|
|
1106
|
+
/**
|
|
1107
|
+
* Unsafe check whether given node is a render function
|
|
1108
|
+
* ```tsx
|
|
1109
|
+
* const renderRow = () => <div />
|
|
1110
|
+
* ` ^^^^^^^^^^^^`
|
|
1111
|
+
* _ = <Component renderRow={() => <div />} />
|
|
1112
|
+
* ` ^^^^^^^^^^^^^ `
|
|
1113
|
+
* ```
|
|
1114
|
+
* @param context The rule context
|
|
1115
|
+
* @param node The AST node to check
|
|
1116
|
+
* @returns `true` if node is a render function, `false` if not
|
|
1117
|
+
*/
|
|
916
1118
|
function isRenderFunctionLoose(context, node) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1119
|
+
const { body, parent } = node;
|
|
1120
|
+
if (AST.getFunctionId(node)?.name.startsWith("render")) return parent.type === AST_NODE_TYPES.JSXExpressionContainer && parent.parent.type === AST_NODE_TYPES.JSXAttribute && parent.parent.name.type === AST_NODE_TYPES.JSXIdentifier && parent.parent.name.name.startsWith("render");
|
|
1121
|
+
return isJsxLike(context.sourceCode, body, JSXDetectionHint.SkipNullLiteral | JSXDetectionHint.SkipUndefined | JSXDetectionHint.StrictLogical | JSXDetectionHint.StrictConditional);
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Unsafe check whether given JSXAttribute is a render prop
|
|
1125
|
+
* ```tsx
|
|
1126
|
+
* _ = <Component renderRow={() => <div />} />
|
|
1127
|
+
* ` ^^^^^^^^^^^^^^^^^^^^^^^^^ `
|
|
1128
|
+
* ```
|
|
1129
|
+
* @param context The rule context
|
|
1130
|
+
* @param node The AST node to check
|
|
1131
|
+
* @returns `true` if node is a render prop, `false` if not
|
|
1132
|
+
*/
|
|
927
1133
|
function isRenderPropLoose(context, node) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1134
|
+
if (node.name.type !== AST_NODE_TYPES.JSXIdentifier) return false;
|
|
1135
|
+
return node.name.name.startsWith("render") && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && AST.isFunction(node.value.expression) && isRenderFunctionLoose(context, node.value.expression);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Unsafe check whether given node is declared directly inside a render property
|
|
1139
|
+
* ```tsx
|
|
1140
|
+
* const rows = { render: () => <div /> }
|
|
1141
|
+
* ` ^^^^^^^^^^^^^ `
|
|
1142
|
+
* _ = <Component rows={ [{ render: () => <div /> }] } />
|
|
1143
|
+
* ` ^^^^^^^^^^^^^ `
|
|
1144
|
+
* ```
|
|
1145
|
+
* @internal
|
|
1146
|
+
* @param node The AST node to check
|
|
1147
|
+
* @returns `true` if component is declared inside a render property, `false` if not
|
|
1148
|
+
*/
|
|
933
1149
|
function isDirectValueOfRenderPropertyLoose(node) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
1150
|
+
const matching = (node$1) => {
|
|
1151
|
+
return node$1.type === AST_NODE_TYPES.Property && node$1.key.type === AST_NODE_TYPES.Identifier && node$1.key.name.startsWith("render");
|
|
1152
|
+
};
|
|
1153
|
+
return matching(node) || node.parent != null && matching(node.parent);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Unsafe check whether given node is declared inside a render prop
|
|
1157
|
+
* ```tsx
|
|
1158
|
+
* _ = <Component renderRow={"node"} />
|
|
1159
|
+
* ` ^^^^^^ `
|
|
1160
|
+
* _ = <Component rows={ [{ render: "node" }] } />
|
|
1161
|
+
* ` ^^^^^^ `
|
|
1162
|
+
* ```
|
|
1163
|
+
* @param node The AST node to check
|
|
1164
|
+
* @returns `true` if component is declared inside a render prop, `false` if not
|
|
1165
|
+
*/
|
|
939
1166
|
function isDeclaredInRenderPropLoose(node) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
if (parent?.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
945
|
-
return false;
|
|
946
|
-
}
|
|
947
|
-
return parent.name.type === AST_NODE_TYPES.JSXIdentifier && parent.name.name.startsWith("render");
|
|
1167
|
+
if (isDirectValueOfRenderPropertyLoose(node)) return true;
|
|
1168
|
+
const parent = AST.findParentNode(node, AST.is(AST_NODE_TYPES.JSXExpressionContainer))?.parent;
|
|
1169
|
+
if (parent?.type !== AST_NODE_TYPES.JSXAttribute) return false;
|
|
1170
|
+
return parent.name.type === AST_NODE_TYPES.JSXIdentifier && parent.name.name.startsWith("render");
|
|
948
1171
|
}
|
|
1172
|
+
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/component/component-state.ts
|
|
949
1175
|
function isThisSetState(node) {
|
|
950
|
-
|
|
951
|
-
|
|
1176
|
+
const { callee } = node;
|
|
1177
|
+
return callee.type === AST_NODE_TYPES.MemberExpression && AST.isThisExpression(callee.object) && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "setState";
|
|
952
1178
|
}
|
|
953
1179
|
function isAssignmentToThisState(node) {
|
|
954
|
-
|
|
955
|
-
|
|
1180
|
+
const { left } = node;
|
|
1181
|
+
return left.type === AST_NODE_TYPES.MemberExpression && AST.isThisExpression(left.object) && AST.getPropertyName(left.property) === "state";
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
//#endregion
|
|
1185
|
+
//#region src/type/type-is.ts
|
|
1186
|
+
/** @internal */
|
|
1187
|
+
const isAnyType = (type) => isTypeFlagSet(type, ts.TypeFlags.TypeParameter | ts.TypeFlags.Any);
|
|
1188
|
+
/** @internal */
|
|
1189
|
+
const isBigIntType = (type) => isTypeFlagSet(type, ts.TypeFlags.BigIntLike);
|
|
1190
|
+
/** @internal */
|
|
1191
|
+
const isBooleanType = (type) => isTypeFlagSet(type, ts.TypeFlags.BooleanLike);
|
|
1192
|
+
/** @internal */
|
|
1193
|
+
const isEnumType = (type) => isTypeFlagSet(type, ts.TypeFlags.EnumLike);
|
|
1194
|
+
/** @internal */
|
|
1195
|
+
const isFalsyBigIntType = (type) => type.isLiteral() && isMatching({ value: { base10Value: "0" } }, type);
|
|
1196
|
+
/** @internal */
|
|
1197
|
+
const isFalsyNumberType = (type) => type.isNumberLiteral() && type.value === 0;
|
|
1198
|
+
/** @internal */
|
|
1199
|
+
const isFalsyStringType = (type) => type.isStringLiteral() && type.value === "";
|
|
1200
|
+
/** @internal */
|
|
1201
|
+
const isNeverType = (type) => isTypeFlagSet(type, ts.TypeFlags.Never);
|
|
1202
|
+
/** @internal */
|
|
1203
|
+
const isNullishType = (type) => isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike);
|
|
1204
|
+
/** @internal */
|
|
1205
|
+
const isNumberType = (type) => isTypeFlagSet(type, ts.TypeFlags.NumberLike);
|
|
1206
|
+
/** @internal */
|
|
1207
|
+
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);
|
|
1208
|
+
/** @internal */
|
|
1209
|
+
const isStringType = (type) => isTypeFlagSet(type, ts.TypeFlags.StringLike);
|
|
1210
|
+
/** @internal */
|
|
1211
|
+
const isTruthyBigIntType = (type) => type.isLiteral() && isMatching({ value: { base10Value: P.not("0") } }, type);
|
|
1212
|
+
/** @internal */
|
|
1213
|
+
const isTruthyNumberType = (type) => type.isNumberLiteral() && type.value !== 0;
|
|
1214
|
+
/** @internal */
|
|
1215
|
+
const isTruthyStringType = (type) => type.isStringLiteral() && type.value !== "";
|
|
1216
|
+
/** @internal */
|
|
1217
|
+
const isUnknownType = (type) => isTypeFlagSet(type, ts.TypeFlags.Unknown);
|
|
1218
|
+
|
|
1219
|
+
//#endregion
|
|
1220
|
+
//#region src/type/type-variant.ts
|
|
1221
|
+
/**
|
|
1222
|
+
* Ported from https://github.com/typescript-eslint/typescript-eslint/blob/eb736bbfc22554694400e6a4f97051d845d32e0b/packages/eslint-plugin/src/rules/strict-boolean-expressions.ts#L826 with some enhancements
|
|
1223
|
+
* Get the variants of an array of types.
|
|
1224
|
+
* @param types The types to get the variants of
|
|
1225
|
+
* @returns The variants of the types
|
|
1226
|
+
* @internal
|
|
1227
|
+
*/
|
|
1228
|
+
function getTypeVariants(types) {
|
|
1229
|
+
const variants = /* @__PURE__ */ new Set();
|
|
1230
|
+
if (types.some(isUnknownType)) {
|
|
1231
|
+
variants.add("unknown");
|
|
1232
|
+
return variants;
|
|
1233
|
+
}
|
|
1234
|
+
if (types.some(isNullishType)) variants.add("nullish");
|
|
1235
|
+
const booleans = types.filter(isBooleanType);
|
|
1236
|
+
const boolean0 = booleans[0];
|
|
1237
|
+
if (booleans.length === 1 && boolean0 != null) {
|
|
1238
|
+
if (isFalseLiteralType(boolean0)) variants.add("falsy boolean");
|
|
1239
|
+
else if (isTrueLiteralType(boolean0)) variants.add("truthy boolean");
|
|
1240
|
+
} else if (booleans.length === 2) variants.add("boolean");
|
|
1241
|
+
const strings = types.filter(isStringType);
|
|
1242
|
+
if (strings.length > 0) {
|
|
1243
|
+
const evaluated = match(strings).when((types$1) => types$1.every(isTruthyStringType), () => "truthy string").when((types$1) => types$1.every(isFalsyStringType), () => "falsy string").otherwise(() => "string");
|
|
1244
|
+
variants.add(evaluated);
|
|
1245
|
+
}
|
|
1246
|
+
const bigints = types.filter(isBigIntType);
|
|
1247
|
+
if (bigints.length > 0) {
|
|
1248
|
+
const evaluated = match(bigints).when((types$1) => types$1.every(isTruthyBigIntType), () => "truthy bigint").when((types$1) => types$1.every(isFalsyBigIntType), () => "falsy bigint").otherwise(() => "bigint");
|
|
1249
|
+
variants.add(evaluated);
|
|
1250
|
+
}
|
|
1251
|
+
const numbers = types.filter(isNumberType);
|
|
1252
|
+
if (numbers.length > 0) {
|
|
1253
|
+
const evaluated = match(numbers).when((types$1) => types$1.every(isTruthyNumberType), () => "truthy number").when((types$1) => types$1.every(isFalsyNumberType), () => "falsy number").otherwise(() => "number");
|
|
1254
|
+
variants.add(evaluated);
|
|
1255
|
+
}
|
|
1256
|
+
if (types.some(isEnumType)) variants.add("enum");
|
|
1257
|
+
if (types.some(isObjectType)) variants.add("object");
|
|
1258
|
+
if (types.some(isAnyType)) variants.add("any");
|
|
1259
|
+
if (types.some(isNeverType)) variants.add("never");
|
|
1260
|
+
return variants;
|
|
956
1261
|
}
|
|
957
1262
|
|
|
958
|
-
|
|
1263
|
+
//#endregion
|
|
1264
|
+
export { ComponentDetectionHint, ComponentFlag, ComponentPhaseRelevance, DEFAULT_COMPONENT_DETECTION_HINT, DEFAULT_JSX_DETECTION_HINT, JSXDetectionHint, JsxEmit, REACT_BUILTIN_HOOK_NAMES, findParentAttribute, getAttribute, getAttributeName, getComponentFlagFromInitPath, getComponentNameFromId, getElementType, getFunctionComponentId, getInstanceId, getJsxConfigFromAnnotation, getJsxConfigFromContext, getTypeVariants, hasAnyAttribute, hasAttribute, hasEveryAttribute, hasNoneOrLooseComponentName, isAnyType, isAssignmentToThisState, isBigIntType, isBooleanType, isCaptureOwnerStack, isCaptureOwnerStackCall, isChildrenCount, isChildrenCountCall, isChildrenForEach, isChildrenForEachCall, isChildrenMap, isChildrenMapCall, isChildrenOfCreateElement, isChildrenOnly, isChildrenOnlyCall, isChildrenToArray, isChildrenToArrayCall, isClassComponent, isCloneElement, isCloneElementCall, isComponentDefinition, isComponentDidCatch, isComponentDidMount, isComponentDidUpdate, isComponentName, isComponentNameLoose, isComponentWillMount, isComponentWillReceiveProps, isComponentWillUnmount, isComponentWillUpdate, isComponentWrapperCall, isComponentWrapperCallLoose, isCreateContext, isCreateContextCall, isCreateElement, isCreateElementCall, isCreateRef, isCreateRefCall, isDeclaredInRenderPropLoose, isDirectValueOfRenderPropertyLoose, isEnumType, isFalseLiteralType, isFalsyBigIntType, isFalsyNumberType, isFalsyStringType, isForwardRef, isForwardRefCall, isFragmentElement, isFunctionOfRenderMethod, isGetChildContext, isGetDefaultProps, isGetDerivedStateFromError, isGetDerivedStateFromProps, isGetInitialState, isGetSnapshotBeforeUpdate, isHostElement, isInitializedFromReact, isInstanceIdEqual, isInversePhase, isJsxLike, isJsxText, isLazy, isLazyCall, isMemo, isMemoCall, isNeverType, isNullishType, isNumberType, isObjectType, isPureComponent, isReactAPI, isReactAPICall, isReactHook, isReactHookCall, isReactHookCallWithName, isReactHookCallWithNameAlias, isReactHookId, isReactHookName, isRender, isRenderFunctionLoose, isRenderMethodLike, isRenderPropLoose, isShouldComponentUpdate, isStringType, isThisSetState, isTrueLiteralType, isTruthyBigIntType, isTruthyNumberType, isTruthyStringType, isUnknownType, isUnsafeComponentWillMount, isUnsafeComponentWillReceiveProps, isUnsafeComponentWillUpdate, isUseActionStateCall, isUseCall, isUseCallbackCall, isUseContextCall, isUseDebugValueCall, isUseDeferredValueCall, isUseEffectCall, isUseEffectLikeCall, isUseFormStatusCall, isUseIdCall, isUseImperativeHandleCall, isUseInsertionEffectCall, isUseLayoutEffectCall, isUseMemoCall, isUseOptimisticCall, isUseReducerCall, isUseRefCall, isUseStateCall, isUseSyncExternalStoreCall, isUseTransitionCall, resolveAttributeValue, stringifyJsx, useComponentCollector, useComponentCollectorLegacy, useHookCollector };
|