eslint-plugin-react-hooks-extra 1.12.5-next.6 → 1.13.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/index.d.mts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +209 -204
- package/dist/index.mjs +211 -206
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ export default [
|
|
|
31
31
|
},
|
|
32
32
|
rules: {
|
|
33
33
|
// react-hooks-extra recommended rules
|
|
34
|
-
"hooks-extra/
|
|
34
|
+
"hooks-extra/no-redundant-custom-hook": "warn",
|
|
35
35
|
"hooks-extra/no-direct-set-state-in-use-effect": "warn",
|
|
36
36
|
"hooks-extra/prefer-use-state-lazy-initialization": "warn",
|
|
37
37
|
},
|
|
@@ -41,10 +41,10 @@ export default [
|
|
|
41
41
|
|
|
42
42
|
## Rules
|
|
43
43
|
|
|
44
|
-
| Rule
|
|
45
|
-
|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `no-
|
|
50
|
-
| `prefer-use-state-lazy-initialization`
|
|
44
|
+
| Rule | Description | 💼 | 💭 | |
|
|
45
|
+
| :------------------------------------- | :------------------------------------------------------------------------ | :-: | :-: | :-: |
|
|
46
|
+
| `no-direct-set-state-in-use-effect` | Disallow direct calls to the `set` function of `useState` in `useEffect`. | ✔️ | | |
|
|
47
|
+
| `no-redundant-custom-hook` | Warns when custom Hooks that don't use other Hooks. | ✔️ | | |
|
|
48
|
+
| `no-unnecessary-use-callback` | Disallow unnecessary usage of `useCallback`. | ✔️ | | |
|
|
49
|
+
| `no-unnecessary-use-memo` | Disallow unnecessary usage of `useMemo`. | ✔️ | | |
|
|
50
|
+
| `prefer-use-state-lazy-initialization` | Warns function calls made inside `useState` calls. | 🚀 | | |
|
package/dist/index.d.mts
CHANGED
|
@@ -5,13 +5,17 @@ declare const meta: {
|
|
|
5
5
|
readonly version: string;
|
|
6
6
|
};
|
|
7
7
|
declare const rules: {
|
|
8
|
-
readonly "ensure-custom-hooks-using-other-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"ensureCustomHooksUsingOtherHooks", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
-
readonly "ensure-use-callback-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ensureUseCallbackHasNonEmptyDeps", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
-
readonly "ensure-use-memo-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ensureUseMemoHasNonEmptyDeps", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
8
|
readonly "no-direct-set-state-in-use-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"noDirectSetStateInUseEffect", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
readonly "no-redundant-custom-hook": _typescript_eslint_utils_ts_eslint.RuleModule<"noRedundantCustomHook", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
+
readonly "no-unnecessary-use-callback": _typescript_eslint_utils_ts_eslint.RuleModule<"noUnnecessaryUseCallback", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
|
+
readonly "no-unnecessary-use-memo": _typescript_eslint_utils_ts_eslint.RuleModule<"noUnnecessaryUseMemo", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
|
+
readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseStateLazyInitialization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
13
|
+
/** @deprecated Use `no-redundant-custom-hook` instead */
|
|
14
|
+
readonly "ensure-custom-hooks-using-other-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"noRedundantCustomHook", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
15
|
+
/** @deprecated Use `no-unnecessary-use-memo` instead */
|
|
16
|
+
readonly "ensure-use-memo-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"noUnnecessaryUseMemo", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
17
|
/** @deprecated Use `no-direct-set-state-in-use-effect` instead */
|
|
13
18
|
readonly "no-direct-set-state-in-use-layout-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"noDirectSetStateInUseEffect", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
14
|
-
readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseStateLazyInitialization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export { meta, rules };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,13 +5,17 @@ declare const meta: {
|
|
|
5
5
|
readonly version: string;
|
|
6
6
|
};
|
|
7
7
|
declare const rules: {
|
|
8
|
-
readonly "ensure-custom-hooks-using-other-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"ensureCustomHooksUsingOtherHooks", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
-
readonly "ensure-use-callback-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ensureUseCallbackHasNonEmptyDeps", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
-
readonly "ensure-use-memo-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ensureUseMemoHasNonEmptyDeps", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
8
|
readonly "no-direct-set-state-in-use-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"noDirectSetStateInUseEffect", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
readonly "no-redundant-custom-hook": _typescript_eslint_utils_ts_eslint.RuleModule<"noRedundantCustomHook", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
+
readonly "no-unnecessary-use-callback": _typescript_eslint_utils_ts_eslint.RuleModule<"noUnnecessaryUseCallback", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
|
+
readonly "no-unnecessary-use-memo": _typescript_eslint_utils_ts_eslint.RuleModule<"noUnnecessaryUseMemo", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
|
+
readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseStateLazyInitialization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
13
|
+
/** @deprecated Use `no-redundant-custom-hook` instead */
|
|
14
|
+
readonly "ensure-custom-hooks-using-other-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"noRedundantCustomHook", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
15
|
+
/** @deprecated Use `no-unnecessary-use-memo` instead */
|
|
16
|
+
readonly "ensure-use-memo-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"noUnnecessaryUseMemo", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
17
|
/** @deprecated Use `no-direct-set-state-in-use-effect` instead */
|
|
13
18
|
readonly "no-direct-set-state-in-use-layout-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"noDirectSetStateInUseEffect", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
14
|
-
readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"preferUseStateLazyInitialization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export { meta, rules };
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var ast = require('@eslint-react/ast');
|
|
3
4
|
var core = require('@eslint-react/core');
|
|
4
5
|
var shared = require('@eslint-react/shared');
|
|
5
|
-
var ast = require('@eslint-react/ast');
|
|
6
6
|
var tools = require('@eslint-react/tools');
|
|
7
7
|
var _var = require('@eslint-react/var');
|
|
8
8
|
var types = require('@typescript-eslint/types');
|
|
9
|
-
var astUtils = require('@typescript-eslint/utils/ast-utils');
|
|
10
9
|
var tsPattern = require('ts-pattern');
|
|
10
|
+
var astUtils = require('@typescript-eslint/utils/ast-utils');
|
|
11
11
|
|
|
12
12
|
// package.json
|
|
13
13
|
var name = "eslint-plugin-react-hooks-extra";
|
|
14
|
-
var version = "1.
|
|
14
|
+
var version = "1.13.0-beta.1";
|
|
15
15
|
var createRule = shared.createRuleForPlugin("hooks-extra");
|
|
16
16
|
function isFromHookCall(name2, context, settings, predicate = tools.F.constTrue) {
|
|
17
17
|
const hookAlias = settings.additionalHooks?.[name2] ?? [];
|
|
@@ -92,201 +92,8 @@ function isVariableDeclaratorFromHookCall(node) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// src/rules/
|
|
96
|
-
var RULE_NAME = "
|
|
97
|
-
var ensure_custom_hooks_using_other_hooks_default = createRule({
|
|
98
|
-
meta: {
|
|
99
|
-
type: "problem",
|
|
100
|
-
docs: {
|
|
101
|
-
description: "enforce custom hooks using other hooks"
|
|
102
|
-
},
|
|
103
|
-
messages: {
|
|
104
|
-
ensureCustomHooksUsingOtherHooks: "A custom hook '{{name}}' should use other hooks."
|
|
105
|
-
},
|
|
106
|
-
schema: []
|
|
107
|
-
},
|
|
108
|
-
name: RULE_NAME,
|
|
109
|
-
create(context) {
|
|
110
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
111
|
-
const { ctx, listeners } = core.useHookCollector();
|
|
112
|
-
return {
|
|
113
|
-
...listeners,
|
|
114
|
-
"Program:exit"(node) {
|
|
115
|
-
const allHooks = ctx.getAllHooks(node);
|
|
116
|
-
for (const { name: name2, node: node2, hookCalls } of allHooks.values()) {
|
|
117
|
-
if (hookCalls.length > 0) continue;
|
|
118
|
-
context.report({
|
|
119
|
-
messageId: "ensureCustomHooksUsingOtherHooks",
|
|
120
|
-
node: node2,
|
|
121
|
-
data: {
|
|
122
|
-
name: name2.value
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
},
|
|
129
|
-
defaultOptions: []
|
|
130
|
-
});
|
|
131
|
-
var RULE_NAME2 = "ensure-use-callback-has-non-empty-deps";
|
|
132
|
-
var ensure_use_callback_has_non_empty_deps_default = createRule({
|
|
133
|
-
meta: {
|
|
134
|
-
type: "problem",
|
|
135
|
-
docs: {
|
|
136
|
-
description: "enforce 'useCallback' has non-empty dependencies array"
|
|
137
|
-
},
|
|
138
|
-
messages: {
|
|
139
|
-
ensureUseCallbackHasNonEmptyDeps: "An 'useCallback' should have a non-empty dependencies array."
|
|
140
|
-
},
|
|
141
|
-
schema: []
|
|
142
|
-
},
|
|
143
|
-
name: RULE_NAME2,
|
|
144
|
-
create(context) {
|
|
145
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
146
|
-
const alias = shared.decodeSettings(context.settings).additionalHooks?.useCallback ?? [];
|
|
147
|
-
return {
|
|
148
|
-
CallExpression(node) {
|
|
149
|
-
if (!core.isReactHookCall(node)) return;
|
|
150
|
-
const initialScope = context.sourceCode.getScope(node);
|
|
151
|
-
if (!core.isUseCallbackCall(node, context) && !alias.some(core.isReactHookCallWithNameLoose(node))) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const scope = context.sourceCode.getScope(node);
|
|
155
|
-
const component = scope.block;
|
|
156
|
-
if (!ast.isFunction(component)) return;
|
|
157
|
-
const [cb, deps] = node.arguments;
|
|
158
|
-
if (!deps) {
|
|
159
|
-
context.report({
|
|
160
|
-
messageId: "ensureUseCallbackHasNonEmptyDeps",
|
|
161
|
-
node
|
|
162
|
-
});
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const hasEmptyDeps = tools.F.pipe(
|
|
166
|
-
tsPattern.match(deps).with({ type: types.AST_NODE_TYPES.ArrayExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
167
|
-
return tools.F.pipe(
|
|
168
|
-
_var.findVariable(n.name, initialScope),
|
|
169
|
-
tools.O.flatMap(_var.getVariableNode(0)),
|
|
170
|
-
tools.O.filter(ast.is(types.AST_NODE_TYPES.ArrayExpression))
|
|
171
|
-
);
|
|
172
|
-
}).otherwise(tools.O.none),
|
|
173
|
-
tools.O.exists((x) => x.elements.length === 0)
|
|
174
|
-
);
|
|
175
|
-
if (!hasEmptyDeps) return;
|
|
176
|
-
if (!cb) {
|
|
177
|
-
context.report({
|
|
178
|
-
messageId: "ensureUseCallbackHasNonEmptyDeps",
|
|
179
|
-
node
|
|
180
|
-
});
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const isReferencedToComponentScope = tools.F.pipe(
|
|
184
|
-
tsPattern.match(cb).with({ type: types.AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
185
|
-
if (n.body.type === types.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
186
|
-
return tools.O.some(n.body);
|
|
187
|
-
}
|
|
188
|
-
return tools.O.some(n);
|
|
189
|
-
}).with({ type: types.AST_NODE_TYPES.FunctionExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
190
|
-
return tools.F.pipe(
|
|
191
|
-
_var.findVariable(n.name, initialScope),
|
|
192
|
-
tools.O.flatMap(_var.getVariableNode(0)),
|
|
193
|
-
tools.O.filter(ast.isFunction)
|
|
194
|
-
);
|
|
195
|
-
}).otherwise(tools.O.none),
|
|
196
|
-
tools.O.map((n) => context.sourceCode.getScope(n)),
|
|
197
|
-
tools.O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
198
|
-
tools.O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
199
|
-
);
|
|
200
|
-
if (isReferencedToComponentScope) return;
|
|
201
|
-
context.report({
|
|
202
|
-
messageId: "ensureUseCallbackHasNonEmptyDeps",
|
|
203
|
-
node
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
},
|
|
208
|
-
defaultOptions: []
|
|
209
|
-
});
|
|
210
|
-
var RULE_NAME3 = "ensure-use-memo-has-non-empty-deps";
|
|
211
|
-
var ensure_use_memo_has_non_empty_deps_default = createRule({
|
|
212
|
-
meta: {
|
|
213
|
-
type: "problem",
|
|
214
|
-
docs: {
|
|
215
|
-
description: "enforce 'useMemo' has non-empty dependencies array"
|
|
216
|
-
},
|
|
217
|
-
messages: {
|
|
218
|
-
ensureUseMemoHasNonEmptyDeps: "An 'useMemo' should have a non-empty dependencies array."
|
|
219
|
-
},
|
|
220
|
-
schema: []
|
|
221
|
-
},
|
|
222
|
-
name: RULE_NAME3,
|
|
223
|
-
create(context) {
|
|
224
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
225
|
-
const alias = shared.decodeSettings(context.settings).additionalHooks?.useMemo ?? [];
|
|
226
|
-
return {
|
|
227
|
-
CallExpression(node) {
|
|
228
|
-
if (!core.isReactHookCall(node)) return;
|
|
229
|
-
const initialScope = context.sourceCode.getScope(node);
|
|
230
|
-
if (!core.isUseMemoCall(node, context) && !alias.some(core.isReactHookCallWithNameLoose(node))) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const scope = context.sourceCode.getScope(node);
|
|
234
|
-
const component = scope.block;
|
|
235
|
-
if (!ast.isFunction(component)) return;
|
|
236
|
-
const [cb, deps] = node.arguments;
|
|
237
|
-
if (!deps) {
|
|
238
|
-
context.report({
|
|
239
|
-
messageId: "ensureUseMemoHasNonEmptyDeps",
|
|
240
|
-
node
|
|
241
|
-
});
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const hasEmptyDeps = tools.F.pipe(
|
|
245
|
-
tsPattern.match(deps).with({ type: types.AST_NODE_TYPES.ArrayExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
246
|
-
return tools.F.pipe(
|
|
247
|
-
_var.findVariable(n.name, initialScope),
|
|
248
|
-
tools.O.flatMap(_var.getVariableNode(0)),
|
|
249
|
-
tools.O.filter(ast.is(types.AST_NODE_TYPES.ArrayExpression))
|
|
250
|
-
);
|
|
251
|
-
}).otherwise(tools.O.none),
|
|
252
|
-
tools.O.exists((x) => x.elements.length === 0)
|
|
253
|
-
);
|
|
254
|
-
if (!hasEmptyDeps) return;
|
|
255
|
-
if (!cb) {
|
|
256
|
-
context.report({
|
|
257
|
-
messageId: "ensureUseMemoHasNonEmptyDeps",
|
|
258
|
-
node
|
|
259
|
-
});
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const isReferencedToComponentScope = tools.F.pipe(
|
|
263
|
-
tsPattern.match(cb).with({ type: types.AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
264
|
-
if (n.body.type === types.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
265
|
-
return tools.O.some(n.body);
|
|
266
|
-
}
|
|
267
|
-
return tools.O.some(n);
|
|
268
|
-
}).with({ type: types.AST_NODE_TYPES.FunctionExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
269
|
-
return tools.F.pipe(
|
|
270
|
-
_var.findVariable(n.name, initialScope),
|
|
271
|
-
tools.O.flatMap(_var.getVariableNode(0)),
|
|
272
|
-
tools.O.filter(ast.isFunction)
|
|
273
|
-
);
|
|
274
|
-
}).otherwise(tools.O.none),
|
|
275
|
-
tools.O.map((n) => context.sourceCode.getScope(n)),
|
|
276
|
-
tools.O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
277
|
-
tools.O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
278
|
-
);
|
|
279
|
-
if (isReferencedToComponentScope) return;
|
|
280
|
-
context.report({
|
|
281
|
-
messageId: "ensureUseMemoHasNonEmptyDeps",
|
|
282
|
-
node
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
},
|
|
287
|
-
defaultOptions: []
|
|
288
|
-
});
|
|
289
|
-
var RULE_NAME4 = "no-direct-set-state-in-use-effect";
|
|
95
|
+
// src/rules/no-direct-set-state-in-use-effect.ts
|
|
96
|
+
var RULE_NAME = "no-direct-set-state-in-use-effect";
|
|
290
97
|
var no_direct_set_state_in_use_effect_default = createRule({
|
|
291
98
|
meta: {
|
|
292
99
|
type: "problem",
|
|
@@ -298,7 +105,7 @@ var no_direct_set_state_in_use_effect_default = createRule({
|
|
|
298
105
|
},
|
|
299
106
|
schema: []
|
|
300
107
|
},
|
|
301
|
-
name:
|
|
108
|
+
name: RULE_NAME,
|
|
302
109
|
create(context) {
|
|
303
110
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
304
111
|
const settings = shared.decodeSettings(context.settings);
|
|
@@ -472,6 +279,199 @@ var no_direct_set_state_in_use_effect_default = createRule({
|
|
|
472
279
|
},
|
|
473
280
|
defaultOptions: []
|
|
474
281
|
});
|
|
282
|
+
var RULE_NAME2 = "no-redundant-custom-hook";
|
|
283
|
+
var no_redundant_custom_hook_default = createRule({
|
|
284
|
+
meta: {
|
|
285
|
+
type: "problem",
|
|
286
|
+
docs: {
|
|
287
|
+
description: "enforce custom hooks to use at least one other hook inside"
|
|
288
|
+
},
|
|
289
|
+
messages: {
|
|
290
|
+
noRedundantCustomHook: "A custom hook '{{name}}' should use at least one other hook."
|
|
291
|
+
},
|
|
292
|
+
schema: []
|
|
293
|
+
},
|
|
294
|
+
name: RULE_NAME2,
|
|
295
|
+
create(context) {
|
|
296
|
+
const { ctx, listeners } = core.useHookCollector();
|
|
297
|
+
return {
|
|
298
|
+
...listeners,
|
|
299
|
+
"Program:exit"(node) {
|
|
300
|
+
const allHooks = ctx.getAllHooks(node);
|
|
301
|
+
for (const { name: name2, node: node2, hookCalls } of allHooks.values()) {
|
|
302
|
+
if (ast.isEmptyFunction(node2)) continue;
|
|
303
|
+
if (hookCalls.length > 0) continue;
|
|
304
|
+
context.report({
|
|
305
|
+
messageId: "noRedundantCustomHook",
|
|
306
|
+
node: node2,
|
|
307
|
+
data: {
|
|
308
|
+
name: name2.value
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
defaultOptions: []
|
|
316
|
+
});
|
|
317
|
+
var RULE_NAME3 = "no-unnecessary-use-callback";
|
|
318
|
+
var no_unnecessary_use_callback_default = createRule({
|
|
319
|
+
meta: {
|
|
320
|
+
type: "problem",
|
|
321
|
+
docs: {
|
|
322
|
+
description: "disallow unnecessary usage of 'useCallback'"
|
|
323
|
+
},
|
|
324
|
+
messages: {
|
|
325
|
+
noUnnecessaryUseCallback: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary."
|
|
326
|
+
},
|
|
327
|
+
schema: []
|
|
328
|
+
},
|
|
329
|
+
name: RULE_NAME3,
|
|
330
|
+
create(context) {
|
|
331
|
+
if (!context.sourceCode.text.includes("use")) return {};
|
|
332
|
+
const alias = shared.decodeSettings(context.settings).additionalHooks?.useCallback ?? [];
|
|
333
|
+
return {
|
|
334
|
+
CallExpression(node) {
|
|
335
|
+
if (!core.isReactHookCall(node)) return;
|
|
336
|
+
const initialScope = context.sourceCode.getScope(node);
|
|
337
|
+
if (!core.isUseCallbackCall(node, context) && !alias.some(core.isReactHookCallWithNameLoose(node))) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const scope = context.sourceCode.getScope(node);
|
|
341
|
+
const component = scope.block;
|
|
342
|
+
if (!ast.isFunction(component)) return;
|
|
343
|
+
const [cb, deps] = node.arguments;
|
|
344
|
+
if (!deps) {
|
|
345
|
+
context.report({
|
|
346
|
+
messageId: "noUnnecessaryUseCallback",
|
|
347
|
+
node
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const hasEmptyDeps = tools.F.pipe(
|
|
352
|
+
tsPattern.match(deps).with({ type: types.AST_NODE_TYPES.ArrayExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
353
|
+
return tools.F.pipe(
|
|
354
|
+
_var.findVariable(n.name, initialScope),
|
|
355
|
+
tools.O.flatMap(_var.getVariableNode(0)),
|
|
356
|
+
tools.O.filter(ast.is(types.AST_NODE_TYPES.ArrayExpression))
|
|
357
|
+
);
|
|
358
|
+
}).otherwise(tools.O.none),
|
|
359
|
+
tools.O.exists((x) => x.elements.length === 0)
|
|
360
|
+
);
|
|
361
|
+
if (!hasEmptyDeps) return;
|
|
362
|
+
if (!cb) {
|
|
363
|
+
context.report({
|
|
364
|
+
messageId: "noUnnecessaryUseCallback",
|
|
365
|
+
node
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const isReferencedToComponentScope = tools.F.pipe(
|
|
370
|
+
tsPattern.match(cb).with({ type: types.AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
371
|
+
if (n.body.type === types.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
372
|
+
return tools.O.some(n.body);
|
|
373
|
+
}
|
|
374
|
+
return tools.O.some(n);
|
|
375
|
+
}).with({ type: types.AST_NODE_TYPES.FunctionExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
376
|
+
return tools.F.pipe(
|
|
377
|
+
_var.findVariable(n.name, initialScope),
|
|
378
|
+
tools.O.flatMap(_var.getVariableNode(0)),
|
|
379
|
+
tools.O.filter(ast.isFunction)
|
|
380
|
+
);
|
|
381
|
+
}).otherwise(tools.O.none),
|
|
382
|
+
tools.O.map((n) => context.sourceCode.getScope(n)),
|
|
383
|
+
tools.O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
384
|
+
tools.O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
385
|
+
);
|
|
386
|
+
if (isReferencedToComponentScope) return;
|
|
387
|
+
context.report({
|
|
388
|
+
messageId: "noUnnecessaryUseCallback",
|
|
389
|
+
node
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
defaultOptions: []
|
|
395
|
+
});
|
|
396
|
+
var RULE_NAME4 = "no-unnecessary-use-memo";
|
|
397
|
+
var no_unnecessary_use_memo_default = createRule({
|
|
398
|
+
meta: {
|
|
399
|
+
type: "problem",
|
|
400
|
+
docs: {
|
|
401
|
+
description: "disallow unnecessary usage of 'useMemo'"
|
|
402
|
+
},
|
|
403
|
+
messages: {
|
|
404
|
+
noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary."
|
|
405
|
+
},
|
|
406
|
+
schema: []
|
|
407
|
+
},
|
|
408
|
+
name: RULE_NAME4,
|
|
409
|
+
create(context) {
|
|
410
|
+
if (!context.sourceCode.text.includes("use")) return {};
|
|
411
|
+
const alias = shared.decodeSettings(context.settings).additionalHooks?.useMemo ?? [];
|
|
412
|
+
return {
|
|
413
|
+
CallExpression(node) {
|
|
414
|
+
if (!core.isReactHookCall(node)) return;
|
|
415
|
+
const initialScope = context.sourceCode.getScope(node);
|
|
416
|
+
if (!core.isUseMemoCall(node, context) && !alias.some(core.isReactHookCallWithNameLoose(node))) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const scope = context.sourceCode.getScope(node);
|
|
420
|
+
const component = scope.block;
|
|
421
|
+
if (!ast.isFunction(component)) return;
|
|
422
|
+
const [cb, deps] = node.arguments;
|
|
423
|
+
if (!deps) {
|
|
424
|
+
context.report({
|
|
425
|
+
messageId: "noUnnecessaryUseMemo",
|
|
426
|
+
node
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const hasEmptyDeps = tools.F.pipe(
|
|
431
|
+
tsPattern.match(deps).with({ type: types.AST_NODE_TYPES.ArrayExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
432
|
+
return tools.F.pipe(
|
|
433
|
+
_var.findVariable(n.name, initialScope),
|
|
434
|
+
tools.O.flatMap(_var.getVariableNode(0)),
|
|
435
|
+
tools.O.filter(ast.is(types.AST_NODE_TYPES.ArrayExpression))
|
|
436
|
+
);
|
|
437
|
+
}).otherwise(tools.O.none),
|
|
438
|
+
tools.O.exists((x) => x.elements.length === 0)
|
|
439
|
+
);
|
|
440
|
+
if (!hasEmptyDeps) return;
|
|
441
|
+
if (!cb) {
|
|
442
|
+
context.report({
|
|
443
|
+
messageId: "noUnnecessaryUseMemo",
|
|
444
|
+
node
|
|
445
|
+
});
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const isReferencedToComponentScope = tools.F.pipe(
|
|
449
|
+
tsPattern.match(cb).with({ type: types.AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
450
|
+
if (n.body.type === types.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
451
|
+
return tools.O.some(n.body);
|
|
452
|
+
}
|
|
453
|
+
return tools.O.some(n);
|
|
454
|
+
}).with({ type: types.AST_NODE_TYPES.FunctionExpression }, tools.O.some).with({ type: types.AST_NODE_TYPES.Identifier }, (n) => {
|
|
455
|
+
return tools.F.pipe(
|
|
456
|
+
_var.findVariable(n.name, initialScope),
|
|
457
|
+
tools.O.flatMap(_var.getVariableNode(0)),
|
|
458
|
+
tools.O.filter(ast.isFunction)
|
|
459
|
+
);
|
|
460
|
+
}).otherwise(tools.O.none),
|
|
461
|
+
tools.O.map((n) => context.sourceCode.getScope(n)),
|
|
462
|
+
tools.O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
463
|
+
tools.O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
464
|
+
);
|
|
465
|
+
if (isReferencedToComponentScope) return;
|
|
466
|
+
context.report({
|
|
467
|
+
messageId: "noUnnecessaryUseMemo",
|
|
468
|
+
node
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
defaultOptions: []
|
|
474
|
+
});
|
|
475
475
|
var RULE_NAME5 = "prefer-use-state-lazy-initialization";
|
|
476
476
|
var ALLOW_LIST = ["Boolean", "String", "Number"];
|
|
477
477
|
var prefer_use_state_lazy_initialization_default = createRule({
|
|
@@ -516,13 +516,18 @@ var meta = {
|
|
|
516
516
|
version
|
|
517
517
|
};
|
|
518
518
|
var rules = {
|
|
519
|
-
"ensure-custom-hooks-using-other-hooks": ensure_custom_hooks_using_other_hooks_default,
|
|
520
|
-
"ensure-use-callback-has-non-empty-deps": ensure_use_callback_has_non_empty_deps_default,
|
|
521
|
-
"ensure-use-memo-has-non-empty-deps": ensure_use_memo_has_non_empty_deps_default,
|
|
522
519
|
"no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default,
|
|
520
|
+
"no-redundant-custom-hook": no_redundant_custom_hook_default,
|
|
521
|
+
"no-unnecessary-use-callback": no_unnecessary_use_callback_default,
|
|
522
|
+
"no-unnecessary-use-memo": no_unnecessary_use_memo_default,
|
|
523
|
+
"prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default,
|
|
524
|
+
// Deprecated Rules
|
|
525
|
+
/** @deprecated Use `no-redundant-custom-hook` instead */
|
|
526
|
+
"ensure-custom-hooks-using-other-hooks": no_redundant_custom_hook_default,
|
|
527
|
+
/** @deprecated Use `no-unnecessary-use-memo` instead */
|
|
528
|
+
"ensure-use-memo-has-non-empty-deps": no_unnecessary_use_memo_default,
|
|
523
529
|
/** @deprecated Use `no-direct-set-state-in-use-effect` instead */
|
|
524
|
-
"no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_effect_default
|
|
525
|
-
"prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
|
|
530
|
+
"no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_effect_default
|
|
526
531
|
};
|
|
527
532
|
|
|
528
533
|
exports.meta = meta;
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { traverseUpGuard, isFunction, getNestedIdentifiers, toReadableNodeName, isEmptyFunction, is, getNestedCallExpressions, isFunctionOfImmediatelyInvoked } from '@eslint-react/ast';
|
|
2
|
+
import { isReactHookCallWithNameAlias, useHookCollector, isReactHookCall, isUseCallbackCall, isReactHookCallWithNameLoose, isUseMemoCall, isUseStateCall, isReactHookName } from '@eslint-react/core';
|
|
2
3
|
import { createRuleForPlugin, decodeSettings } from '@eslint-react/shared';
|
|
3
|
-
import {
|
|
4
|
-
import { F, O, MutRef } from '@eslint-react/tools';
|
|
4
|
+
import { MutRef, O, F } from '@eslint-react/tools';
|
|
5
5
|
import { findVariable, getVariableNode } from '@eslint-react/var';
|
|
6
6
|
import { AST_NODE_TYPES } from '@typescript-eslint/types';
|
|
7
|
-
import { getStaticValue } from '@typescript-eslint/utils/ast-utils';
|
|
8
7
|
import { match, isMatching } from 'ts-pattern';
|
|
8
|
+
import { getStaticValue } from '@typescript-eslint/utils/ast-utils';
|
|
9
9
|
|
|
10
10
|
// package.json
|
|
11
11
|
var name = "eslint-plugin-react-hooks-extra";
|
|
12
|
-
var version = "1.
|
|
12
|
+
var version = "1.13.0-beta.1";
|
|
13
13
|
var createRule = createRuleForPlugin("hooks-extra");
|
|
14
14
|
function isFromHookCall(name2, context, settings, predicate = F.constTrue) {
|
|
15
15
|
const hookAlias = settings.additionalHooks?.[name2] ?? [];
|
|
@@ -90,201 +90,8 @@ function isVariableDeclaratorFromHookCall(node) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// src/rules/
|
|
94
|
-
var RULE_NAME = "
|
|
95
|
-
var ensure_custom_hooks_using_other_hooks_default = createRule({
|
|
96
|
-
meta: {
|
|
97
|
-
type: "problem",
|
|
98
|
-
docs: {
|
|
99
|
-
description: "enforce custom hooks using other hooks"
|
|
100
|
-
},
|
|
101
|
-
messages: {
|
|
102
|
-
ensureCustomHooksUsingOtherHooks: "A custom hook '{{name}}' should use other hooks."
|
|
103
|
-
},
|
|
104
|
-
schema: []
|
|
105
|
-
},
|
|
106
|
-
name: RULE_NAME,
|
|
107
|
-
create(context) {
|
|
108
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
109
|
-
const { ctx, listeners } = useHookCollector();
|
|
110
|
-
return {
|
|
111
|
-
...listeners,
|
|
112
|
-
"Program:exit"(node) {
|
|
113
|
-
const allHooks = ctx.getAllHooks(node);
|
|
114
|
-
for (const { name: name2, node: node2, hookCalls } of allHooks.values()) {
|
|
115
|
-
if (hookCalls.length > 0) continue;
|
|
116
|
-
context.report({
|
|
117
|
-
messageId: "ensureCustomHooksUsingOtherHooks",
|
|
118
|
-
node: node2,
|
|
119
|
-
data: {
|
|
120
|
-
name: name2.value
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
defaultOptions: []
|
|
128
|
-
});
|
|
129
|
-
var RULE_NAME2 = "ensure-use-callback-has-non-empty-deps";
|
|
130
|
-
var ensure_use_callback_has_non_empty_deps_default = createRule({
|
|
131
|
-
meta: {
|
|
132
|
-
type: "problem",
|
|
133
|
-
docs: {
|
|
134
|
-
description: "enforce 'useCallback' has non-empty dependencies array"
|
|
135
|
-
},
|
|
136
|
-
messages: {
|
|
137
|
-
ensureUseCallbackHasNonEmptyDeps: "An 'useCallback' should have a non-empty dependencies array."
|
|
138
|
-
},
|
|
139
|
-
schema: []
|
|
140
|
-
},
|
|
141
|
-
name: RULE_NAME2,
|
|
142
|
-
create(context) {
|
|
143
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
144
|
-
const alias = decodeSettings(context.settings).additionalHooks?.useCallback ?? [];
|
|
145
|
-
return {
|
|
146
|
-
CallExpression(node) {
|
|
147
|
-
if (!isReactHookCall(node)) return;
|
|
148
|
-
const initialScope = context.sourceCode.getScope(node);
|
|
149
|
-
if (!isUseCallbackCall(node, context) && !alias.some(isReactHookCallWithNameLoose(node))) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const scope = context.sourceCode.getScope(node);
|
|
153
|
-
const component = scope.block;
|
|
154
|
-
if (!isFunction(component)) return;
|
|
155
|
-
const [cb, deps] = node.arguments;
|
|
156
|
-
if (!deps) {
|
|
157
|
-
context.report({
|
|
158
|
-
messageId: "ensureUseCallbackHasNonEmptyDeps",
|
|
159
|
-
node
|
|
160
|
-
});
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const hasEmptyDeps = F.pipe(
|
|
164
|
-
match(deps).with({ type: AST_NODE_TYPES.ArrayExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
165
|
-
return F.pipe(
|
|
166
|
-
findVariable(n.name, initialScope),
|
|
167
|
-
O.flatMap(getVariableNode(0)),
|
|
168
|
-
O.filter(is(AST_NODE_TYPES.ArrayExpression))
|
|
169
|
-
);
|
|
170
|
-
}).otherwise(O.none),
|
|
171
|
-
O.exists((x) => x.elements.length === 0)
|
|
172
|
-
);
|
|
173
|
-
if (!hasEmptyDeps) return;
|
|
174
|
-
if (!cb) {
|
|
175
|
-
context.report({
|
|
176
|
-
messageId: "ensureUseCallbackHasNonEmptyDeps",
|
|
177
|
-
node
|
|
178
|
-
});
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const isReferencedToComponentScope = F.pipe(
|
|
182
|
-
match(cb).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
183
|
-
if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
184
|
-
return O.some(n.body);
|
|
185
|
-
}
|
|
186
|
-
return O.some(n);
|
|
187
|
-
}).with({ type: AST_NODE_TYPES.FunctionExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
188
|
-
return F.pipe(
|
|
189
|
-
findVariable(n.name, initialScope),
|
|
190
|
-
O.flatMap(getVariableNode(0)),
|
|
191
|
-
O.filter(isFunction)
|
|
192
|
-
);
|
|
193
|
-
}).otherwise(O.none),
|
|
194
|
-
O.map((n) => context.sourceCode.getScope(n)),
|
|
195
|
-
O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
196
|
-
O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
197
|
-
);
|
|
198
|
-
if (isReferencedToComponentScope) return;
|
|
199
|
-
context.report({
|
|
200
|
-
messageId: "ensureUseCallbackHasNonEmptyDeps",
|
|
201
|
-
node
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
},
|
|
206
|
-
defaultOptions: []
|
|
207
|
-
});
|
|
208
|
-
var RULE_NAME3 = "ensure-use-memo-has-non-empty-deps";
|
|
209
|
-
var ensure_use_memo_has_non_empty_deps_default = createRule({
|
|
210
|
-
meta: {
|
|
211
|
-
type: "problem",
|
|
212
|
-
docs: {
|
|
213
|
-
description: "enforce 'useMemo' has non-empty dependencies array"
|
|
214
|
-
},
|
|
215
|
-
messages: {
|
|
216
|
-
ensureUseMemoHasNonEmptyDeps: "An 'useMemo' should have a non-empty dependencies array."
|
|
217
|
-
},
|
|
218
|
-
schema: []
|
|
219
|
-
},
|
|
220
|
-
name: RULE_NAME3,
|
|
221
|
-
create(context) {
|
|
222
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
223
|
-
const alias = decodeSettings(context.settings).additionalHooks?.useMemo ?? [];
|
|
224
|
-
return {
|
|
225
|
-
CallExpression(node) {
|
|
226
|
-
if (!isReactHookCall(node)) return;
|
|
227
|
-
const initialScope = context.sourceCode.getScope(node);
|
|
228
|
-
if (!isUseMemoCall(node, context) && !alias.some(isReactHookCallWithNameLoose(node))) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const scope = context.sourceCode.getScope(node);
|
|
232
|
-
const component = scope.block;
|
|
233
|
-
if (!isFunction(component)) return;
|
|
234
|
-
const [cb, deps] = node.arguments;
|
|
235
|
-
if (!deps) {
|
|
236
|
-
context.report({
|
|
237
|
-
messageId: "ensureUseMemoHasNonEmptyDeps",
|
|
238
|
-
node
|
|
239
|
-
});
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const hasEmptyDeps = F.pipe(
|
|
243
|
-
match(deps).with({ type: AST_NODE_TYPES.ArrayExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
244
|
-
return F.pipe(
|
|
245
|
-
findVariable(n.name, initialScope),
|
|
246
|
-
O.flatMap(getVariableNode(0)),
|
|
247
|
-
O.filter(is(AST_NODE_TYPES.ArrayExpression))
|
|
248
|
-
);
|
|
249
|
-
}).otherwise(O.none),
|
|
250
|
-
O.exists((x) => x.elements.length === 0)
|
|
251
|
-
);
|
|
252
|
-
if (!hasEmptyDeps) return;
|
|
253
|
-
if (!cb) {
|
|
254
|
-
context.report({
|
|
255
|
-
messageId: "ensureUseMemoHasNonEmptyDeps",
|
|
256
|
-
node
|
|
257
|
-
});
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const isReferencedToComponentScope = F.pipe(
|
|
261
|
-
match(cb).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
262
|
-
if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
263
|
-
return O.some(n.body);
|
|
264
|
-
}
|
|
265
|
-
return O.some(n);
|
|
266
|
-
}).with({ type: AST_NODE_TYPES.FunctionExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
267
|
-
return F.pipe(
|
|
268
|
-
findVariable(n.name, initialScope),
|
|
269
|
-
O.flatMap(getVariableNode(0)),
|
|
270
|
-
O.filter(isFunction)
|
|
271
|
-
);
|
|
272
|
-
}).otherwise(O.none),
|
|
273
|
-
O.map((n) => context.sourceCode.getScope(n)),
|
|
274
|
-
O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
275
|
-
O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
276
|
-
);
|
|
277
|
-
if (isReferencedToComponentScope) return;
|
|
278
|
-
context.report({
|
|
279
|
-
messageId: "ensureUseMemoHasNonEmptyDeps",
|
|
280
|
-
node
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
defaultOptions: []
|
|
286
|
-
});
|
|
287
|
-
var RULE_NAME4 = "no-direct-set-state-in-use-effect";
|
|
93
|
+
// src/rules/no-direct-set-state-in-use-effect.ts
|
|
94
|
+
var RULE_NAME = "no-direct-set-state-in-use-effect";
|
|
288
95
|
var no_direct_set_state_in_use_effect_default = createRule({
|
|
289
96
|
meta: {
|
|
290
97
|
type: "problem",
|
|
@@ -296,7 +103,7 @@ var no_direct_set_state_in_use_effect_default = createRule({
|
|
|
296
103
|
},
|
|
297
104
|
schema: []
|
|
298
105
|
},
|
|
299
|
-
name:
|
|
106
|
+
name: RULE_NAME,
|
|
300
107
|
create(context) {
|
|
301
108
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
302
109
|
const settings = decodeSettings(context.settings);
|
|
@@ -470,6 +277,199 @@ var no_direct_set_state_in_use_effect_default = createRule({
|
|
|
470
277
|
},
|
|
471
278
|
defaultOptions: []
|
|
472
279
|
});
|
|
280
|
+
var RULE_NAME2 = "no-redundant-custom-hook";
|
|
281
|
+
var no_redundant_custom_hook_default = createRule({
|
|
282
|
+
meta: {
|
|
283
|
+
type: "problem",
|
|
284
|
+
docs: {
|
|
285
|
+
description: "enforce custom hooks to use at least one other hook inside"
|
|
286
|
+
},
|
|
287
|
+
messages: {
|
|
288
|
+
noRedundantCustomHook: "A custom hook '{{name}}' should use at least one other hook."
|
|
289
|
+
},
|
|
290
|
+
schema: []
|
|
291
|
+
},
|
|
292
|
+
name: RULE_NAME2,
|
|
293
|
+
create(context) {
|
|
294
|
+
const { ctx, listeners } = useHookCollector();
|
|
295
|
+
return {
|
|
296
|
+
...listeners,
|
|
297
|
+
"Program:exit"(node) {
|
|
298
|
+
const allHooks = ctx.getAllHooks(node);
|
|
299
|
+
for (const { name: name2, node: node2, hookCalls } of allHooks.values()) {
|
|
300
|
+
if (isEmptyFunction(node2)) continue;
|
|
301
|
+
if (hookCalls.length > 0) continue;
|
|
302
|
+
context.report({
|
|
303
|
+
messageId: "noRedundantCustomHook",
|
|
304
|
+
node: node2,
|
|
305
|
+
data: {
|
|
306
|
+
name: name2.value
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
defaultOptions: []
|
|
314
|
+
});
|
|
315
|
+
var RULE_NAME3 = "no-unnecessary-use-callback";
|
|
316
|
+
var no_unnecessary_use_callback_default = createRule({
|
|
317
|
+
meta: {
|
|
318
|
+
type: "problem",
|
|
319
|
+
docs: {
|
|
320
|
+
description: "disallow unnecessary usage of 'useCallback'"
|
|
321
|
+
},
|
|
322
|
+
messages: {
|
|
323
|
+
noUnnecessaryUseCallback: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary."
|
|
324
|
+
},
|
|
325
|
+
schema: []
|
|
326
|
+
},
|
|
327
|
+
name: RULE_NAME3,
|
|
328
|
+
create(context) {
|
|
329
|
+
if (!context.sourceCode.text.includes("use")) return {};
|
|
330
|
+
const alias = decodeSettings(context.settings).additionalHooks?.useCallback ?? [];
|
|
331
|
+
return {
|
|
332
|
+
CallExpression(node) {
|
|
333
|
+
if (!isReactHookCall(node)) return;
|
|
334
|
+
const initialScope = context.sourceCode.getScope(node);
|
|
335
|
+
if (!isUseCallbackCall(node, context) && !alias.some(isReactHookCallWithNameLoose(node))) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const scope = context.sourceCode.getScope(node);
|
|
339
|
+
const component = scope.block;
|
|
340
|
+
if (!isFunction(component)) return;
|
|
341
|
+
const [cb, deps] = node.arguments;
|
|
342
|
+
if (!deps) {
|
|
343
|
+
context.report({
|
|
344
|
+
messageId: "noUnnecessaryUseCallback",
|
|
345
|
+
node
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const hasEmptyDeps = F.pipe(
|
|
350
|
+
match(deps).with({ type: AST_NODE_TYPES.ArrayExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
351
|
+
return F.pipe(
|
|
352
|
+
findVariable(n.name, initialScope),
|
|
353
|
+
O.flatMap(getVariableNode(0)),
|
|
354
|
+
O.filter(is(AST_NODE_TYPES.ArrayExpression))
|
|
355
|
+
);
|
|
356
|
+
}).otherwise(O.none),
|
|
357
|
+
O.exists((x) => x.elements.length === 0)
|
|
358
|
+
);
|
|
359
|
+
if (!hasEmptyDeps) return;
|
|
360
|
+
if (!cb) {
|
|
361
|
+
context.report({
|
|
362
|
+
messageId: "noUnnecessaryUseCallback",
|
|
363
|
+
node
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const isReferencedToComponentScope = F.pipe(
|
|
368
|
+
match(cb).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
369
|
+
if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
370
|
+
return O.some(n.body);
|
|
371
|
+
}
|
|
372
|
+
return O.some(n);
|
|
373
|
+
}).with({ type: AST_NODE_TYPES.FunctionExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
374
|
+
return F.pipe(
|
|
375
|
+
findVariable(n.name, initialScope),
|
|
376
|
+
O.flatMap(getVariableNode(0)),
|
|
377
|
+
O.filter(isFunction)
|
|
378
|
+
);
|
|
379
|
+
}).otherwise(O.none),
|
|
380
|
+
O.map((n) => context.sourceCode.getScope(n)),
|
|
381
|
+
O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
382
|
+
O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
383
|
+
);
|
|
384
|
+
if (isReferencedToComponentScope) return;
|
|
385
|
+
context.report({
|
|
386
|
+
messageId: "noUnnecessaryUseCallback",
|
|
387
|
+
node
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
defaultOptions: []
|
|
393
|
+
});
|
|
394
|
+
var RULE_NAME4 = "no-unnecessary-use-memo";
|
|
395
|
+
var no_unnecessary_use_memo_default = createRule({
|
|
396
|
+
meta: {
|
|
397
|
+
type: "problem",
|
|
398
|
+
docs: {
|
|
399
|
+
description: "disallow unnecessary usage of 'useMemo'"
|
|
400
|
+
},
|
|
401
|
+
messages: {
|
|
402
|
+
noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary."
|
|
403
|
+
},
|
|
404
|
+
schema: []
|
|
405
|
+
},
|
|
406
|
+
name: RULE_NAME4,
|
|
407
|
+
create(context) {
|
|
408
|
+
if (!context.sourceCode.text.includes("use")) return {};
|
|
409
|
+
const alias = decodeSettings(context.settings).additionalHooks?.useMemo ?? [];
|
|
410
|
+
return {
|
|
411
|
+
CallExpression(node) {
|
|
412
|
+
if (!isReactHookCall(node)) return;
|
|
413
|
+
const initialScope = context.sourceCode.getScope(node);
|
|
414
|
+
if (!isUseMemoCall(node, context) && !alias.some(isReactHookCallWithNameLoose(node))) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const scope = context.sourceCode.getScope(node);
|
|
418
|
+
const component = scope.block;
|
|
419
|
+
if (!isFunction(component)) return;
|
|
420
|
+
const [cb, deps] = node.arguments;
|
|
421
|
+
if (!deps) {
|
|
422
|
+
context.report({
|
|
423
|
+
messageId: "noUnnecessaryUseMemo",
|
|
424
|
+
node
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const hasEmptyDeps = F.pipe(
|
|
429
|
+
match(deps).with({ type: AST_NODE_TYPES.ArrayExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
430
|
+
return F.pipe(
|
|
431
|
+
findVariable(n.name, initialScope),
|
|
432
|
+
O.flatMap(getVariableNode(0)),
|
|
433
|
+
O.filter(is(AST_NODE_TYPES.ArrayExpression))
|
|
434
|
+
);
|
|
435
|
+
}).otherwise(O.none),
|
|
436
|
+
O.exists((x) => x.elements.length === 0)
|
|
437
|
+
);
|
|
438
|
+
if (!hasEmptyDeps) return;
|
|
439
|
+
if (!cb) {
|
|
440
|
+
context.report({
|
|
441
|
+
messageId: "noUnnecessaryUseMemo",
|
|
442
|
+
node
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const isReferencedToComponentScope = F.pipe(
|
|
447
|
+
match(cb).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
448
|
+
if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
449
|
+
return O.some(n.body);
|
|
450
|
+
}
|
|
451
|
+
return O.some(n);
|
|
452
|
+
}).with({ type: AST_NODE_TYPES.FunctionExpression }, O.some).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
453
|
+
return F.pipe(
|
|
454
|
+
findVariable(n.name, initialScope),
|
|
455
|
+
O.flatMap(getVariableNode(0)),
|
|
456
|
+
O.filter(isFunction)
|
|
457
|
+
);
|
|
458
|
+
}).otherwise(O.none),
|
|
459
|
+
O.map((n) => context.sourceCode.getScope(n)),
|
|
460
|
+
O.map((s) => [...s.childScopes, s].flatMap((x) => x.references)),
|
|
461
|
+
O.exists((refs) => refs.some((x) => x.resolved?.scope.block === component))
|
|
462
|
+
);
|
|
463
|
+
if (isReferencedToComponentScope) return;
|
|
464
|
+
context.report({
|
|
465
|
+
messageId: "noUnnecessaryUseMemo",
|
|
466
|
+
node
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
defaultOptions: []
|
|
472
|
+
});
|
|
473
473
|
var RULE_NAME5 = "prefer-use-state-lazy-initialization";
|
|
474
474
|
var ALLOW_LIST = ["Boolean", "String", "Number"];
|
|
475
475
|
var prefer_use_state_lazy_initialization_default = createRule({
|
|
@@ -514,13 +514,18 @@ var meta = {
|
|
|
514
514
|
version
|
|
515
515
|
};
|
|
516
516
|
var rules = {
|
|
517
|
-
"ensure-custom-hooks-using-other-hooks": ensure_custom_hooks_using_other_hooks_default,
|
|
518
|
-
"ensure-use-callback-has-non-empty-deps": ensure_use_callback_has_non_empty_deps_default,
|
|
519
|
-
"ensure-use-memo-has-non-empty-deps": ensure_use_memo_has_non_empty_deps_default,
|
|
520
517
|
"no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default,
|
|
518
|
+
"no-redundant-custom-hook": no_redundant_custom_hook_default,
|
|
519
|
+
"no-unnecessary-use-callback": no_unnecessary_use_callback_default,
|
|
520
|
+
"no-unnecessary-use-memo": no_unnecessary_use_memo_default,
|
|
521
|
+
"prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default,
|
|
522
|
+
// Deprecated Rules
|
|
523
|
+
/** @deprecated Use `no-redundant-custom-hook` instead */
|
|
524
|
+
"ensure-custom-hooks-using-other-hooks": no_redundant_custom_hook_default,
|
|
525
|
+
/** @deprecated Use `no-unnecessary-use-memo` instead */
|
|
526
|
+
"ensure-use-memo-has-non-empty-deps": no_unnecessary_use_memo_default,
|
|
521
527
|
/** @deprecated Use `no-direct-set-state-in-use-effect` instead */
|
|
522
|
-
"no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_effect_default
|
|
523
|
-
"prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
|
|
528
|
+
"no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_effect_default
|
|
524
529
|
};
|
|
525
530
|
|
|
526
531
|
export { meta, rules };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-hooks-extra",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0-beta.1",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for React Hooks related rules.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
"@typescript-eslint/types": "^8.4.0",
|
|
47
47
|
"@typescript-eslint/utils": "^8.4.0",
|
|
48
48
|
"ts-pattern": "^5.3.1",
|
|
49
|
-
"@eslint-react/ast": "1.
|
|
50
|
-
"@eslint-react/core": "1.
|
|
51
|
-
"@eslint-react/jsx": "1.
|
|
52
|
-
"@eslint-react/tools": "1.
|
|
53
|
-
"@eslint-react/shared": "1.
|
|
54
|
-
"@eslint-react/
|
|
55
|
-
"@eslint-react/
|
|
49
|
+
"@eslint-react/ast": "1.13.0-beta.1",
|
|
50
|
+
"@eslint-react/core": "1.13.0-beta.1",
|
|
51
|
+
"@eslint-react/jsx": "1.13.0-beta.1",
|
|
52
|
+
"@eslint-react/tools": "1.13.0-beta.1",
|
|
53
|
+
"@eslint-react/shared": "1.13.0-beta.1",
|
|
54
|
+
"@eslint-react/var": "1.13.0-beta.1",
|
|
55
|
+
"@eslint-react/types": "1.13.0-beta.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/react": "^18.3.5",
|