Skip to content

Commit d63df03

Browse files
committed
feat(linter/exhaustive-deps): add auto-fixer
1 parent 58fcdf5 commit d63df03

File tree

3 files changed

+147
-14
lines changed

3 files changed

+147
-14
lines changed

crates/oxc_linter/src/context/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,27 @@ impl<'a> LintContext<'a> {
291291
self.diagnostic_with_fix_of_kind(diagnostic, FixKind::Suggestion, fix);
292292
}
293293

294+
/// Report a lint rule violation and provide a suggestion for fixing it.
295+
///
296+
/// The second argument is a [closure] that takes a [`RuleFixer`] and
297+
/// returns something that can turn into a `CompositeFix`.
298+
///
299+
/// Fixes created this way should not create parse errors, but have the
300+
/// potential to change the code's semantics. If your fix is completely safe
301+
/// and definitely does not change semantics, use [`LintContext::diagnostic_with_fix`].
302+
/// If your fix has the potential to create parse errors, use
303+
/// [`LintContext::diagnostic_with_dangerous_fix`].
304+
///
305+
/// [closure]: <https://doc.rust-lang.org/book/ch13-01-closures.html>
306+
#[inline]
307+
pub fn diagnostic_with_dangerous_suggestion<C, F>(&self, diagnostic: OxcDiagnostic, fix: F)
308+
where
309+
C: Into<RuleFix<'a>>,
310+
F: FnOnce(RuleFixer<'_, 'a>) -> C,
311+
{
312+
self.diagnostic_with_fix_of_kind(diagnostic, FixKind::DangerousSuggestion, fix);
313+
}
314+
294315
/// Report a lint rule violation and provide a potentially dangerous
295316
/// automatic fix for it.
296317
///

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

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ declare_oxc_lint!(
232232
/// ```
233233
ExhaustiveDeps,
234234
react,
235-
correctness
235+
correctness,
236+
safe_fixes_and_dangerous_suggestions
236237
);
237238

238239
const 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)]
749750
struct 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]
14391477
fn 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
}

crates/oxc_macros/src/declare_oxc_lint.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,11 @@ fn parse_fix(s: &str) -> proc_macro2::TokenStream {
256256
is_conditional = true;
257257
false
258258
}
259-
"and" | "or" => false, // e.g. fix_or_suggestion
259+
// e.g. "safe_fix". safe is implied
260+
"safe"
261+
// e.g. fix_or_suggestion
262+
| "and" | "or"
263+
=> false,
260264
_ => true,
261265
})
262266
.unique()
@@ -273,8 +277,8 @@ fn parse_fix(s: &str) -> proc_macro2::TokenStream {
273277

274278
fn parse_fix_kind(s: &str) -> proc_macro2::TokenStream {
275279
match s {
276-
"fix" => quote! { FixKind::Fix },
277-
"suggestion" => quote! { FixKind::Suggestion },
280+
"fix" | "fixes" => quote! { FixKind::Fix },
281+
"suggestion" | "suggestions" => quote! { FixKind::Suggestion },
278282
"dangerous" => quote! { FixKind::Dangerous },
279283
_ => panic!("invalid fix kind: {s}. Valid fix kinds are fix, suggestion, or dangerous."),
280284
}

0 commit comments

Comments
 (0)