@@ -242,7 +242,8 @@ declare_oxc_lint!(
242242 /// ```
243243 ExhaustiveDeps ,
244244 react,
245- correctness
245+ correctness,
246+ safe_fixes_and_dangerous_suggestions
246247) ;
247248
248249const HOOKS_USELESS_WITHOUT_DEPENDENCIES : [ & str ; 2 ] = [ "useCallback" , "useMemo" ] ;
@@ -297,10 +298,10 @@ impl Rule for ExhaustiveDeps {
297298
298299 if dependencies_node. is_none ( ) && !is_effect {
299300 if HOOKS_USELESS_WITHOUT_DEPENDENCIES . contains ( & hook_name. as_str ( ) ) {
300- ctx. diagnostic ( dependency_array_required_diagnostic (
301- hook_name. as_str ( ) ,
302- call_expr . span ( ) ,
303- ) ) ;
301+ ctx. diagnostic_with_fix (
302+ dependency_array_required_diagnostic ( hook_name. as_str ( ) , call_expr . span ( ) ) ,
303+ |fixer| fixer . insert_text_after ( callback_node , ", []" ) ,
304+ ) ;
304305 }
305306 return ;
306307 }
@@ -580,11 +581,11 @@ impl Rule for ExhaustiveDeps {
580581 } ) ;
581582
582583 if undeclared_deps. clone ( ) . count ( ) > 0 {
583- ctx . diagnostic ( missing_dependency_diagnostic (
584- hook_name ,
585- & undeclared_deps . map ( Name :: from ) . collect :: < Vec < _ > > ( ) ,
586- dependencies_node. span ( ) ,
587- ) ) ;
584+ let undeclared = undeclared_deps . map ( Name :: from ) . collect :: < Vec < _ > > ( ) ;
585+ ctx . diagnostic_with_dangerous_suggestion (
586+ missing_dependency_diagnostic ( hook_name , & undeclared , dependencies_node . span ( ) ) ,
587+ |fixer| fix :: append_dependencies ( fixer , & undeclared , dependencies_node. as_ref ( ) ) ,
588+ ) ;
588589 }
589590
590591 // effects are allowed to have extra dependencies
@@ -756,7 +757,7 @@ fn get_node_name_without_react_namespace<'a, 'b>(expr: &'b Expression<'a>) -> Op
756757 }
757758}
758759
759- #[ derive( Debug ) ]
760+ #[ derive( Debug , Clone ) ]
760761struct Name < ' a > {
761762 pub span : Span ,
762763 pub name : Cow < ' a , str > ,
@@ -1446,6 +1447,43 @@ fn is_inside_effect_cleanup(stack: &[AstType]) -> bool {
14461447 false
14471448}
14481449
1450+ mod fix {
1451+ use super :: Name ;
1452+ use itertools:: Itertools ;
1453+ use oxc_ast:: ast:: ArrayExpression ;
1454+ use oxc_span:: GetSpan ;
1455+
1456+ use crate :: fixer:: { RuleFix , RuleFixer } ;
1457+
1458+ pub fn append_dependencies < ' c , ' a : ' c > (
1459+ fixer : RuleFixer < ' c , ' a > ,
1460+ names : & [ Name < ' a > ] ,
1461+ deps : & ArrayExpression < ' a > ,
1462+ ) -> RuleFix < ' a > {
1463+ let names_as_deps = names. iter ( ) . map ( |n| n. name . as_ref ( ) ) . join ( ", " ) ;
1464+ let Some ( last) = deps. elements . last ( ) else {
1465+ return fixer. replace ( deps. span , format ! ( "[{names_as_deps}]" ) ) ;
1466+ } ;
1467+ // look for a trailing comma. we'll need to add one if its not there already
1468+ let mut needs_comma = true ;
1469+ let last_span = last. span ( ) ;
1470+ for c in fixer. source_text ( ) [ ( last_span. end as usize ) ..] . chars ( ) {
1471+ match c {
1472+ ',' => {
1473+ needs_comma = false ;
1474+ break ;
1475+ }
1476+ ']' => break ,
1477+ _ => { } // continue
1478+ }
1479+ }
1480+ fixer. insert_text_after_range (
1481+ last_span,
1482+ if needs_comma { format ! ( ", {names_as_deps}" ) } else { format ! ( " {names_as_deps}" ) } ,
1483+ )
1484+ }
1485+ }
1486+
14491487#[ test]
14501488fn test ( ) {
14511489 use crate :: tester:: Tester ;
@@ -3996,11 +4034,81 @@ fn test() {
39964034 Some ( serde_json:: json!( [ { "additionalHooks" : "useSpecialEffect" } ] ) ) ,
39974035 ) ] ;
39984036
4037+ let fix = vec ! [
4038+ (
4039+ "const useHook = x => useCallback(() => x)" ,
4040+ "const useHook = x => useCallback(() => x, [])" ,
4041+ // None,
4042+ // FixKind::SafeFix,
4043+ ) ,
4044+ (
4045+ "const useHook = x => useCallback(() => { return x; })" ,
4046+ "const useHook = x => useCallback(() => { return x; }, [])" ,
4047+ // None,
4048+ // FixKind::SafeFix,
4049+ ) ,
4050+ (
4051+ r"const useHook = () => {
4052+ const [state, setState] = useState(0);
4053+ const foo = useCallback(() => state, []);
4054+ }" ,
4055+ r"const useHook = () => {
4056+ const [state, setState] = useState(0);
4057+ const foo = useCallback(() => state, [state]);
4058+ }" ,
4059+ // None,
4060+ // FixKind::DangerousSuggestion,
4061+ ) ,
4062+ (
4063+ r"const useHook = () => {
4064+ const [x] = useState(0);
4065+ const [y] = useState(0);
4066+ const foo = useCallback(() => x + y, []);
4067+ }" ,
4068+ r"const useHook = () => {
4069+ const [x] = useState(0);
4070+ const [y] = useState(0);
4071+ const foo = useCallback(() => x + y, [x, y]);
4072+ }" ,
4073+ // None,
4074+ // FixKind::DangerousSuggestion,
4075+ ) ,
4076+ (
4077+ r"const useHook = () => {
4078+ const [x] = useState(0);
4079+ const [y] = useState(0);
4080+ const [z] = useState(0);
4081+ const foo = useCallback(() => x + y + z, [x]);
4082+ }" ,
4083+ r"const useHook = () => {
4084+ const [x] = useState(0);
4085+ const [y] = useState(0);
4086+ const [z] = useState(0);
4087+ const foo = useCallback(() => x + y + z, [x, y, z]);
4088+ }" ,
4089+ // None,
4090+ // FixKind::DangerousSuggestion,
4091+ ) ,
4092+ // (
4093+ // r#"const useHook = () => {
4094+ // const [state, setState] = useState(0);
4095+ // const foo = useCallback(() => state);
4096+ // }"#,
4097+ // r#"const useHook = () => {
4098+ // const [state, setState] = useState(0);
4099+ // const foo = useCallback(() => state, [state]);
4100+ // }"#,
4101+ // // None,
4102+ // // FixKind::DangerousSuggestion,
4103+ // ),
4104+ ] ;
4105+
39994106 Tester :: new (
40004107 ExhaustiveDeps :: NAME ,
40014108 ExhaustiveDeps :: PLUGIN ,
40024109 pass. iter ( ) . map ( |& code| ( code, None ) ) . chain ( pass_additional_hooks) . collect :: < Vec < _ > > ( ) ,
40034110 fail. iter ( ) . map ( |& code| ( code, None ) ) . chain ( fail_additional_hooks) . collect :: < Vec < _ > > ( ) ,
40044111 )
4112+ . expect_fix ( fix)
40054113 . test_and_snapshot ( ) ;
40064114}
0 commit comments