Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion crates/oxc_linter/src/rules/react/exhaustive_deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ use oxc_ast::{
},
match_expression,
};
use oxc_ast_visit::{Visit, walk::walk_function_body};
use oxc_ast_visit::{
Visit,
walk::{walk_arrow_function_expression, walk_function, walk_function_body},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::{ReferenceId, ScopeId, Semantic, SymbolId};
use oxc_span::{Atom, GetSpan, Span};
use oxc_syntax::scope::ScopeFlags;

use crate::{
AstNode,
Expand Down Expand Up @@ -1386,6 +1390,24 @@ impl<'a> Visit<'a> for ExhaustiveDepsVisitor<'a, '_> {
self.stack.pop();
}

fn visit_arrow_function_expression(&mut self, it: &ArrowFunctionExpression<'a>) {
// Reset is_callee_of_call_expr when entering a nested function boundary.
// Without this, IIFEs like `(() => { obj.a })()` would incorrectly
// treat property reads inside the arrow body as method call callees,
// collecting `obj` instead of `obj.a`.
let was_callee = self.is_callee_of_call_expr;
self.is_callee_of_call_expr = false;
walk_arrow_function_expression(self, it);
self.is_callee_of_call_expr = was_callee;
}

fn visit_function(&mut self, it: &Function<'a>, flags: ScopeFlags) {
let was_callee = self.is_callee_of_call_expr;
self.is_callee_of_call_expr = false;
walk_function(self, it, flags);
self.is_callee_of_call_expr = was_callee;
}

fn visit_static_member_expression(&mut self, it: &StaticMemberExpression<'a>) {
if it.property.name == "current" && is_inside_effect_cleanup(&self.stack) {
// Safety: this is safe
Expand Down Expand Up @@ -2773,6 +2795,30 @@ fn test() {
}
",
"function MyComponent4({ myRef }) { useCallback(() => { console.log(myRef.current); }, [myRef]); }",
// IIFE: property reads inside arrow IIFE should be collected as member expressions
r"function MyComponent({ obj, flag }) {
return useMemo(() => {
return (() => {
return flag ? obj.a : obj.b;
})();
}, [obj.a, obj.b, flag]);
}",
// IIFE: property reads inside function expression IIFE
r"function MyComponent({ obj }) {
return useMemo(() => {
return (function() {
return obj.a;
})();
}, [obj.a]);
}",
// IIFE: method calls inside IIFE should still collect base object
r"function MyComponent({ obj }) {
return useMemo(() => {
return (() => {
obj.method();
})();
}, [obj]);
}",
];

let fail = vec![
Expand Down Expand Up @@ -4255,6 +4301,14 @@ fn test() {
useCallback(() => { console.log(myRef.current); }, []);
// React Hook useCallback has a missing dependency: 'myRef'. Either include it or remove the dependency array.
}",
// IIFE: missing member expression deps inside arrow IIFE
r"function MyComponent({ obj, flag }) {
return useMemo(() => {
return (() => {
return flag ? obj.a : obj.b;
})();
}, []);
}",
];

let pass_additional_hooks = vec![(
Expand Down
12 changes: 12 additions & 0 deletions crates/oxc_linter/src/snapshots/react_exhaustive_deps.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3124,6 +3124,18 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Either include it or remove the dependency array.

⚠ eslint-plugin-react-hooks(exhaustive-deps): React Hook useMemo has missing dependencies: 'flag', 'obj.b', and 'obj.a'
╭─[exhaustive_deps.tsx:6:14]
3 │ return (() => {
4 │ return flag ? obj.a : obj.b;
· ──── ─── ───
5 │ })();
6 │ }, []);
· ──
7 │ }
╰────
help: Either include it or remove the dependency array.

⚠ eslint-plugin-react-hooks(exhaustive-deps): React Hook useSpecialEffect has a missing dependency: 'state'
╭─[exhaustive_deps.tsx:7:14]
5 │ const someNumber: typeof state = 2;
Expand Down
Loading