@@ -232,7 +232,8 @@ declare_oxc_lint!(
232232 /// ```
233233 ExhaustiveDeps ,
234234 react,
235- correctness
235+ correctness,
236+ safe_fixes_and_dangerous_suggestions
236237) ;
237238
238239const HOOKS_USELESS_WITHOUT_DEPENDENCIES : [ & str ; 2 ] = [ "useCallback" , "useMemo" ] ;
@@ -287,10 +288,10 @@ impl Rule for ExhaustiveDeps {
287288
288289 if dependencies_node. is_none ( ) && !is_effect {
289290 if HOOKS_USELESS_WITHOUT_DEPENDENCIES . contains ( & hook_name. as_str ( ) ) {
290- ctx. diagnostic ( dependency_array_required_diagnostic (
291- hook_name. as_str ( ) ,
292- call_expr . span ( ) ,
293- ) ) ;
291+ ctx. diagnostic_with_fix (
292+ dependency_array_required_diagnostic ( hook_name. as_str ( ) , call_expr . span ( ) ) ,
293+ |fixer| fixer . insert_text_after ( callback_node , ", []" ) ,
294+ ) ;
294295 }
295296 return ;
296297 }
@@ -570,11 +571,11 @@ impl Rule for ExhaustiveDeps {
570571 } ) ;
571572
572573 if undeclared_deps. clone ( ) . count ( ) > 0 {
573- ctx . diagnostic ( missing_dependency_diagnostic (
574- hook_name ,
575- & undeclared_deps . map ( Name :: from ) . collect :: < Vec < _ > > ( ) ,
576- dependencies_node. span ( ) ,
577- ) ) ;
574+ let undeclared = undeclared_deps . map ( Name :: from ) . collect :: < Vec < _ > > ( ) ;
575+ ctx . diagnostic_with_dangerous_suggestion (
576+ missing_dependency_diagnostic ( hook_name , & undeclared , dependencies_node . span ( ) ) ,
577+ |fixer| fix :: append_dependencies ( fixer , & undeclared , dependencies_node. as_ref ( ) ) ,
578+ ) ;
578579 }
579580
580581 // effects are allowed to have extra dependencies
@@ -745,7 +746,7 @@ fn get_node_name_without_react_namespace<'a, 'b>(expr: &'b Expression<'a>) -> Op
745746 }
746747}
747748
748- #[ derive( Debug ) ]
749+ #[ derive( Debug , Clone ) ]
749750struct Name < ' a > {
750751 pub span : Span ,
751752 pub name : Cow < ' a , str > ,
@@ -1435,6 +1436,43 @@ fn is_inside_effect_cleanup(stack: &[AstType]) -> bool {
14351436 false
14361437}
14371438
1439+ mod fix {
1440+ use super :: Name ;
1441+ use itertools:: Itertools ;
1442+ use oxc_ast:: ast:: ArrayExpression ;
1443+ use oxc_span:: GetSpan ;
1444+
1445+ use crate :: fixer:: { RuleFix , RuleFixer } ;
1446+
1447+ pub fn append_dependencies < ' c , ' a : ' c > (
1448+ fixer : RuleFixer < ' c , ' a > ,
1449+ names : & [ Name < ' a > ] ,
1450+ deps : & ArrayExpression < ' a > ,
1451+ ) -> RuleFix < ' a > {
1452+ let names_as_deps = names. iter ( ) . map ( |n| n. name . as_ref ( ) ) . join ( ", " ) ;
1453+ let Some ( last) = deps. elements . last ( ) else {
1454+ return fixer. replace ( deps. span , format ! ( "[{names_as_deps}]" ) ) ;
1455+ } ;
1456+ // look for a trailing comma. we'll need to add one if its not there already
1457+ let mut needs_comma = true ;
1458+ let last_span = last. span ( ) ;
1459+ for c in fixer. source_text ( ) [ ( last_span. end as usize ) ..] . chars ( ) {
1460+ match c {
1461+ ',' => {
1462+ needs_comma = false ;
1463+ break ;
1464+ }
1465+ ']' => break ,
1466+ _ => { } // continue
1467+ }
1468+ }
1469+ fixer. insert_text_after_range (
1470+ last_span,
1471+ if needs_comma { format ! ( ", {names_as_deps}" ) } else { format ! ( " {names_as_deps}" ) } ,
1472+ )
1473+ }
1474+ }
1475+
14381476#[ test]
14391477fn test ( ) {
14401478 use crate :: tester:: Tester ;
@@ -3984,11 +4022,81 @@ fn test() {
39844022 Some ( serde_json:: json!( [ { "additionalHooks" : "useSpecialEffect" } ] ) ) ,
39854023 ) ] ;
39864024
4025+ let fix = vec ! [
4026+ (
4027+ "const useHook = x => useCallback(() => x)" ,
4028+ "const useHook = x => useCallback(() => x, [])" ,
4029+ // None,
4030+ // FixKind::SafeFix,
4031+ ) ,
4032+ (
4033+ "const useHook = x => useCallback(() => { return x; })" ,
4034+ "const useHook = x => useCallback(() => { return x; }, [])" ,
4035+ // None,
4036+ // FixKind::SafeFix,
4037+ ) ,
4038+ (
4039+ r"const useHook = () => {
4040+ const [state, setState] = useState(0);
4041+ const foo = useCallback(() => state, []);
4042+ }" ,
4043+ r"const useHook = () => {
4044+ const [state, setState] = useState(0);
4045+ const foo = useCallback(() => state, [state]);
4046+ }" ,
4047+ // None,
4048+ // FixKind::DangerousSuggestion,
4049+ ) ,
4050+ (
4051+ r"const useHook = () => {
4052+ const [x] = useState(0);
4053+ const [y] = useState(0);
4054+ const foo = useCallback(() => x + y, []);
4055+ }" ,
4056+ r"const useHook = () => {
4057+ const [x] = useState(0);
4058+ const [y] = useState(0);
4059+ const foo = useCallback(() => x + y, [x, y]);
4060+ }" ,
4061+ // None,
4062+ // FixKind::DangerousSuggestion,
4063+ ) ,
4064+ (
4065+ r"const useHook = () => {
4066+ const [x] = useState(0);
4067+ const [y] = useState(0);
4068+ const [z] = useState(0);
4069+ const foo = useCallback(() => x + y + z, [x]);
4070+ }" ,
4071+ r"const useHook = () => {
4072+ const [x] = useState(0);
4073+ const [y] = useState(0);
4074+ const [z] = useState(0);
4075+ const foo = useCallback(() => x + y + z, [x, y, z]);
4076+ }" ,
4077+ // None,
4078+ // FixKind::DangerousSuggestion,
4079+ ) ,
4080+ // (
4081+ // r#"const useHook = () => {
4082+ // const [state, setState] = useState(0);
4083+ // const foo = useCallback(() => state);
4084+ // }"#,
4085+ // r#"const useHook = () => {
4086+ // const [state, setState] = useState(0);
4087+ // const foo = useCallback(() => state, [state]);
4088+ // }"#,
4089+ // // None,
4090+ // // FixKind::DangerousSuggestion,
4091+ // ),
4092+ ] ;
4093+
39874094 Tester :: new (
39884095 ExhaustiveDeps :: NAME ,
39894096 ExhaustiveDeps :: PLUGIN ,
39904097 pass. iter ( ) . map ( |& code| ( code, None ) ) . chain ( pass_additional_hooks) . collect :: < Vec < _ > > ( ) ,
39914098 fail. iter ( ) . map ( |& code| ( code, None ) ) . chain ( fail_additional_hooks) . collect :: < Vec < _ > > ( ) ,
39924099 )
4100+ . expect_fix ( fix)
39934101 . test_and_snapshot ( ) ;
39944102}
0 commit comments