From a3a88350c6240cea6acab13329017c641dbe9683 Mon Sep 17 00:00:00 2001 From: qraqras Date: Sun, 24 Aug 2025 04:45:36 +0000 Subject: [PATCH 1/9] feat(biome_js_analyze): add `noUselessCatchBindings` --- .changeset/pink-coats-jog.md | 12 +++ .../src/analyzer/linter/rules.rs | 75 ++++++++------ .../src/categories.rs | 9 +- crates/biome_js_analyze/src/lint/nursery.rs | 3 +- .../lint/nursery/no_useless_catch_binding.rs | 97 +++++++++++++++++++ .../nursery/noUselessCatchBinding/invalid.js | 5 + .../noUselessCatchBinding/invalid.js.snap | 36 +++++++ .../nursery/noUselessCatchBinding/invalid.ts | 5 + .../noUselessCatchBinding/invalid.ts.snap | 36 +++++++ .../nursery/noUselessCatchBinding/valid.js | 13 +++ .../noUselessCatchBinding/valid.js.snap | 22 +++++ .../nursery/noUselessCatchBinding/valid.ts | 13 +++ .../noUselessCatchBinding/valid.ts.snap | 22 +++++ crates/biome_rule_options/src/lib.rs | 1 + .../src/no_useless_catch_binding.rs | 6 ++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 27 +++++- .../@biomejs/biome/configuration_schema.json | 32 ++++++ 17 files changed, 378 insertions(+), 36 deletions(-) create mode 100644 .changeset/pink-coats-jog.md create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap create mode 100644 crates/biome_rule_options/src/no_useless_catch_binding.rs diff --git a/.changeset/pink-coats-jog.md b/.changeset/pink-coats-jog.md new file mode 100644 index 000000000000..1e0a7803c924 --- /dev/null +++ b/.changeset/pink-coats-jog.md @@ -0,0 +1,12 @@ +--- +"@biomejs/biome": minor +--- + +Fixed #6476: Added the new rule `noUselessCatchBindings`. This rule disallows unnecessary catch bindings. + +```diff +try { + // Do something +- } catch (unused) {} ++ } catch {} +``` diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 1988b07bc0f4..d569a05ae2a5 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -293,6 +293,7 @@ pub enum RuleName { NoUnusedVariables, NoUnwantedPolyfillio, NoUselessCatch, + NoUselessCatchBinding, NoUselessConstructor, NoUselessContinue, NoUselessElse, @@ -650,6 +651,7 @@ impl RuleName { Self::NoUnusedVariables => "noUnusedVariables", Self::NoUnwantedPolyfillio => "noUnwantedPolyfillio", Self::NoUselessCatch => "noUselessCatch", + Self::NoUselessCatchBinding => "noUselessCatchBinding", Self::NoUselessConstructor => "noUselessConstructor", Self::NoUselessContinue => "noUselessContinue", Self::NoUselessElse => "noUselessElse", @@ -1003,6 +1005,7 @@ impl RuleName { Self::NoUnusedVariables => RuleGroup::Correctness, Self::NoUnwantedPolyfillio => RuleGroup::Performance, Self::NoUselessCatch => RuleGroup::Complexity, + Self::NoUselessCatchBinding => RuleGroup::Nursery, Self::NoUselessConstructor => RuleGroup::Complexity, Self::NoUselessContinue => RuleGroup::Complexity, Self::NoUselessElse => RuleGroup::Style, @@ -1365,6 +1368,7 @@ impl std::str::FromStr for RuleName { "noUnusedVariables" => Ok(Self::NoUnusedVariables), "noUnwantedPolyfillio" => Ok(Self::NoUnwantedPolyfillio), "noUselessCatch" => Ok(Self::NoUselessCatch), + "noUselessCatchBinding" => Ok(Self::NoUselessCatchBinding), "noUselessConstructor" => Ok(Self::NoUselessConstructor), "noUselessContinue" => Ok(Self::NoUselessContinue), "noUselessElse" => Ok(Self::NoUselessElse), @@ -4594,7 +4598,7 @@ impl From for Correctness { #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", default, deny_unknown_fields)] #[doc = r" A list of rules that belong to this group"] -pub struct Nursery { # [doc = r" It enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> } +pub struct Nursery { # [doc = r" It enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Succinct description of the rule."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> } impl Nursery { const GROUP_NAME: &'static str = "nursery"; pub(crate) const GROUP_RULES: &'static [&'static str] = &[ @@ -4608,6 +4612,7 @@ impl Nursery { "noShadow", "noUnnecessaryConditions", "noUnresolvedImports", + "noUselessCatchBinding", "noUselessUndefined", "noVueDataObjectDeclaration", "noVueReservedKeys", @@ -4648,6 +4653,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), ]; } impl RuleGroupExt for Nursery { @@ -4709,71 +4715,76 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_useless_undefined.as_ref() + if let Some(rule) = self.no_useless_catch_binding.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_vue_data_object_declaration.as_ref() + if let Some(rule) = self.no_useless_undefined.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_data_object_declaration.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_vue_reserved_props.as_ref() + if let Some(rule) = self.no_vue_reserved_keys.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } - if let Some(rule) = self.use_anchor_href.as_ref() + if let Some(rule) = self.no_vue_reserved_props.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } - if let Some(rule) = self.use_consistent_type_definitions.as_ref() + if let Some(rule) = self.use_anchor_href.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_consistent_type_definitions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.use_explicit_type.as_ref() + if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - if let Some(rule) = self.use_image_size.as_ref() + if let Some(rule) = self.use_explicit_type.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_image_size.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } - if let Some(rule) = self.use_qwik_classlist.as_ref() + if let Some(rule) = self.use_max_params.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - if let Some(rule) = self.use_react_function_components.as_ref() + if let Some(rule) = self.use_qwik_classlist.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } - if let Some(rule) = self.use_sorted_classes.as_ref() + if let Some(rule) = self.use_react_function_components.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } + if let Some(rule) = self.use_sorted_classes.as_ref() + && rule.is_enabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { @@ -4828,71 +4839,76 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_useless_undefined.as_ref() + if let Some(rule) = self.no_useless_catch_binding.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_vue_data_object_declaration.as_ref() + if let Some(rule) = self.no_useless_undefined.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_data_object_declaration.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_vue_reserved_props.as_ref() + if let Some(rule) = self.no_vue_reserved_keys.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } - if let Some(rule) = self.use_anchor_href.as_ref() + if let Some(rule) = self.no_vue_reserved_props.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } - if let Some(rule) = self.use_consistent_type_definitions.as_ref() + if let Some(rule) = self.use_anchor_href.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_consistent_type_definitions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.use_explicit_type.as_ref() + if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - if let Some(rule) = self.use_image_size.as_ref() + if let Some(rule) = self.use_explicit_type.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_image_size.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } - if let Some(rule) = self.use_qwik_classlist.as_ref() + if let Some(rule) = self.use_max_params.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - if let Some(rule) = self.use_react_function_components.as_ref() + if let Some(rule) = self.use_qwik_classlist.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } - if let Some(rule) = self.use_sorted_classes.as_ref() + if let Some(rule) = self.use_react_function_components.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } + if let Some(rule) = self.use_sorted_classes.as_ref() + && rule.is_disabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4963,6 +4979,10 @@ impl RuleGroupExt for Nursery { .no_unresolved_imports .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noUselessCatchBinding" => self + .no_useless_catch_binding + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUselessUndefined" => self .no_useless_undefined .as_ref() @@ -5033,6 +5053,7 @@ impl From for Nursery { no_shadow: Some(value.into()), no_unnecessary_conditions: Some(value.into()), no_unresolved_imports: Some(value.into()), + no_useless_catch_binding: Some(value.into()), no_useless_undefined: Some(value.into()), no_vue_data_object_declaration: Some(value.into()), no_vue_reserved_keys: Some(value.into()), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index f8745843c7aa..051a23f8668e 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -149,25 +149,25 @@ define_categories! { "lint/correctness/noVoidElementsWithChildren": "https://biomejs.dev/linter/rules/no-void-elements-with-children", "lint/correctness/noVoidTypeReturn": "https://biomejs.dev/linter/rules/no-void-type-return", "lint/correctness/useExhaustiveDependencies": "https://biomejs.dev/linter/rules/use-exhaustive-dependencies", + "lint/correctness/useGraphqlNamedOperations": "https://biomejs.dev/linter/rules/use-graphql-named-operations", "lint/correctness/useHookAtTopLevel": "https://biomejs.dev/linter/rules/use-hook-at-top-level", "lint/correctness/useImportExtensions": "https://biomejs.dev/linter/rules/use-import-extensions", "lint/correctness/useIsNan": "https://biomejs.dev/linter/rules/use-is-nan", "lint/correctness/useJsonImportAttributes": "https://biomejs.dev/linter/rules/use-json-import-attributes", "lint/correctness/useJsxKeyInIterable": "https://biomejs.dev/linter/rules/use-jsx-key-in-iterable", - "lint/correctness/useGraphqlNamedOperations": "https://biomejs.dev/linter/rules/use-graphql-named-operations", "lint/correctness/useParseIntRadix": "https://biomejs.dev/linter/rules/use-parse-int-radix", "lint/correctness/useSingleJsDocAsterisk": "https://biomejs.dev/linter/rules/use-single-js-doc-asterisk", "lint/correctness/useUniqueElementIds": "https://biomejs.dev/linter/rules/use-unique-element-ids", "lint/correctness/useValidForDirection": "https://biomejs.dev/linter/rules/use-valid-for-direction", "lint/correctness/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", - "lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", "lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises", "lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion", "lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles", "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", "lint/nursery/noMisusedPromises": "https://biomejs.dev/linter/rules/no-misused-promises", + "lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component", "lint/nursery/noNonNullAssertedOptionalChain": "https://biomejs.dev/linter/rules/no-non-null-asserted-optional-chain", "lint/nursery/noQwikUseVisibleTask": "https://biomejs.dev/linter/rules/no-qwik-use-visible-task", "lint/nursery/noSecrets": "https://biomejs.dev/linter/rules/no-secrets", @@ -176,6 +176,7 @@ define_categories! { "lint/nursery/noUnresolvedImports": "https://biomejs.dev/linter/rules/no-unresolved-imports", "lint/nursery/noUnwantedPolyfillio": "https://biomejs.dev/linter/rules/no-unwanted-polyfillio", "lint/nursery/noUselessBackrefInRegex": "https://biomejs.dev/linter/rules/no-useless-backref-in-regex", + "lint/nursery/noUselessCatchBinding": "https://biomejs.dev/linter/rules/no-useless-catch-binding", "lint/nursery/noUselessUndefined": "https://biomejs.dev/linter/rules/no-useless-undefined", "lint/nursery/noVueDataObjectDeclaration": "https://biomejs.dev/linter/rules/no-vue-data-object-declaration", "lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys", @@ -359,20 +360,20 @@ define_categories! { "lint/suspicious/noUnknownAtRules": "https://biomejs.dev/linter/rules/no-unknown-at-rules", "lint/suspicious/noUnsafeDeclarationMerging": "https://biomejs.dev/linter/rules/no-unsafe-declaration-merging", "lint/suspicious/noUnsafeNegation": "https://biomejs.dev/linter/rules/no-unsafe-negation", - "lint/suspicious/noUselessRegexBackrefs": "https://biomejs.dev/linter/rules/no-useless-regex-backrefs", "lint/suspicious/noUselessEscapeInString": "https://biomejs.dev/linter/rules/no-useless-escape-in-string", + "lint/suspicious/noUselessRegexBackrefs": "https://biomejs.dev/linter/rules/no-useless-regex-backrefs", "lint/suspicious/noVar": "https://biomejs.dev/linter/rules/no-var", "lint/suspicious/noWith": "https://biomejs.dev/linter/rules/no-with", "lint/suspicious/useAdjacentOverloadSignatures": "https://biomejs.dev/linter/rules/use-adjacent-overload-signatures", "lint/suspicious/useAwait": "https://biomejs.dev/linter/rules/use-await", "lint/suspicious/useBiomeIgnoreFolder": "https://biomejs.dev/linter/rules/use-biome-ignore-folder", - "lint/suspicious/useIterableCallbackReturn": "https://biomejs.dev/linter/rules/use-iterable-callback-return", "lint/suspicious/useDefaultSwitchClauseLast": "https://biomejs.dev/linter/rules/use-default-switch-clause-last", "lint/suspicious/useErrorMessage": "https://biomejs.dev/linter/rules/use-error-message", "lint/suspicious/useGetterReturn": "https://biomejs.dev/linter/rules/use-getter-return", "lint/suspicious/useGoogleFontDisplay": "https://biomejs.dev/linter/rules/use-google-font-display", "lint/suspicious/useGuardForIn": "https://biomejs.dev/linter/rules/use-guard-for-in", "lint/suspicious/useIsArray": "https://biomejs.dev/linter/rules/use-is-array", + "lint/suspicious/useIterableCallbackReturn": "https://biomejs.dev/linter/rules/use-iterable-callback-return", "lint/suspicious/useNamespaceKeyword": "https://biomejs.dev/linter/rules/use-namespace-keyword", "lint/suspicious/useNumberToFixedDigitsArgument": "https://biomejs.dev/linter/rules/use-number-to-fixed-digits-argument", "lint/suspicious/useStaticResponseMethods": "https://biomejs.dev/linter/rules/use-static-response-methods", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 3ebaf8df98dc..245502f07ce4 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -13,6 +13,7 @@ pub mod no_secrets; pub mod no_shadow; pub mod no_unnecessary_conditions; pub mod no_unresolved_imports; +pub mod no_useless_catch_binding; pub mod no_useless_undefined; pub mod no_vue_data_object_declaration; pub mod no_vue_reserved_keys; @@ -26,4 +27,4 @@ pub mod use_max_params; pub mod use_qwik_classlist; pub mod use_react_function_components; pub mod use_sorted_classes; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs new file mode 100644 index 000000000000..fdbc8dc1d3ea --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs @@ -0,0 +1,97 @@ +use crate::JsRuleAction; +use crate::lint::correctness::no_unused_variables::is_unused; +use crate::services::semantic::Semantic; +use biome_analyze::{FixKind, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule}; +use biome_console::markup; +use biome_js_syntax::{JsCatchDeclaration, binding_ext::AnyJsIdentifierBinding}; +use biome_rowan::{AstNode, BatchMutationExt}; +use biome_rule_options::no_useless_catch_binding::NoUselessCatchBindingOptions; + +declare_lint_rule! { + /// Disallow unused catch bindings. + /// + /// This rule enforces removing unnecessary catch bindings in accordance with ECMAScript 2019. + /// See also: [Optional catch binding](https://tc39.es/proposal-optional-catch-binding) + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// try { + /// // Do something + /// } catch (unused) {} + /// ``` + /// + /// ### Valid + /// + /// ```js + /// try { + /// // Do something + /// } catch (used) { + /// console.error(used); + /// } + /// ``` + /// + /// ```js + /// try { + /// // Do something + /// } catch {} + /// ``` + /// + pub NoUselessCatchBinding { + version: "next", + name: "noUselessCatchBinding", + language: "js", + recommended: false, + fix_kind: FixKind::Unsafe, + } +} + +impl Rule for NoUselessCatchBinding { + type Query = Semantic; + type State = (); + type Signals = Option; + type Options = NoUselessCatchBindingOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let catch_declaration = ctx.query(); + let catch_binding = catch_declaration.binding().ok()?; + let catch_binding_ident = catch_binding + .as_any_js_binding()? + .as_js_identifier_binding()?; + let catch_binding_any_ident = AnyJsIdentifierBinding::from(catch_binding_ident.clone()); + if !is_unused(ctx.model(), &catch_binding_any_ident) { + return None; + } + Some(()) + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let catch_declaration = ctx.query(); + Some( + RuleDiagnostic::new( + rule_category!(), + catch_declaration.range(), + markup! { + "This ""catch binding"" is unused." + }, + ) + .note(markup! { + "Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it." + }), + ) + } + + fn action(ctx: &RuleContext, _state: &Self::State) -> Option { + let mut mutation = ctx.root().begin(); + let catch_declaration = ctx.query(); + mutation.remove_node(catch_declaration.clone()); + Some(JsRuleAction::new( + ctx.metadata().action_category(ctx.category(), ctx.group()), + ctx.metadata().applicability(), + markup! { "Remove the catch binding." }.to_owned(), + mutation, + )) + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js new file mode 100644 index 000000000000..79e099d8d20d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js @@ -0,0 +1,5 @@ +try { + // Do something +} catch (unused) { + // Do something +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap new file mode 100644 index 000000000000..662938449865 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap @@ -0,0 +1,36 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: invalid.js +--- +# Input +```js +try { + // Do something +} catch (unused) { + // Do something +} + +``` + +# Diagnostics +``` +invalid.js:3:9 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 1 │ try { + 2 │ // Do something + > 3 │ } catch (unused) { + │ ^^^^^^^^ + 4 │ // Do something + 5 │ } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 3 │ }·catch·(unused)·{ + │ --------- + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts new file mode 100644 index 000000000000..79e099d8d20d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts @@ -0,0 +1,5 @@ +try { + // Do something +} catch (unused) { + // Do something +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap new file mode 100644 index 000000000000..26b78b07691e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap @@ -0,0 +1,36 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: invalid.ts +--- +# Input +```ts +try { + // Do something +} catch (unused) { + // Do something +} + +``` + +# Diagnostics +``` +invalid.ts:3:9 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 1 │ try { + 2 │ // Do something + > 3 │ } catch (unused) { + │ ^^^^^^^^ + 4 │ // Do something + 5 │ } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 3 │ }·catch·(unused)·{ + │ --------- + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js new file mode 100644 index 000000000000..cbb295eb0bfc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js @@ -0,0 +1,13 @@ +// should not generate diagnostics + +try { + // Do something +} catch (used) { + console.error(used); +} + +try { + // Do something +} catch { + // Do something +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap new file mode 100644 index 000000000000..5996cc1e3149 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap @@ -0,0 +1,22 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: valid.js +--- +# Input +```js +// should not generate diagnostics + +try { + // Do something +} catch (used) { + console.error(used); +} + +try { + // Do something +} catch { + // Do something +} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts new file mode 100644 index 000000000000..cbb295eb0bfc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts @@ -0,0 +1,13 @@ +// should not generate diagnostics + +try { + // Do something +} catch (used) { + console.error(used); +} + +try { + // Do something +} catch { + // Do something +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap new file mode 100644 index 000000000000..75a61d13ec97 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap @@ -0,0 +1,22 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: valid.ts +--- +# Input +```ts +// should not generate diagnostics + +try { + // Do something +} catch (used) { + console.error(used); +} + +try { + // Do something +} catch { + // Do something +} + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 0a34aaf83e6a..71147f85c345 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -206,6 +206,7 @@ pub mod no_unused_template_literal; pub mod no_unused_variables; pub mod no_unwanted_polyfillio; pub mod no_useless_catch; +pub mod no_useless_catch_binding; pub mod no_useless_constructor; pub mod no_useless_continue; pub mod no_useless_else; diff --git a/crates/biome_rule_options/src/no_useless_catch_binding.rs b/crates/biome_rule_options/src/no_useless_catch_binding.rs new file mode 100644 index 000000000000..26ece33b9134 --- /dev/null +++ b/crates/biome_rule_options/src/no_useless_catch_binding.rs @@ -0,0 +1,6 @@ +use biome_deserialize_macros::Deserializable; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct NoUselessCatchBindingOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 15bb9ec7a4ec..ee87b60390b2 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1665,6 +1665,10 @@ export interface Nursery { * Warn when importing non-existing exports. */ noUnresolvedImports?: RuleConfiguration_for_NoUnresolvedImportsOptions; + /** + * Succinct description of the rule. + */ + noUselessCatchBinding?: RuleConfiguration_for_NoUselessCatchBindingOptions; /** * Disallow the use of useless undefined. */ @@ -2988,6 +2992,9 @@ export type RuleConfiguration_for_NoUnnecessaryConditionsOptions = export type RuleConfiguration_for_NoUnresolvedImportsOptions = | RulePlainConfiguration | RuleWithOptions_for_NoUnresolvedImportsOptions; +export type RuleConfiguration_for_NoUselessCatchBindingOptions = + | RulePlainConfiguration + | RuleWithOptions_for_NoUselessCatchBindingOptions; export type RuleFixConfiguration_for_NoUselessUndefinedOptions = | RulePlainConfiguration | RuleWithFixOptions_for_NoUselessUndefinedOptions; @@ -5420,6 +5427,16 @@ export interface RuleWithOptions_for_NoUnresolvedImportsOptions { */ options: NoUnresolvedImportsOptions; } +export interface RuleWithOptions_for_NoUselessCatchBindingOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: NoUselessCatchBindingOptions; +} export interface RuleWithFixOptions_for_NoUselessUndefinedOptions { /** * The kind of the code actions emitted by the rule @@ -7985,6 +8002,7 @@ export interface NoSecretsOptions { export interface NoShadowOptions {} export interface NoUnnecessaryConditionsOptions {} export interface NoUnresolvedImportsOptions {} +export interface NoUselessCatchBindingOptions {} export interface NoUselessUndefinedOptions {} export interface NoVueDataObjectDeclarationOptions {} export interface NoVueReservedKeysOptions {} @@ -8681,25 +8699,25 @@ export type Category = | "lint/correctness/noVoidElementsWithChildren" | "lint/correctness/noVoidTypeReturn" | "lint/correctness/useExhaustiveDependencies" + | "lint/correctness/useGraphqlNamedOperations" | "lint/correctness/useHookAtTopLevel" | "lint/correctness/useImportExtensions" | "lint/correctness/useIsNan" | "lint/correctness/useJsonImportAttributes" | "lint/correctness/useJsxKeyInIterable" - | "lint/correctness/useGraphqlNamedOperations" | "lint/correctness/useParseIntRadix" | "lint/correctness/useSingleJsDocAsterisk" | "lint/correctness/useUniqueElementIds" | "lint/correctness/useValidForDirection" | "lint/correctness/useValidTypeof" | "lint/correctness/useYield" - | "lint/nursery/noNextAsyncClientComponent" | "lint/nursery/noColorInvalidHex" | "lint/nursery/noFloatingPromises" | "lint/nursery/noImplicitCoercion" | "lint/nursery/noImportCycles" | "lint/nursery/noMissingGenericFamilyKeyword" | "lint/nursery/noMisusedPromises" + | "lint/nursery/noNextAsyncClientComponent" | "lint/nursery/noNonNullAssertedOptionalChain" | "lint/nursery/noQwikUseVisibleTask" | "lint/nursery/noSecrets" @@ -8708,6 +8726,7 @@ export type Category = | "lint/nursery/noUnresolvedImports" | "lint/nursery/noUnwantedPolyfillio" | "lint/nursery/noUselessBackrefInRegex" + | "lint/nursery/noUselessCatchBinding" | "lint/nursery/noUselessUndefined" | "lint/nursery/noVueDataObjectDeclaration" | "lint/nursery/noVueReservedKeys" @@ -8891,20 +8910,20 @@ export type Category = | "lint/suspicious/noUnknownAtRules" | "lint/suspicious/noUnsafeDeclarationMerging" | "lint/suspicious/noUnsafeNegation" - | "lint/suspicious/noUselessRegexBackrefs" | "lint/suspicious/noUselessEscapeInString" + | "lint/suspicious/noUselessRegexBackrefs" | "lint/suspicious/noVar" | "lint/suspicious/noWith" | "lint/suspicious/useAdjacentOverloadSignatures" | "lint/suspicious/useAwait" | "lint/suspicious/useBiomeIgnoreFolder" - | "lint/suspicious/useIterableCallbackReturn" | "lint/suspicious/useDefaultSwitchClauseLast" | "lint/suspicious/useErrorMessage" | "lint/suspicious/useGetterReturn" | "lint/suspicious/useGoogleFontDisplay" | "lint/suspicious/useGuardForIn" | "lint/suspicious/useIsArray" + | "lint/suspicious/useIterableCallbackReturn" | "lint/suspicious/useNamespaceKeyword" | "lint/suspicious/useNumberToFixedDigitsArgument" | "lint/suspicious/useStaticResponseMethods" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index b88197bcd22f..c398a2ddf1a5 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -4605,6 +4605,16 @@ "type": "object", "additionalProperties": false }, + "NoUselessCatchBindingConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithNoUselessCatchBindingOptions" } + ] + }, + "NoUselessCatchBindingOptions": { + "type": "object", + "additionalProperties": false + }, "NoUselessCatchConfiguration": { "anyOf": [ { "$ref": "#/definitions/RulePlainConfiguration" }, @@ -4972,6 +4982,13 @@ { "type": "null" } ] }, + "noUselessCatchBinding": { + "description": "Succinct description of the rule.", + "anyOf": [ + { "$ref": "#/definitions/NoUselessCatchBindingConfiguration" }, + { "type": "null" } + ] + }, "noUselessUndefined": { "description": "Disallow the use of useless undefined.", "anyOf": [ @@ -9068,6 +9085,21 @@ }, "additionalProperties": false }, + "RuleWithNoUselessCatchBindingOptions": { + "type": "object", + "required": ["level"], + "properties": { + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] + }, + "options": { + "description": "Rule's options", + "allOf": [{ "$ref": "#/definitions/NoUselessCatchBindingOptions" }] + } + }, + "additionalProperties": false + }, "RuleWithNoUselessCatchOptions": { "type": "object", "required": ["level"], From e37e490d21634220ce8920303588b008c4c539a4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 07:50:50 +0000 Subject: [PATCH 2/9] [autofix.ci] apply automated fixes --- .../src/analyzer/linter/rules.rs | 2 +- packages/@biomejs/backend-jsonrpc/src/workspace.ts | 14 +++++++++----- packages/@biomejs/biome/configuration_schema.json | 6 +++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index d569a05ae2a5..87b2db94f8e7 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -4598,7 +4598,7 @@ impl From for Correctness { #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", default, deny_unknown_fields)] #[doc = r" A list of rules that belong to this group"] -pub struct Nursery { # [doc = r" It enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Succinct description of the rule."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> } +pub struct Nursery { # [doc = r" It enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow unused catch bindings."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> } impl Nursery { const GROUP_NAME: &'static str = "nursery"; pub(crate) const GROUP_RULES: &'static [&'static str] = &[ diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index ee87b60390b2..ec5f6b513490 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1666,9 +1666,9 @@ export interface Nursery { */ noUnresolvedImports?: RuleConfiguration_for_NoUnresolvedImportsOptions; /** - * Succinct description of the rule. + * Disallow unused catch bindings. */ - noUselessCatchBinding?: RuleConfiguration_for_NoUselessCatchBindingOptions; + noUselessCatchBinding?: RuleFixConfiguration_for_NoUselessCatchBindingOptions; /** * Disallow the use of useless undefined. */ @@ -2992,9 +2992,9 @@ export type RuleConfiguration_for_NoUnnecessaryConditionsOptions = export type RuleConfiguration_for_NoUnresolvedImportsOptions = | RulePlainConfiguration | RuleWithOptions_for_NoUnresolvedImportsOptions; -export type RuleConfiguration_for_NoUselessCatchBindingOptions = +export type RuleFixConfiguration_for_NoUselessCatchBindingOptions = | RulePlainConfiguration - | RuleWithOptions_for_NoUselessCatchBindingOptions; + | RuleWithFixOptions_for_NoUselessCatchBindingOptions; export type RuleFixConfiguration_for_NoUselessUndefinedOptions = | RulePlainConfiguration | RuleWithFixOptions_for_NoUselessUndefinedOptions; @@ -5427,7 +5427,11 @@ export interface RuleWithOptions_for_NoUnresolvedImportsOptions { */ options: NoUnresolvedImportsOptions; } -export interface RuleWithOptions_for_NoUselessCatchBindingOptions { +export interface RuleWithFixOptions_for_NoUselessCatchBindingOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind; /** * The severity of the emitted diagnostics by the rule */ diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index c398a2ddf1a5..a93a732618b2 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -4983,7 +4983,7 @@ ] }, "noUselessCatchBinding": { - "description": "Succinct description of the rule.", + "description": "Disallow unused catch bindings.", "anyOf": [ { "$ref": "#/definitions/NoUselessCatchBindingConfiguration" }, { "type": "null" } @@ -9089,6 +9089,10 @@ "type": "object", "required": ["level"], "properties": { + "fix": { + "description": "The kind of the code actions emitted by the rule", + "anyOf": [{ "$ref": "#/definitions/FixKind" }, { "type": "null" }] + }, "level": { "description": "The severity of the emitted diagnostics by the rule", "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] From e7a5cd2f81b2b5bd39cd9aefc10baf9e1d8ae652 Mon Sep 17 00:00:00 2001 From: qraqras Date: Mon, 25 Aug 2025 12:42:40 +0000 Subject: [PATCH 3/9] Address reviewer feedback --- .changeset/pink-coats-jog.md | 2 +- .../lint/nursery/no_useless_catch_binding.rs | 78 +++++-- .../nursery/noUselessCatchBinding/invalid.js | 26 ++- .../noUselessCatchBinding/invalid.js.snap | 217 ++++++++++++++++-- .../nursery/noUselessCatchBinding/invalid.ts | 26 ++- .../noUselessCatchBinding/invalid.ts.snap | 217 ++++++++++++++++-- .../nursery/noUselessCatchBinding/valid.js | 26 ++- .../noUselessCatchBinding/valid.js.snap | 26 ++- .../nursery/noUselessCatchBinding/valid.ts | 26 ++- .../noUselessCatchBinding/valid.ts.snap | 26 ++- .../src/no_useless_catch_binding.rs | 2 + 11 files changed, 576 insertions(+), 96 deletions(-) diff --git a/.changeset/pink-coats-jog.md b/.changeset/pink-coats-jog.md index 1e0a7803c924..32a236263f10 100644 --- a/.changeset/pink-coats-jog.md +++ b/.changeset/pink-coats-jog.md @@ -2,7 +2,7 @@ "@biomejs/biome": minor --- -Fixed #6476: Added the new rule `noUselessCatchBindings`. This rule disallows unnecessary catch bindings. +Fixed #6476: Added the new rule `noUselessCatchBinding`. This rule disallows unnecessary catch bindings. ```diff try { diff --git a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs index fdbc8dc1d3ea..20c779196c5c 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs @@ -3,15 +3,17 @@ use crate::lint::correctness::no_unused_variables::is_unused; use crate::services::semantic::Semantic; use biome_analyze::{FixKind, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule}; use biome_console::markup; -use biome_js_syntax::{JsCatchDeclaration, binding_ext::AnyJsIdentifierBinding}; -use biome_rowan::{AstNode, BatchMutationExt}; +use biome_js_syntax::{ + JsCatchClause, JsCatchClauseFields, JsCatchDeclaration, binding_ext::AnyJsIdentifierBinding, +}; +use biome_rowan::{AstNode, BatchMutationExt, trim_leading_trivia_pieces}; use biome_rule_options::no_useless_catch_binding::NoUselessCatchBindingOptions; declare_lint_rule! { /// Disallow unused catch bindings. /// - /// This rule enforces removing unnecessary catch bindings in accordance with ECMAScript 2019. - /// See also: [Optional catch binding](https://tc39.es/proposal-optional-catch-binding) + /// This rule disallows unnecessary catch bindings in accordance with ECMAScript 2019. + /// See also: the ECMAScript 2019 “optional catch binding” feature in the language specification. /// /// ## Examples /// @@ -23,6 +25,18 @@ declare_lint_rule! { /// } catch (unused) {} /// ``` /// + /// ```js,expect_diagnostic + /// try { + /// // Do something + /// } catch ({ unused }) {} + /// ``` + /// + /// ```js,expect_diagnostic + /// try { + /// // Do something + /// } catch ({ unused1, unused2 }) {} + /// ``` + /// /// ### Valid /// /// ```js @@ -36,6 +50,22 @@ declare_lint_rule! { /// ```js /// try { /// // Do something + /// } catch ({ used }) { + /// console.error(used); + /// } + /// ``` + /// + /// ```js + /// try { + /// // Do something + /// } catch ({ used, unused }) { + /// console.error(used); + /// } + /// ``` + /// + /// ```js + /// try { + /// // Do something /// } catch {} /// ``` /// @@ -55,16 +85,20 @@ impl Rule for NoUselessCatchBinding { type Options = NoUselessCatchBindingOptions; fn run(ctx: &RuleContext) -> Self::Signals { + let model = ctx.model(); let catch_declaration = ctx.query(); let catch_binding = catch_declaration.binding().ok()?; - let catch_binding_ident = catch_binding - .as_any_js_binding()? - .as_js_identifier_binding()?; - let catch_binding_any_ident = AnyJsIdentifierBinding::from(catch_binding_ident.clone()); - if !is_unused(ctx.model(), &catch_binding_any_ident) { - return None; + + let mut idents: Vec = Vec::new(); + for descendant in catch_binding.syntax().descendants() { + if let Some(ident) = AnyJsIdentifierBinding::cast(descendant) { + idents.push(ident); + } } - Some(()) + if idents.iter().all(|ident| is_unused(model, ident)) { + return Some(()); + } + None } fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { @@ -85,8 +119,26 @@ impl Rule for NoUselessCatchBinding { fn action(ctx: &RuleContext, _state: &Self::State) -> Option { let mut mutation = ctx.root().begin(); - let catch_declaration = ctx.query(); - mutation.remove_node(catch_declaration.clone()); + let node = ctx.query(); + + let catch_clause = node.syntax().parent()?; + let JsCatchClauseFields { + catch_token, + declaration, + .. + } = JsCatchClause::cast(catch_clause)?.as_fields(); + + let catch_token = catch_token.ok()?; + let declaration = declaration?; + let catch_token_replacement = + catch_token + .clone() + .append_trivia_pieces(trim_leading_trivia_pieces( + declaration.syntax().last_trailing_trivia()?.pieces(), + )); + mutation.remove_node(declaration); + mutation.replace_token_discard_trivia(catch_token, catch_token_replacement); + Some(JsRuleAction::new( ctx.metadata().action_category(ctx.category(), ctx.group()), ctx.metadata().applicability(), diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js index 79e099d8d20d..f4a14559c4ec 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js @@ -1,5 +1,21 @@ -try { - // Do something -} catch (unused) { - // Do something -} +// should not generate diagnostics + +try { /* ... */ } catch (unused) { } + +try { /* ... */ } catch (_unused) { } + + +try { /* ... */ } catch ({ unused }) { } + +try { /* ... */ } catch ({ _unused }) { } + +try { /* ... */ } catch ({ unused, _unused }) { } + + +try { /* ... */ } catch (/* leading inner */ unused /* trailing inner */) { } + +try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap index 662938449865..e0edbe33af57 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap @@ -5,32 +5,221 @@ expression: invalid.js --- # Input ```js -try { - // Do something -} catch (unused) { - // Do something -} + + +try { /* ... */ } catch (unused) { } + +try { /* ... */ } catch (_unused) { } + + +try { /* ... */ } catch ({ unused }) { } + +try { /* ... */ } catch ({ _unused }) { } + +try { /* ... */ } catch ({ unused, _unused }) { } + + +try { /* ... */ } catch (/* leading inner */ unused /* trailing inner */) { } + +try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } ``` # Diagnostics ``` -invalid.js:3:9 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.js:3:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 3 │ try { /* ... */ } catch (unused) { } + │ ^^^^^^^^ + 4 │ + 5 │ try { /* ... */ } catch (_unused) { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 3 │ try·{·/*·...·*/·}·catch·(unused)·{·} + │ --------- + +``` + +``` +invalid.js:5:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 3 │ try { /* ... */ } catch (unused) { } + 4 │ + > 5 │ try { /* ... */ } catch (_unused) { } + │ ^^^^^^^^^ + 6 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 5 │ try·{·/*·...·*/·}·catch·(_unused)·{·} + │ ---------- + +``` + +``` +invalid.js:8:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 8 │ try { /* ... */ } catch ({ unused }) { } + │ ^^^^^^^^^^^^ + 9 │ + 10 │ try { /* ... */ } catch ({ _unused }) { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 8 │ try·{·/*·...·*/·}·catch·({·unused·})·{·} + │ ------------- + +``` + +``` +invalid.js:10:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i This catch binding is unused. - 1 │ try { - 2 │ // Do something - > 3 │ } catch (unused) { - │ ^^^^^^^^ - 4 │ // Do something - 5 │ } + 8 │ try { /* ... */ } catch ({ unused }) { } + 9 │ + > 10 │ try { /* ... */ } catch ({ _unused }) { } + │ ^^^^^^^^^^^^^ + 11 │ + 12 │ try { /* ... */ } catch ({ unused, _unused }) { } i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. i Unsafe fix: Remove the catch binding. - 3 │ }·catch·(unused)·{ - │ --------- + 10 │ try·{·/*·...·*/·}·catch·({·_unused·})·{·} + │ -------------- + +``` + +``` +invalid.js:12:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 10 │ try { /* ... */ } catch ({ _unused }) { } + 11 │ + > 12 │ try { /* ... */ } catch ({ unused, _unused }) { } + │ ^^^^^^^^^^^^^^^^^^^^^ + 13 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 12 │ try·{·/*·...·*/·}·catch·({·unused,·_unused·})·{·} + │ ---------------------- + +``` + +``` +invalid.js:15:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 15 │ try { /* ... */ } catch (/* leading inner */ unused /* trailing inner */) { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ + 17 │ try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 15 │ try·{·/*·...·*/·}·catch·(/*·leading·inner·*/·unused·/*·trailing·inner·*/)·{·} + │ -------------------------------------------------- + +``` + +``` +invalid.js:17:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 15 │ try { /* ... */ } catch (/* leading inner */ unused /* trailing inner */) { } + 16 │ + > 17 │ try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } + │ ^^^^^^^^ + 18 │ + 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 15 15 │ try { /* ... */ } catch (/* leading inner */ unused /* trailing inner */) { } + 16 16 │ + 17 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(unused)·/*·trailing·outer·*/·{·} + 17 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} + 18 18 │ + 19 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + + +``` + +``` +invalid.js:19:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 17 │ try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } + 18 │ + > 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 20 │ + 21 │ try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 17 17 │ try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } + 18 18 │ + 19 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(/*·leading·inner·*/·unused·/*·trailing·inner·*/)·/*·trailing·outer·*/·{·} + 19 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} + 20 20 │ + 21 21 │ try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + + +``` + +``` +invalid.js:21:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + 20 │ + > 21 │ try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 22 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 19 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } + 20 20 │ + 21 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(/*·leading·inner·1·*/·{·/*·leading·inner·2·*/·unused·/*·trailing·inner·2·*/·}·/*·trailing·inner·1·*/)·/*·trailing·outer·*/·{·} + 21 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} + 22 22 │ + ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts index 79e099d8d20d..17d2fff3147a 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts @@ -1,5 +1,21 @@ -try { - // Do something -} catch (unused) { - // Do something -} +// should not generate diagnostics + +try { /* ... */ } catch (unused: any) { } + +try { /* ... */ } catch (_unused: any) { } + + +try { /* ... */ } catch ({ unused }: any) { } + +try { /* ... */ } catch ({ _unused }: any) { } + +try { /* ... */ } catch ({ unused, _unused }: any) { } + + +try { /* ... */ } catch (/* leading inner */ unused: any /* trailing inner */) { } + +try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap index 26b78b07691e..1ec82423a32a 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap @@ -5,32 +5,221 @@ expression: invalid.ts --- # Input ```ts -try { - // Do something -} catch (unused) { - // Do something -} + + +try { /* ... */ } catch (unused: any) { } + +try { /* ... */ } catch (_unused: any) { } + + +try { /* ... */ } catch ({ unused }: any) { } + +try { /* ... */ } catch ({ _unused }: any) { } + +try { /* ... */ } catch ({ unused, _unused }: any) { } + + +try { /* ... */ } catch (/* leading inner */ unused: any /* trailing inner */) { } + +try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + +try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } ``` # Diagnostics ``` -invalid.ts:3:9 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:3:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 3 │ try { /* ... */ } catch (unused: any) { } + │ ^^^^^^^^^^^^^ + 4 │ + 5 │ try { /* ... */ } catch (_unused: any) { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 3 │ try·{·/*·...·*/·}·catch·(unused:·any)·{·} + │ -------------- + +``` + +``` +invalid.ts:5:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 3 │ try { /* ... */ } catch (unused: any) { } + 4 │ + > 5 │ try { /* ... */ } catch (_unused: any) { } + │ ^^^^^^^^^^^^^^ + 6 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 5 │ try·{·/*·...·*/·}·catch·(_unused:·any)·{·} + │ --------------- + +``` + +``` +invalid.ts:8:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 8 │ try { /* ... */ } catch ({ unused }: any) { } + │ ^^^^^^^^^^^^^^^^^ + 9 │ + 10 │ try { /* ... */ } catch ({ _unused }: any) { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 8 │ try·{·/*·...·*/·}·catch·({·unused·}:·any)·{·} + │ ------------------ + +``` + +``` +invalid.ts:10:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i This catch binding is unused. - 1 │ try { - 2 │ // Do something - > 3 │ } catch (unused) { - │ ^^^^^^^^ - 4 │ // Do something - 5 │ } + 8 │ try { /* ... */ } catch ({ unused }: any) { } + 9 │ + > 10 │ try { /* ... */ } catch ({ _unused }: any) { } + │ ^^^^^^^^^^^^^^^^^^ + 11 │ + 12 │ try { /* ... */ } catch ({ unused, _unused }: any) { } i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. i Unsafe fix: Remove the catch binding. - 3 │ }·catch·(unused)·{ - │ --------- + 10 │ try·{·/*·...·*/·}·catch·({·_unused·}:·any)·{·} + │ ------------------- + +``` + +``` +invalid.ts:12:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 10 │ try { /* ... */ } catch ({ _unused }: any) { } + 11 │ + > 12 │ try { /* ... */ } catch ({ unused, _unused }: any) { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 13 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 12 │ try·{·/*·...·*/·}·catch·({·unused,·_unused·}:·any)·{·} + │ --------------------------- + +``` + +``` +invalid.ts:15:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 15 │ try { /* ... */ } catch (/* leading inner */ unused: any /* trailing inner */) { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ + 17 │ try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 15 │ try·{·/*·...·*/·}·catch·(/*·leading·inner·*/·unused:·any·/*·trailing·inner·*/)·{·} + │ ------------------------------------------------------- + +``` + +``` +invalid.ts:17:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 15 │ try { /* ... */ } catch (/* leading inner */ unused: any /* trailing inner */) { } + 16 │ + > 17 │ try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { } + │ ^^^^^^^^^^^^^ + 18 │ + 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 15 15 │ try { /* ... */ } catch (/* leading inner */ unused: any /* trailing inner */) { } + 16 16 │ + 17 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(unused:·any)·/*·trailing·outer·*/·{·} + 17 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} + 18 18 │ + 19 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + + +``` + +``` +invalid.ts:19:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 17 │ try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { } + 18 │ + > 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 20 │ + 21 │ try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 17 17 │ try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { } + 18 18 │ + 19 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(/*·leading·inner·*/·unused:·any·/*·trailing·inner·*/)·/*·trailing·outer·*/·{·} + 19 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} + 20 20 │ + 21 21 │ try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + + +``` + +``` +invalid.ts:21:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + 20 │ + > 21 │ try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 22 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 19 19 │ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } + 20 20 │ + 21 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(/*·leading·inner·1·*/·{·/*·leading·inner·2·*/·unused:·any·/*·trailing·inner·2·*/·}·/*·trailing·inner·1·*/)·/*·trailing·outer·*/·{·} + 21 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} + 22 22 │ + ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js index cbb295eb0bfc..283fdecd67ca 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js @@ -1,13 +1,17 @@ // should not generate diagnostics -try { - // Do something -} catch (used) { - console.error(used); -} - -try { - // Do something -} catch { - // Do something -} +try { /* ... */ } catch (used) { console.error(used); } + +try { /* ... */ } catch ({ used }) { console.error(used); } + +try { /* ... */ } catch ({ used, unused }) { console.error(used); } + + +try { /* ... */ } catch (used) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used }) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used, unused }) { const log = () => console.error(used); log(); } + + +try { /* ... */ } catch { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap index 5996cc1e3149..c601412ffbc2 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap @@ -7,16 +7,20 @@ expression: valid.js ```js // should not generate diagnostics -try { - // Do something -} catch (used) { - console.error(used); -} - -try { - // Do something -} catch { - // Do something -} +try { /* ... */ } catch (used) { console.error(used); } + +try { /* ... */ } catch ({ used }) { console.error(used); } + +try { /* ... */ } catch ({ used, unused }) { console.error(used); } + + +try { /* ... */ } catch (used) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used }) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used, unused }) { const log = () => console.error(used); log(); } + + +try { /* ... */ } catch { } ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts index cbb295eb0bfc..1adf6b5d4b90 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts @@ -1,13 +1,17 @@ // should not generate diagnostics -try { - // Do something -} catch (used) { - console.error(used); -} - -try { - // Do something -} catch { - // Do something -} +try { /* ... */ } catch (used: any) { console.error(used); } + +try { /* ... */ } catch ({ used }: any) { console.error(used); } + +try { /* ... */ } catch ({ used, unused }: any) { console.error(used); } + + +try { /* ... */ } catch (used: any) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used }: any) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used, unused }: any) { const log = () => console.error(used); log(); } + + +try { /* ... */ } catch { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap index 75a61d13ec97..04bd6fc9a4ac 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap @@ -7,16 +7,20 @@ expression: valid.ts ```ts // should not generate diagnostics -try { - // Do something -} catch (used) { - console.error(used); -} - -try { - // Do something -} catch { - // Do something -} +try { /* ... */ } catch (used: any) { console.error(used); } + +try { /* ... */ } catch ({ used }: any) { console.error(used); } + +try { /* ... */ } catch ({ used, unused }: any) { console.error(used); } + + +try { /* ... */ } catch (used: any) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used }: any) { const log = () => console.error(used); log(); } + +try { /* ... */ } catch ({ used, unused }: any) { const log = () => console.error(used); log(); } + + +try { /* ... */ } catch { } ``` diff --git a/crates/biome_rule_options/src/no_useless_catch_binding.rs b/crates/biome_rule_options/src/no_useless_catch_binding.rs index 26ece33b9134..0820c0f33e64 100644 --- a/crates/biome_rule_options/src/no_useless_catch_binding.rs +++ b/crates/biome_rule_options/src/no_useless_catch_binding.rs @@ -3,4 +3,6 @@ use serde::{Deserialize, Serialize}; #[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields, default)] +/// Options for the `noUselessCatchBinding` rule. +/// Currently empty; reserved for future extensions (e.g. allowlist of names). pub struct NoUselessCatchBindingOptions {} From 1b118ad059ae69606829f62c830e123126cdb385 Mon Sep 17 00:00:00 2001 From: qraqras Date: Mon, 25 Aug 2025 12:58:09 +0000 Subject: [PATCH 4/9] Fix file header --- .../tests/specs/nursery/noUselessCatchBinding/invalid.js | 2 +- .../tests/specs/nursery/noUselessCatchBinding/invalid.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js index f4a14559c4ec..eb7be0da81b0 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js @@ -1,4 +1,4 @@ -// should not generate diagnostics +// should generate diagnostics try { /* ... */ } catch (unused) { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts index 17d2fff3147a..795846174f2b 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts @@ -1,4 +1,4 @@ -// should not generate diagnostics +// should generate diagnostics try { /* ... */ } catch (unused: any) { } From 3909df8aeca624daaaa6705b981275ccb92f3545 Mon Sep 17 00:00:00 2001 From: qraqras Date: Mon, 25 Aug 2025 13:11:24 +0000 Subject: [PATCH 5/9] =?UTF-8?q?Apply=20coderabbitai=E2=80=99s=20suggestion?= =?UTF-8?q?=20and=20update=20snapshot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lint/nursery/no_useless_catch_binding.rs | 12 +++++++----- .../nursery/noUselessCatchBinding/invalid.js.snap | 4 +++- .../nursery/noUselessCatchBinding/invalid.ts.snap | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs index 20c779196c5c..815e616e856c 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs @@ -131,11 +131,13 @@ impl Rule for NoUselessCatchBinding { let catch_token = catch_token.ok()?; let declaration = declaration?; let catch_token_replacement = - catch_token - .clone() - .append_trivia_pieces(trim_leading_trivia_pieces( - declaration.syntax().last_trailing_trivia()?.pieces(), - )); + if let Some(trivia) = declaration.syntax().last_trailing_trivia() { + catch_token + .clone() + .append_trivia_pieces(trim_leading_trivia_pieces(trivia.pieces())) + } else { + catch_token.clone() + }; mutation.remove_node(declaration); mutation.replace_token_discard_trivia(catch_token, catch_token_replacement); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap index e0edbe33af57..f6ddbc12f2a0 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap @@ -5,7 +5,7 @@ expression: invalid.js --- # Input ```js - +// should generate diagnostics try { /* ... */ } catch (unused) { } @@ -35,6 +35,8 @@ invalid.js:3:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━ i This catch binding is unused. + 1 │ // should generate diagnostics + 2 │ > 3 │ try { /* ... */ } catch (unused) { } │ ^^^^^^^^ 4 │ diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap index 1ec82423a32a..e650ef4e4d1b 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap @@ -5,7 +5,7 @@ expression: invalid.ts --- # Input ```ts - +// should generate diagnostics try { /* ... */ } catch (unused: any) { } @@ -35,6 +35,8 @@ invalid.ts:3:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━ i This catch binding is unused. + 1 │ // should generate diagnostics + 2 │ > 3 │ try { /* ... */ } catch (unused: any) { } │ ^^^^^^^^^^^^^ 4 │ From 726eabb85bd08811eca3156b79fc2ceb3b7a31bc Mon Sep 17 00:00:00 2001 From: qraqras Date: Mon, 25 Aug 2025 13:32:33 +0000 Subject: [PATCH 6/9] Add tests for edge cases --- .../nursery/noUselessCatchBinding/invalid.js | 7 ++ .../noUselessCatchBinding/invalid.js.snap | 68 +++++++++++++++++++ .../nursery/noUselessCatchBinding/invalid.ts | 7 ++ .../noUselessCatchBinding/invalid.ts.snap | 68 +++++++++++++++++++ .../nursery/noUselessCatchBinding/valid.js | 9 ++- .../noUselessCatchBinding/valid.js.snap | 9 ++- .../nursery/noUselessCatchBinding/valid.ts | 9 ++- .../noUselessCatchBinding/valid.ts.snap | 9 ++- 8 files changed, 182 insertions(+), 4 deletions(-) diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js index eb7be0da81b0..f0f93de2a0b0 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js @@ -19,3 +19,10 @@ try { /* ... */ } catch /* leading outer */ (unused) /* trailing outer */ { } try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trailing inner */) /* trailing outer */ { } try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + + +try { /* ... */ } catch ({ used: alias }) { } + +try { /* ... */ } catch ({ nested: { unused } }) { } + +try { /* ... */ } catch ({ ...rest }) { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap index f6ddbc12f2a0..d405087d212b 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.js.snap @@ -27,6 +27,13 @@ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused /* trail try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + +try { /* ... */ } catch ({ used: alias }) { } + +try { /* ... */ } catch ({ nested: { unused } }) { } + +try { /* ... */ } catch ({ ...rest }) { } + ``` # Diagnostics @@ -222,6 +229,67 @@ invalid.js:21:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━ 21 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(/*·leading·inner·1·*/·{·/*·leading·inner·2·*/·unused·/*·trailing·inner·2·*/·}·/*·trailing·inner·1·*/)·/*·trailing·outer·*/·{·} 21 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} 22 22 │ + 23 23 │ + + +``` + +``` +invalid.js:24:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 24 │ try { /* ... */ } catch ({ used: alias }) { } + │ ^^^^^^^^^^^^^^^^^ + 25 │ + 26 │ try { /* ... */ } catch ({ nested: { unused } }) { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 24 │ try·{·/*·...·*/·}·catch·({·used:·alias·})·{·} + │ ------------------ + +``` + +``` +invalid.js:26:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 24 │ try { /* ... */ } catch ({ used: alias }) { } + 25 │ + > 26 │ try { /* ... */ } catch ({ nested: { unused } }) { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 27 │ + 28 │ try { /* ... */ } catch ({ ...rest }) { } + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 26 │ try·{·/*·...·*/·}·catch·({·nested:·{·unused·}·})·{·} + │ ------------------------- + +``` + +``` +invalid.js:28:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 26 │ try { /* ... */ } catch ({ nested: { unused } }) { } + 27 │ + > 28 │ try { /* ... */ } catch ({ ...rest }) { } + │ ^^^^^^^^^^^^^ + 29 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 28 │ try·{·/*·...·*/·}·catch·({·...rest·})·{·} + │ -------------- ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts index 795846174f2b..25a1d2b7da39 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts @@ -19,3 +19,10 @@ try { /* ... */ } catch /* leading outer */ (unused: any) /* trailing outer */ { try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* trailing inner */) /* trailing outer */ { } try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + + +try { /* ... */ } catch ({ used: alias }: any) { } + +try { /* ... */ } catch ({ nested: { unused } }: any) { } + +try { /* ... */ } catch ({ ...rest }: any) { } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap index e650ef4e4d1b..7633cfb8b587 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/invalid.ts.snap @@ -27,6 +27,13 @@ try { /* ... */ } catch /* leading outer */ (/* leading inner */ unused: any /* try { /* ... */ } catch /* leading outer */ (/* leading inner 1 */ { /* leading inner 2 */ unused: any /* trailing inner 2 */ } /* trailing inner 1 */) /* trailing outer */ { } + +try { /* ... */ } catch ({ used: alias }: any) { } + +try { /* ... */ } catch ({ nested: { unused } }: any) { } + +try { /* ... */ } catch ({ ...rest }: any) { } + ``` # Diagnostics @@ -222,6 +229,67 @@ invalid.ts:21:45 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━ 21 │ - try·{·/*·...·*/·}·catch·/*·leading·outer·*/·(/*·leading·inner·1·*/·{·/*·leading·inner·2·*/·unused:·any·/*·trailing·inner·2·*/·}·/*·trailing·inner·1·*/)·/*·trailing·outer·*/·{·} 21 │ + try·{·/*·...·*/·}·catch·/*·leading·outer·*/·/*·trailing·outer·*/·{·} 22 22 │ + 23 23 │ + + +``` + +``` +invalid.ts:24:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + > 24 │ try { /* ... */ } catch ({ used: alias }: any) { } + │ ^^^^^^^^^^^^^^^^^^^^^^ + 25 │ + 26 │ try { /* ... */ } catch ({ nested: { unused } }: any) { } + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 24 │ try·{·/*·...·*/·}·catch·({·used:·alias·}:·any)·{·} + │ ----------------------- + +``` + +``` +invalid.ts:26:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 24 │ try { /* ... */ } catch ({ used: alias }: any) { } + 25 │ + > 26 │ try { /* ... */ } catch ({ nested: { unused } }: any) { } + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 27 │ + 28 │ try { /* ... */ } catch ({ ...rest }: any) { } + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 26 │ try·{·/*·...·*/·}·catch·({·nested:·{·unused·}·}:·any)·{·} + │ ------------------------------ + +``` + +``` +invalid.ts:28:25 lint/nursery/noUselessCatchBinding FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This catch binding is unused. + + 26 │ try { /* ... */ } catch ({ nested: { unused } }: any) { } + 27 │ + > 28 │ try { /* ... */ } catch ({ ...rest }: any) { } + │ ^^^^^^^^^^^^^^^^^^ + 29 │ + + i Since ECMAScript 2019, catch bindings are optional; you can omit the catch binding if you don't need it. + + i Unsafe fix: Remove the catch binding. + + 28 │ try·{·/*·...·*/·}·catch·({·...rest·}:·any)·{·} + │ ------------------- ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js index 283fdecd67ca..8509d0bef2ad 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js @@ -1,5 +1,8 @@ // should not generate diagnostics +try { /* ... */ } catch { } + + try { /* ... */ } catch (used) { console.error(used); } try { /* ... */ } catch ({ used }) { console.error(used); } @@ -14,4 +17,8 @@ try { /* ... */ } catch ({ used }) { const log = () => console.error(used); log( try { /* ... */ } catch ({ used, unused }) { const log = () => console.error(used); log(); } -try { /* ... */ } catch { } +try { /* ... */ } catch ({ used: alias }) { console.error(alias); } + +try { /* ... */ } catch ({ nested: { unused } }) { console.error(unused); } + +try { /* ... */ } catch ({ ...rest }) { console.error(rest); } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap index c601412ffbc2..aef2ee0619af 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.js.snap @@ -7,6 +7,9 @@ expression: valid.js ```js // should not generate diagnostics +try { /* ... */ } catch { } + + try { /* ... */ } catch (used) { console.error(used); } try { /* ... */ } catch ({ used }) { console.error(used); } @@ -21,6 +24,10 @@ try { /* ... */ } catch ({ used }) { const log = () => console.error(used); log( try { /* ... */ } catch ({ used, unused }) { const log = () => console.error(used); log(); } -try { /* ... */ } catch { } +try { /* ... */ } catch ({ used: alias }) { console.error(alias); } + +try { /* ... */ } catch ({ nested: { unused } }) { console.error(unused); } + +try { /* ... */ } catch ({ ...rest }) { console.error(rest); } ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts index 1adf6b5d4b90..fc478a855063 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts @@ -1,5 +1,8 @@ // should not generate diagnostics +try { /* ... */ } catch { } + + try { /* ... */ } catch (used: any) { console.error(used); } try { /* ... */ } catch ({ used }: any) { console.error(used); } @@ -14,4 +17,8 @@ try { /* ... */ } catch ({ used }: any) { const log = () => console.error(used); try { /* ... */ } catch ({ used, unused }: any) { const log = () => console.error(used); log(); } -try { /* ... */ } catch { } +try { /* ... */ } catch ({ used: alias }: any) { console.error(alias); } + +try { /* ... */ } catch ({ nested: { used } }: any) { console.error(used); } + +try { /* ... */ } catch ({ ...rest }: any) { console.error(rest); } diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap index 04bd6fc9a4ac..bdddcc8aefcf 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessCatchBinding/valid.ts.snap @@ -7,6 +7,9 @@ expression: valid.ts ```ts // should not generate diagnostics +try { /* ... */ } catch { } + + try { /* ... */ } catch (used: any) { console.error(used); } try { /* ... */ } catch ({ used }: any) { console.error(used); } @@ -21,6 +24,10 @@ try { /* ... */ } catch ({ used }: any) { const log = () => console.error(used); try { /* ... */ } catch ({ used, unused }: any) { const log = () => console.error(used); log(); } -try { /* ... */ } catch { } +try { /* ... */ } catch ({ used: alias }: any) { console.error(alias); } + +try { /* ... */ } catch ({ nested: { used } }: any) { console.error(used); } + +try { /* ... */ } catch ({ ...rest }: any) { console.error(rest); } ``` From 1b8ce65b18fd55ee2e01a0c6c3c8d9e5eb895612 Mon Sep 17 00:00:00 2001 From: qraqras Date: Mon, 25 Aug 2025 13:40:35 +0000 Subject: [PATCH 7/9] Avoid allocation when checking bindings --- .../src/lint/nursery/no_useless_catch_binding.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs index 815e616e856c..b273f8ca9c60 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs @@ -89,13 +89,12 @@ impl Rule for NoUselessCatchBinding { let catch_declaration = ctx.query(); let catch_binding = catch_declaration.binding().ok()?; - let mut idents: Vec = Vec::new(); - for descendant in catch_binding.syntax().descendants() { - if let Some(ident) = AnyJsIdentifierBinding::cast(descendant) { - idents.push(ident); - } - } - if idents.iter().all(|ident| is_unused(model, ident)) { + let all_unused = catch_binding + .syntax() + .descendants() + .filter_map(AnyJsIdentifierBinding::cast) + .all(|ident| is_unused(model, &ident)); + if all_unused { return Some(()); } None From faf6153f0091467e5d2cf49c6aaeb2cfaa3f41cd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:14:15 +0000 Subject: [PATCH 8/9] [autofix.ci] apply automated fixes --- packages/@biomejs/backend-jsonrpc/src/workspace.ts | 3 +++ packages/@biomejs/biome/configuration_schema.json | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index ec5f6b513490..7cf2946fbd98 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -8006,6 +8006,9 @@ export interface NoSecretsOptions { export interface NoShadowOptions {} export interface NoUnnecessaryConditionsOptions {} export interface NoUnresolvedImportsOptions {} +/** + * Options for the `noUselessCatchBinding` rule. Currently empty; reserved for future extensions (e.g. allowlist of names). + */ export interface NoUselessCatchBindingOptions {} export interface NoUselessUndefinedOptions {} export interface NoVueDataObjectDeclarationOptions {} diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index a93a732618b2..acd7e8f008c2 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -4612,6 +4612,7 @@ ] }, "NoUselessCatchBindingOptions": { + "description": "Options for the `noUselessCatchBinding` rule. Currently empty; reserved for future extensions (e.g. allowlist of names).", "type": "object", "additionalProperties": false }, From 98dc2aed80d74d3198a22223b5b8a71bab48c1d3 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Thu, 28 Aug 2025 14:38:01 +0100 Subject: [PATCH 9/9] Update .changeset/pink-coats-jog.md --- .changeset/pink-coats-jog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pink-coats-jog.md b/.changeset/pink-coats-jog.md index 32a236263f10..21be2b6eb837 100644 --- a/.changeset/pink-coats-jog.md +++ b/.changeset/pink-coats-jog.md @@ -2,7 +2,7 @@ "@biomejs/biome": minor --- -Fixed #6476: Added the new rule `noUselessCatchBinding`. This rule disallows unnecessary catch bindings. +Added the new nursery rule `noUselessCatchBinding`. This rule disallows unnecessary catch bindings. ```diff try {