Skip to content

Commit 06bce8f

Browse files
committed
feat(linter/exhaustive-deps): implement fixer for missing dep (#13782)
Previously, code such as: ``` function Foo() { const local = {} useEffect(() => {}, [local]) } ``` Would not be fixed/have no suggestions. It now has a dangerous suggestion to fix it into: ``` function Foo() { const local = {} useEffect(() => {}, []) } ```
1 parent 12baf5e commit 06bce8f

File tree

1 file changed

+75
-6
lines changed

1 file changed

+75
-6
lines changed

crates/oxc_linter/src/rules/react/exhaustive_deps.rs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,12 @@ impl Rule for ExhaustiveDeps {
628628
if dep.chain.is_empty() && is_symbol_declaration_referentially_unique(symbol_id, ctx) {
629629
let name = ctx.scoping().symbol_name(symbol_id);
630630
let decl_span = ctx.scoping().symbol_span(symbol_id);
631-
ctx.diagnostic(dependency_changes_on_every_render_diagnostic(
632-
hook_name, dep.span, name, decl_span,
633-
));
631+
ctx.diagnostic_with_dangerous_suggestion(
632+
dependency_changes_on_every_render_diagnostic(
633+
hook_name, dep.span, name, decl_span,
634+
),
635+
|fixer| fix::remove_dependency(fixer, &dep, dependencies_node),
636+
);
634637
}
635638
}
636639
}
@@ -1464,10 +1467,16 @@ fn is_inside_effect_cleanup(stack: &[AstType]) -> bool {
14641467
mod fix {
14651468
use super::Name;
14661469
use oxc_allocator::{Allocator, CloneIn};
1467-
use oxc_ast::{AstBuilder, ast::ArrayExpression};
1468-
use oxc_span::{Atom, SPAN};
1470+
use oxc_ast::{
1471+
AstBuilder,
1472+
ast::{ArrayExpression, Expression},
1473+
};
1474+
use oxc_span::{Atom, GetSpan, SPAN};
14691475

1470-
use crate::fixer::{RuleFix, RuleFixer};
1476+
use crate::{
1477+
fixer::{RuleFix, RuleFixer},
1478+
rules::react::exhaustive_deps::Dependency,
1479+
};
14711480

14721481
pub fn append_dependencies<'c, 'a: 'c>(
14731482
fixer: RuleFixer<'c, 'a>,
@@ -1492,6 +1501,29 @@ mod fix {
14921501
codegen.print_expression(&ast_builder.expression_array(SPAN, vec));
14931502
fixer.replace(deps.span, codegen.into_source_text())
14941503
}
1504+
1505+
pub fn remove_dependency<'c, 'a: 'c>(
1506+
fixer: RuleFixer<'c, 'a>,
1507+
dependency: &Dependency,
1508+
deps: &ArrayExpression<'a>,
1509+
) -> RuleFix<'a> {
1510+
let mut codegen = fixer.codegen();
1511+
1512+
let alloc = Allocator::default();
1513+
let ast_builder = AstBuilder::new(&alloc);
1514+
1515+
let new_deps = deps
1516+
.elements
1517+
.iter()
1518+
.filter(|el| (*el).span() != dependency.span)
1519+
.map(|el| el.clone_in(&alloc));
1520+
1521+
codegen.print_expression(&Expression::ArrayExpression(ast_builder.alloc_array_expression(
1522+
deps.span,
1523+
oxc_allocator::Vec::from_iter_in(new_deps, &alloc),
1524+
)));
1525+
fixer.replace(deps.span, codegen.into_source_text())
1526+
}
14951527
}
14961528

14971529
#[test]
@@ -4128,6 +4160,43 @@ fn test() {
41284160
// // None,
41294161
// // FixKind::DangerousSuggestion,
41304162
// ),
4163+
// Test missing dependency fixes
4164+
(
4165+
"function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, []); }",
4166+
"function MyComponent() { const local = someFunc(); useEffect(() => { console.log(local); }, [local]); }",
4167+
),
4168+
(
4169+
"function MyComponent(props) { useEffect(() => { console.log(props.foo); }, []); }",
4170+
"function MyComponent(props) { useEffect(() => { console.log(props.foo); }, [props.foo]); }",
4171+
),
4172+
(
4173+
"function MyComponent(props) { useEffect(() => { console.log(props.foo, props.bar); }, []); }",
4174+
"function MyComponent(props) { useEffect(() => { console.log(props.foo, props.bar); }, [props.foo, props.bar]); }",
4175+
),
4176+
// Test adding to existing dependencies
4177+
(
4178+
"function MyComponent(props) { const local = someFunc(); useEffect(() => { console.log(props.foo, local); }, [props.foo]); }",
4179+
"function MyComponent(props) { const local = someFunc(); useEffect(() => { console.log(props.foo, local); }, [props.foo, local]); }",
4180+
),
4181+
// Test dependency array creation for hooks that require it
4182+
(
4183+
"function MyComponent() { const fn = useCallback(() => { alert('foo'); }); }",
4184+
"function MyComponent() { const fn = useCallback(() => { alert('foo'); }, []); }",
4185+
),
4186+
(
4187+
"function MyComponent() { const value = useMemo(() => { return 2*2; }); }",
4188+
"function MyComponent() { const value = useMemo(() => { return 2*2; }, []); }",
4189+
),
4190+
// Test unnecessary dependency removal for non-effect hooks
4191+
(
4192+
"function MyComponent() { const local1 = {}; useCallback(() => {}, [local1]); }",
4193+
"function MyComponent() { const local1 = {}; useCallback(() => {}, []); }",
4194+
),
4195+
// Test duplicate dependency removal
4196+
(
4197+
"function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local, local]); }",
4198+
"function MyComponent() { const local = {}; useEffect(() => { console.log(local); }, [local]); }",
4199+
),
41314200
];
41324201

41334202
Tester::new(

0 commit comments

Comments
 (0)