fix[eslint-plugin-react-hooks]: Fix error when callback argument is an identifier with an `as` expression (#31119)
Mark Skelton committed
Nov 19, 2024 at 03:36 UTC
eaf2d5c670c84124618977156d81946435922eb3
2 files changed
+55
-25
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+31
-12
@@ -7088,18 +7088,7 @@ const tests = {
7088
errors: [
7089
{
7090
message:
7091
- "React Hook useEffect has a missing dependency: 'myEffect'. " +
7092
- 'Either include it or remove the dependency array.',
7093
- suggestions: [
7094
- {
7095
- desc: 'Update the dependencies array to be: [myEffect]',
7096
- output: normalizeIndent`
7097
- function MyComponent({myEffect}) {
7098
- useEffect(myEffect, [myEffect]);
7099
- }
7100
- `,
7101
- },
7102
- ],
7091
+ 'React Hook useEffect received a function whose dependencies are unknown. Pass an inline function instead.',
7092
},
7093
],
7094
},
@@ -7670,6 +7659,19 @@ const tests = {
7659
},
7660
],
7661
},
7662
+ {
7663
+ code: normalizeIndent`
7664
+ function useCustomCallback(callback, deps) {
7665
+ return useCallback(callback, deps)
7666
+ }
7667
+ `,
7668
+ errors: [
7669
+ {
7670
+ message:
7671
+ 'React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.',
7672
+ },
7673
+ ],
7674
+ },
7675
],
7676
};
7677
@@ -8193,6 +8195,19 @@ const testsTypescript = {
8195
},
8196
],
8197
},
8198
+ {
8199
+ code: normalizeIndent`
8200
+ function useCustomCallback(callback, deps) {
8201
+ return useCallback(callback as any, deps)
8202
+ }
8203
+ `,
8204
+ errors: [
8205
+ {
8206
+ message:
8207
+ 'React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.',
8208
+ },
8209
+ ],
8210
+ },
8211
],
8212
};
8213
@@ -8271,6 +8286,10 @@ if (!process.env.CI) {
8286
testsFlow.invalid = testsFlow.invalid.filter(predicate);
8287
testsTypescript.valid = testsTypescript.valid.filter(predicate);
8288
testsTypescript.invalid = testsTypescript.invalid.filter(predicate);
8289
+ testsTypescriptEslintParserV4.valid =
8290
+ testsTypescriptEslintParserV4.valid.filter(predicate);
8291
+ testsTypescriptEslintParserV4.invalid =
8292
+ testsTypescriptEslintParserV4.invalid.filter(predicate);
8293
}
8294
8295
describe('rules-of-hooks/exhaustive-deps', () => {
packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+24
-13
@@ -652,6 +652,7 @@ export default {
652
const isTSAsArrayExpression =
653
declaredDependenciesNode.type === 'TSAsExpression' &&
654
declaredDependenciesNode.expression.type === 'ArrayExpression';
655
+
656
if (!isArrayExpression && !isTSAsArrayExpression) {
657
// If the declared dependencies are not an array expression then we
658
// can't verify that the user provided the correct dependencies. Tell
@@ -1196,7 +1197,7 @@ export default {
1197
// Not a React Hook call that needs deps.
1198
return;
1199
}
1199
- const callback = node.arguments[callbackIndex];
1200
+ let callback = node.arguments[callbackIndex];
1201
const reactiveHook = node.callee;
1202
const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
1203
const maybeNode = node.arguments[callbackIndex + 1];
@@ -1241,6 +1242,13 @@ export default {
1242
return;
1243
}
1244
1245
+ while (
1246
+ callback.type === 'TSAsExpression' ||
1247
+ callback.type === 'AsExpression'
1248
+ ) {
1249
+ callback = callback.expression;
1250
+ }
1251
+
1252
switch (callback.type) {
1253
case 'FunctionExpression':
1254
case 'ArrowFunctionExpression':
@@ -1252,15 +1260,6 @@ export default {
1260
isEffect,
1261
);
1262
return; // Handled
1255
- case 'TSAsExpression':
1256
- visitFunctionWithDependencies(
1257
- callback.expression,
1258
- declaredDependenciesNode,
1259
- reactiveHook,
1260
- reactiveHookName,
1261
- isEffect,
1262
- );
1263
- return; // Handled
1263
case 'Identifier':
1264
if (!declaredDependenciesNode) {
1265
// No deps, no problems.
@@ -1291,6 +1290,13 @@ export default {
1290
if (!def || !def.node) {
1291
break; // Unhandled
1292
}
1293
+ if (def.type === 'Parameter') {
1294
+ reportProblem({
1295
+ node: reactiveHook,
1296
+ message: getUnknownDependenciesMessage(reactiveHookName),
1297
+ });
1298
+ return;
1299
+ }
1300
if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1301
// Parameter or an unusual pattern. Bail out.
1302
break; // Unhandled
@@ -1333,9 +1339,7 @@ export default {
1339
// useEffect(generateEffectBody(), []);
1340
reportProblem({
1341
node: reactiveHook,
1336
- message:
1337
- `React Hook ${reactiveHookName} received a function whose dependencies ` +
1338
- `are unknown. Pass an inline function instead.`,
1342
+ message: getUnknownDependenciesMessage(reactiveHookName),
1343
});
1344
return; // Handled
1345
}
@@ -1912,3 +1916,10 @@ function isUseEffectEventIdentifier(node) {
1916
}
1917
return false;
1918
}
1919
+
1920
+function getUnknownDependenciesMessage(reactiveHookName) {
1921
+ return (
1922
+ `React Hook ${reactiveHookName} received a function whose dependencies ` +
1923
+ `are unknown. Pass an inline function instead.`
1924
+ );
1925
+}