From 35bb88b6aa8fbdbae0501cf174e0e45e0dbe1d62 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Fri, 17 Apr 2026 07:39:24 +0000 Subject: [PATCH] [eslint-plugin-react-hooks] Skip TSQualifiedName chains inside typeof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `exhaustive-deps` skipped references whose parent was a `TSTypeQuery` or `TSTypeReference`, which correctly handled `typeof foo` / `Foo` but not nested access like `typeof foo.bar` — there the root identifier's parent is a `TSQualifiedName`, not the outer `TSTypeQuery`, so the reference slipped through and was reported as a missing dependency. Walk up through any `TSQualifiedName` nodes before checking the ancestor kind, so `typeof foo.bar.baz` and similar chains are now also treated as purely type-level references (fixes #27335). Co-Authored-By: Claude --- .../__tests__/ESLintRuleExhaustiveDeps-test.js | 14 ++++++++++++++ .../src/rules/ExhaustiveDeps.ts | 12 ++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 29e956d314a2..fe759d35303d 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -8009,6 +8009,20 @@ const testsTypescript = { } `, }, + { + // Regression test for facebook/react#27335: typeof with nested + // property access (TSQualifiedName) must not require the root + // identifier as a dependency. + code: normalizeIndent` + function MyComponent() { + const [foo, setFoo] = React.useState<{bar: number}>({bar: 42}); + React.useEffect(() => { + const square = (x: typeof foo.bar) => x * x; + setFoo(previous => ({...previous, bar: square(previous.bar)})); + }, []); + } + `, + }, { code: normalizeIndent` function MyComponent() { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 6b790680608d..dd9b475c5bd5 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -548,9 +548,17 @@ const rule = { }); } + // Skip references that appear inside a TypeScript type position. + // Walk up through TSQualifiedName chains so that nested accesses + // like `typeof foo.bar.baz` or `Foo.Bar.Baz` are also skipped, not + // just the direct `typeof foo` / `Foo` cases (fixes #27335). + let typeAncestor = dependencyNode.parent; + while (typeAncestor?.type === 'TSQualifiedName') { + typeAncestor = typeAncestor.parent; + } if ( - dependencyNode.parent?.type === 'TSTypeQuery' || - dependencyNode.parent?.type === 'TSTypeReference' + typeAncestor?.type === 'TSTypeQuery' || + typeAncestor?.type === 'TSTypeReference' ) { continue; }