diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index c982cb9238ad0..59c927cde2520 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -148,6 +148,18 @@ impl<'a> LintContext<'a> { .map(|(a, _)| a as u32) } + /// Finds the previous occurrence of the given token in the source code, + /// starting from the specified position, skipping over comments. + #[expect(clippy::cast_possible_truncation)] + pub fn find_prev_token_from(&self, start: u32, token: &str) -> Option { + let source = self.source_range(Span::from(0..start)); + + source + .rmatch_indices(token) + .find(|(a, _)| !self.is_inside_comment(*a as u32)) + .map(|(a, _)| a as u32) + } + /// Finds the next occurrence of the given token within a bounded span, /// starting from the specified position, skipping over comments. /// diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs index aec8d09f79663..7191f31ef958a 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs @@ -15,7 +15,7 @@ use options::{IgnorePattern, NoUnusedVarsOptions}; use oxc_ast::AstKind; use oxc_macros::declare_oxc_lint; use oxc_semantic::{AstNode, ScopeFlags, SymbolFlags}; -use oxc_span::GetSpan; +use oxc_span::{GetSpan, Span}; use symbol::Symbol; use crate::{ @@ -338,12 +338,26 @@ impl NoUnusedVars { } ctx.diagnostic(diagnostic::declared(symbol, &self.vars_ignore_pattern, false)); } - AstKind::CatchParameter(_) => { - ctx.diagnostic(diagnostic::declared( - symbol, - &self.caught_errors_ignore_pattern, - false, - )); + AstKind::CatchParameter(catch) => { + // NOTE: these are safe suggestions as deleting unused catch + // bindings wont have any side effects. + ctx.diagnostic_with_suggestion( + diagnostic::declared(symbol, &self.caught_errors_ignore_pattern, false), + |fixer| { + let Span { start, end, .. } = catch.span(); + + let (Some(paren_start), Some(paren_end_offset)) = ( + ctx.find_prev_token_from(start, "("), + ctx.find_next_token_from(end, ")"), + ) else { + return fixer.noop(); + }; + + let paren_end = end + paren_end_offset; + let delete_span = Span::new(paren_start, paren_end + 1); + fixer.delete_range(delete_span) + }, + ); } _ => ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None, false)), } diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs index d23409cc2634c..16bda7e5ab993 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -518,8 +518,51 @@ fn test_vars_catch() { ), ]; + // these suggestion fixes are safe + let fix = vec![ + ("try {} catch (error) { }", "try {} catch { }", None, FixKind::Suggestion), + ( + "try { const x = (1 + 1); } catch (error) { }", + "try { const x = (1 + 1); } catch { }", + None, + FixKind::Suggestion, + ), + ("try {} catch ({ msg }) { }", "try {} catch { }", None, FixKind::Suggestion), + // spacing + ("try {} catch (e) { }", "try {} catch { }", None, FixKind::Suggestion), + ("try {} catch(e){ }", "try {} catch{ }", None, FixKind::Suggestion), + ("try {} catch ( e) { }", "try {} catch { }", None, FixKind::Suggestion), + ("try {} catch ( e \t\n ) { }", "try {} catch { }", None, FixKind::Suggestion), + // comments + ("try {} catch (/* comment() */ e) { }", "try {} catch { }", None, FixKind::Suggestion), + ("try {} catch (e /* comment() */) { }", "try {} catch { }", None, FixKind::Suggestion), + ( + "try {} catch /* comment */ (e) { }", + "try {} catch /* comment */ { }", + None, + FixKind::Suggestion, + ), + ( + r"try {} catch ( + // comment + // () + e) { }", + "try {} catch { }", + None, + FixKind::Suggestion, + ), + // typescript + ("try {} catch (error: Error) { }", "try {} catch { }", None, FixKind::Suggestion), + ( + "try {} catch (error: (typeof thing)[number]) { }", + "try {} catch { }", + None, + FixKind::Suggestion, + ), + ]; + Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail) - .intentionally_allow_no_fix_tests() + .expect_fix(fix) .with_snapshot_suffix("oxc-vars-catch") .test_and_snapshot(); }