Repository: facebook/react
Pull request: fix[eslint-plugin-react-hooks]: Fix error when callback argument is an identifier with an `as` expression

<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`.
  2. Run `yarn` in the repository root.
  3. If you've fixed a bug or added code that should be tested, add tests!
  4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
  5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`.
  6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect".
  7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
  8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

  Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Using eslint-plugin-react-hooks with ESLint v9 and TypeScript ESLint v8 causes the following issue:

```
Oops! Something went wrong! :(

ESLint: 9.11.1

TypeError: Cannot read properties of null (reading 'upper')
Occurred while linting /app/src/hooks/useCustomMemo.tsx:9
Rule: "react-hooks/exhaustive-deps"
    at visitFunctionWithDependencies (/app/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js:1106:38)
    at CallExpression (/app/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js:1920:25)
    at ruleErrorHandler (/app/node_modules/eslint/lib/linter/linter.js:1084:48)
    at /app/node_modules/eslint/lib/linter/safe-emitter.js:45:58
    at Array.forEach (<anonymous>)
    at Object.emit (/app/node_modules/eslint/lib/linter/safe-emitter.js:45:38)
    at NodeEventGenerator.applySelector (/app/node_modules/eslint/lib/linter/node-event-generator.js:297:26)
    at NodeEventGenerator.applySelectors (/app/node_modules/eslint/lib/linter/node-event-generator.js:326:22)
    at NodeEventGenerator.enterNode (/app/node_modules/eslint/lib/linter/node-event-generator.js:337:14)
    at runRules (/app/node_modules/eslint/lib/linter/linter.js:1128:40)
```

I can reproduce this with the above mentioned versions and the following file:


```ts
function useCustomCallback(callback, deps) {
  return useCallback(callback as any, deps)
}
```

## How did you test this change?

<!--
  Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface.
  How exactly did you verify that your PR solves the issue you wanted to solve?
  If you leave this empty, your PR will very likely be closed.
-->

Added a test case.

Changed files:
filename: packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
patch: @@ -7088,18 +7088,7 @@ const tests = {
       errors: [
         {
           message:
-            "React Hook useEffect has a missing dependency: 'myEffect'. " +
-            'Either include it or remove the dependency array.',
-          suggestions: [
-            {
-              desc: 'Update the dependencies array to be: [myEffect]',
-              output: normalizeIndent`
-                function MyComponent({myEffect}) {
-                  useEffect(myEffect, [myEffect]);
-                }
-              `,
-            },
-          ],
+            'React Hook useEffect received a function whose dependencies are unknown. Pass an inline function instead.',
         },
       ],
     },
@@ -7670,6 +7659,19 @@ const tests = {
         },
       ],
     },
+    {
+      code: normalizeIndent`
+        function useCustomCallback(callback, deps) {
+          return useCallback(callback, deps)
+        }
+      `,
+      errors: [
+        {
+          message:
+            'React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.',
+        },
+      ],
+    },
   ],
 };
 
@@ -8193,6 +8195,19 @@ const testsTypescript = {
         },
       ],
     },
+    {
+      code: normalizeIndent`
+        function useCustomCallback(callback, deps) {
+          return useCallback(callback as any, deps)
+        }
+      `,
+      errors: [
+        {
+          message:
+            'React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.',
+        },
+      ],
+    },
   ],
 };
 
@@ -8271,6 +8286,10 @@ if (!process.env.CI) {
   testsFlow.invalid = testsFlow.invalid.filter(predicate);
   testsTypescript.valid = testsTypescript.valid.filter(predicate);
   testsTypescript.invalid = testsTypescript.invalid.filter(predicate);
+  testsTypescriptEslintParserV4.valid =
+    testsTypescriptEslintParserV4.valid.filter(predicate);
+  testsTypescriptEslintParserV4.invalid =
+    testsTypescriptEslintParserV4.invalid.filter(predicate);
 }
 
 describe('rules-of-hooks/exhaustive-deps', () => {

filename: packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
patch: @@ -652,6 +652,7 @@ export default {
       const isTSAsArrayExpression =
         declaredDependenciesNode.type === 'TSAsExpression' &&
         declaredDependenciesNode.expression.type === 'ArrayExpression';
+
       if (!isArrayExpression && !isTSAsArrayExpression) {
         // If the declared dependencies are not an array expression then we
         // can't verify that the user provided the correct dependencies. Tell
@@ -1196,7 +1197,7 @@ export default {
         // Not a React Hook call that needs deps.
         return;
       }
-      const callback = node.arguments[callbackIndex];
+      let callback = node.arguments[callbackIndex];
       const reactiveHook = node.callee;
       const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
       const maybeNode = node.arguments[callbackIndex + 1];
@@ -1241,6 +1242,13 @@ export default {
         return;
       }
 
+      while (
+        callback.type === 'TSAsExpression' ||
+        callback.type === 'AsExpression'
+      ) {
+        callback = callback.expression;
+      }
+
       switch (callback.type) {
         case 'FunctionExpression':
         case 'ArrowFunctionExpression':
@@ -1252,15 +1260,6 @@ export default {
             isEffect,
           );
           return; // Handled
-        case 'TSAsExpression':
-          visitFunctionWithDependencies(
-            callback.expression,
-            declaredDependenciesNode,
-            reactiveHook,
-            reactiveHookName,
-            isEffect,
-          );
-          return; // Handled
         case 'Identifier':
           if (!declaredDependenciesNode) {
             // No deps, no problems.
@@ -1291,6 +1290,13 @@ export default {
           if (!def || !def.node) {
             break; // Unhandled
           }
+          if (def.type === 'Parameter') {
+            reportProblem({
+              node: reactiveHook,
+              message: getUnknownDependenciesMessage(reactiveHookName),
+            });
+            return;
+          }
           if (def.type !== 'Variable' && def.type !== 'FunctionName') {
             // Parameter or an unusual pattern. Bail out.
             break; // Unhandled
@@ -1333,9 +1339,7 @@ export default {
           // useEffect(generateEffectBody(), []);
           reportProblem({
             node: reactiveHook,
-            message:
-              `React Hook ${reactiveHookName} received a function whose dependencies ` +
-              `are unknown. Pass an inline function instead.`,
+            message: getUnknownDependenciesMessage(reactiveHookName),
           });
           return; // Handled
       }
@@ -1912,3 +1916,10 @@ function isUseEffectEventIdentifier(node) {
   }
   return false;
 }
+
+function getUnknownDependenciesMessage(reactiveHookName) {
+  return (
+    `React Hook ${reactiveHookName} received a function whose dependencies ` +
+    `are unknown. Pass an inline function instead.`
+  );
+}