Skip to content
Merged
12 changes: 12 additions & 0 deletions .changeset/pink-coats-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@biomejs/biome": minor
---

Added the new nursery rule `noUselessCatchBinding`. This rule disallows unnecessary catch bindings.

```diff
try {
// Do something
- } catch (unused) {}
+ } catch {}
```
75 changes: 48 additions & 27 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ,] } }
150 changes: 150 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_useless_catch_binding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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::{
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 disallows unnecessary catch bindings in accordance with ECMAScript 2019.
/// See also: the ECMAScript 2019 “optional catch binding” feature in the language specification.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// try {
/// // Do something
/// } catch (unused) {}
/// ```
///
/// ```js,expect_diagnostic
/// try {
/// // Do something
/// } catch ({ unused }) {}
/// ```
///
/// ```js,expect_diagnostic
/// try {
/// // Do something
/// } catch ({ unused1, unused2 }) {}
/// ```
///
/// ### Valid
///
/// ```js
/// try {
/// // Do something
/// } catch (used) {
/// console.error(used);
/// }
/// ```
///
/// ```js
/// try {
/// // Do something
/// } catch ({ used }) {
/// console.error(used);
/// }
/// ```
///
/// ```js
/// try {
/// // Do something
/// } catch ({ used, unused }) {
/// 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<JsCatchDeclaration>;
type State = ();
type Signals = Option<Self::State>;
type Options = NoUselessCatchBindingOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let model = ctx.model();
let catch_declaration = ctx.query();
let catch_binding = catch_declaration.binding().ok()?;

let all_unused = catch_binding
.syntax()
.descendants()
.filter_map(AnyJsIdentifierBinding::cast)
.all(|ident| is_unused(model, &ident));
if all_unused {
return Some(());
}
None
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let catch_declaration = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
catch_declaration.range(),
markup! {
"This "<Emphasis>"catch binding"</Emphasis>" 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<Self>, _state: &Self::State) -> Option<JsRuleAction> {
let mut mutation = ctx.root().begin();
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 =
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);

Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Remove the catch binding." }.to_owned(),
mutation,
))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// should 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 */ { }


try { /* ... */ } catch ({ used: alias }) { }

try { /* ... */ } catch ({ nested: { unused } }) { }

try { /* ... */ } catch ({ ...rest }) { }
Loading