From 79b6571c0af6289f20dafaeb11791177582e97df Mon Sep 17 00:00:00 2001 From: Victor <78874691+victor-teles@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:09:37 -0300 Subject: [PATCH] feat(lint/noUnusedPrivateClassMember): add rule (#585) --- CHANGELOG.md | 7 + .../src/categories.rs | 1 + .../biome_js_analyze/src/analyzers/nursery.rs | 2 + .../no_unused_private_class_members.rs | 365 ++++++++++++++++++ .../noUnusedPrivateClassMembers/invalid.js | 54 +++ .../invalid.js.snap | 296 ++++++++++++++ .../noUnusedPrivateClassMembers/invalid.ts | 35 ++ .../invalid.ts.snap | 203 ++++++++++ .../noUnusedPrivateClassMembers/valid.js | 181 +++++++++ .../noUnusedPrivateClassMembers/valid.js.snap | 191 +++++++++ crates/biome_js_syntax/src/identifier_ext.rs | 21 +- .../src/configuration/linter/rules.rs | 67 +++- .../src/configuration/parse/json/rules.rs | 10 + .../invalid/hooks_incorrect_options.json.snap | 1 + .../invalid/hooks_missing_name.json.snap | 1 + editors/vscode/configuration_schema.json | 7 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + .../components/generated/NumberOfRules.astro | 2 +- .../src/content/docs/internals/changelog.mdx | 7 + .../src/content/docs/linter/rules/index.mdx | 1 + .../rules/no-unused-private-class-members.md | 116 ++++++ 22 files changed, 1557 insertions(+), 23 deletions(-) create mode 100644 crates/biome_js_analyze/src/analyzers/nursery/no_unused_private_class_members.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js.snap create mode 100644 website/src/content/docs/linter/rules/no-unused-private-class-members.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 95fd9747b2b4..b84c129dc9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,13 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom - Fix [#591](https://github.com/biomejs/biome/issues/591) which made [noRedeclare](https://biomejs.dev/linter/rules/no-redeclare) report type parameters with identical names but in different method signatures. Contributed by @Conaclos - Support more a11y roles and fix some methods for a11y lint rules Contributed @nissy-dev - Fix [#609](https://github.com/biomejs/biome/issues/609) `useExhaustiveDependencies`, by removing `useContext`, `useId` and `useSyncExternalStore` from the known hooks. Contributed by @msdlisper +- Fix `useExhaustiveDependencies`, by removing `useContext`, `useId` and `useSyncExternalStore` from the known hooks. Contributed by @msdlisper + +#### New features + +- Add [noUnusedPrivateClassMembers](https://biomejs.dev/linter/rules/no-unused-private-class-members) rule. + The rule disallow unused private class members. + Contributed by @victor-teles ### Parser diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 612a642d109e..d2725659f97b 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -102,6 +102,7 @@ define_categories! { "lint/nursery/noMisrefactoredShorthandAssign": "https://biomejs.dev/linter/rules/no-misrefactored-shorthand-assign", "lint/nursery/noThisInStatic": "https://biomejs.dev/linter/rules/no-this-in-static", "lint/nursery/noUnusedImports": "https://biomejs.dev/linter/rules/no-unused-imports", + "lint/nursery/noUnusedPrivateClassMembers": "https://biomejs.dev/linter/rules/no-unused-private-class-members", "lint/nursery/noUselessElse": "https://biomejs.dev/linter/rules/no-useless-else", "lint/nursery/noUselessLoneBlockStatements": "https://biomejs.dev/linter/rules/no-useless-lone-block-statements", "lint/nursery/useAriaActivedescendantWithTabindex": "https://biomejs.dev/linter/rules/use-aria-activedescendant-with-tabindex", diff --git a/crates/biome_js_analyze/src/analyzers/nursery.rs b/crates/biome_js_analyze/src/analyzers/nursery.rs index 4890cc39b436..c7fbfb4313bf 100644 --- a/crates/biome_js_analyze/src/analyzers/nursery.rs +++ b/crates/biome_js_analyze/src/analyzers/nursery.rs @@ -7,6 +7,7 @@ pub(crate) mod no_empty_block_statements; pub(crate) mod no_empty_character_class_in_regex; pub(crate) mod no_misleading_instantiator; pub(crate) mod no_misrefactored_shorthand_assign; +pub(crate) mod no_unused_private_class_members; pub(crate) mod no_useless_else; pub(crate) mod no_useless_lone_block_statements; pub(crate) mod use_arrow_function; @@ -24,6 +25,7 @@ declare_group! { self :: no_empty_character_class_in_regex :: NoEmptyCharacterClassInRegex , self :: no_misleading_instantiator :: NoMisleadingInstantiator , self :: no_misrefactored_shorthand_assign :: NoMisrefactoredShorthandAssign , + self :: no_unused_private_class_members :: NoUnusedPrivateClassMembers , self :: no_useless_else :: NoUselessElse , self :: no_useless_lone_block_statements :: NoUselessLoneBlockStatements , self :: use_arrow_function :: UseArrowFunction , diff --git a/crates/biome_js_analyze/src/analyzers/nursery/no_unused_private_class_members.rs b/crates/biome_js_analyze/src/analyzers/nursery/no_unused_private_class_members.rs new file mode 100644 index 000000000000..e031eca21387 --- /dev/null +++ b/crates/biome_js_analyze/src/analyzers/nursery/no_unused_private_class_members.rs @@ -0,0 +1,365 @@ +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, +}; +use biome_console::markup; +use biome_diagnostics::Applicability; +use biome_js_syntax::{ + AnyJsClassMember, AnyJsClassMemberName, AnyJsFormalParameter, AnyJsName, + JsAssignmentExpression, JsAssignmentOperator, JsClassDeclaration, JsSyntaxKind, JsSyntaxNode, + TsAccessibilityModifier, TsPropertyParameter, +}; +use biome_rowan::{ + declare_node_union, AstNode, AstNodeList, AstSeparatedList, BatchMutationExt, + SyntaxNodeOptionExt, TextRange, +}; +use rustc_hash::FxHashSet; + +use crate::{utils::is_node_equal, JsRuleAction}; + +declare_rule! { + /// Disallow unused private class members + /// + /// Private class members that are declared and not used anywhere in the code are most likely an error due to incomplete refactoring. + /// Such class members take up space in the code and can lead to confusion by readers. + /// + /// Source: https://eslint.org/docs/latest/rules/no-unused-private-class-members/ + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// class OnlyWrite { + /// #usedOnlyInWrite = 5; + /// + /// method() { + /// this.#usedOnlyInWrite = 212; + /// } + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// class TsBioo { + /// private unusedProperty = 5; + /// + /// private unusedMethod() { + + /// }; + /// } + /// ``` + /// + /// ## Valid + /// + /// ```js + /// class UsedMember { + /// #usedMember = 42; + /// + /// method() { + /// return this.#usedMember; + /// } + /// } + /// ``` + /// + pub(crate) NoUnusedPrivateClassMembers { + version: "next", + name: "noUnusedPrivateClassMembers", + recommended: false, + fix_kind: FixKind::Unsafe, + } +} + +declare_node_union! { + pub(crate) AnyMember = AnyJsClassMember | TsPropertyParameter +} + +impl Rule for NoUnusedPrivateClassMembers { + type Query = Ast; + type State = AnyMember; + type Signals = Vec; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let private_members: FxHashSet = get_all_declared_private_members(node); + + if private_members.is_empty() { + return vec![]; + } + + traverse_members_usage(node.syntax(), private_members) + } + + fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + rule_category!(), + state.property_range(), + markup! { + "This private class member is defined but never used." + }, + )) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let mut mutation = ctx.root().begin(); + + mutation.remove_node(state.clone()); + + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::MaybeIncorrect, + message: markup! { "Remove unused declaration." }.to_owned(), + mutation, + }) + } +} + +/// Check for private member usage +/// if the member usage is found, we remove it from the hashmap +fn traverse_members_usage( + syntax: &JsSyntaxNode, + mut private_members: FxHashSet, +) -> Vec { + let iter = syntax.preorder(); + + for event in iter { + match event { + biome_rowan::WalkEvent::Enter(node) => { + if let Some(js_name) = AnyJsName::cast(node) { + private_members.retain(|private_member| { + let member_being_used = + private_member.match_js_name(&js_name) == Some(true); + let is_write_only = + is_write_only(&js_name) == Some(true) && !private_member.is_accessor(); + let is_in_update_expression = is_in_update_expression(&js_name); + + if member_being_used && is_in_update_expression { + return true; + } + + if member_being_used && is_write_only { + return true; + } + + false + }); + + if private_members.is_empty() { + break; + } + } + } + biome_rowan::WalkEvent::Leave(_) => continue, + } + } + + private_members.into_iter().collect() +} + +fn get_all_declared_private_members( + class_declaration: &JsClassDeclaration, +) -> FxHashSet { + class_declaration + .members() + .iter() + .map(AnyMember::AnyJsClassMember) + .chain(get_constructor_params(class_declaration)) + .filter(|member| member.is_private() == Some(true)) + .collect() +} + +fn get_constructor_params(class_declaration: &JsClassDeclaration) -> FxHashSet { + let constructor_member = class_declaration + .members() + .iter() + .find_map(|member| match member { + AnyJsClassMember::JsConstructorClassMember(member) => Some(member), + _ => None, + }); + + if let Some(constructor_member) = constructor_member { + if let Ok(constructor_params) = constructor_member.parameters() { + return constructor_params + .parameters() + .iter() + .filter_map(|param| match param.ok()? { + biome_js_syntax::AnyJsConstructorParameter::TsPropertyParameter( + ts_property, + ) => Some(ts_property.into()), + _ => None, + }) + .collect(); + } + } + + FxHashSet::default() +} + +/// Check whether the provided `AnyJsName` is part of a potentially write-only assignment expression. +/// This function inspects the syntax tree around the given `AnyJsName` to check whether it is involved in an assignment operation and whether that assignment can be write-only. +/// +/// # Returns +/// +/// - `Some(true)`: If the `js_name` is in a write-only assignment. +/// - `Some(false)`: If the `js_name` is in a assignments that also reads like shorthand operators +/// - `None`: If the parent is not present or grand parent is not a JsAssignmentExpression +/// +/// # Examples of write only expressions +/// +/// ```js +/// this.usedOnlyInWrite = 2; +/// this.usedOnlyInWrite = this.usedOnlyInWrite; +/// ``` +/// +fn is_write_only(js_name: &AnyJsName) -> Option { + let parent = js_name.syntax().parent()?; + let grand_parent = parent.parent()?; + let assignment_expression = JsAssignmentExpression::cast(grand_parent)?; + let left = assignment_expression.left().ok()?; + + if !is_node_equal(left.syntax(), &parent) { + return Some(false); + } + + if !matches!( + assignment_expression.operator(), + Ok(JsAssignmentOperator::Assign) + ) { + let kind = assignment_expression.syntax().parent().kind(); + return Some( + kind.is_some_and(|kind| matches!(kind, JsSyntaxKind::JS_EXPRESSION_STATEMENT)), + ); + } + + Some(true) +} + +fn is_in_update_expression(js_name: &AnyJsName) -> bool { + let grand_parent = js_name.syntax().grand_parent(); + + grand_parent.kind().is_some_and(|kind| { + matches!( + kind, + JsSyntaxKind::JS_POST_UPDATE_EXPRESSION | JsSyntaxKind::JS_PRE_UPDATE_EXPRESSION + ) + }) +} + +impl AnyMember { + fn is_accessor(&self) -> bool { + matches!( + self.syntax().kind(), + JsSyntaxKind::JS_SETTER_CLASS_MEMBER | JsSyntaxKind::JS_GETTER_CLASS_MEMBER + ) + } + + fn is_private(&self) -> Option { + match self { + AnyMember::AnyJsClassMember(member) => { + let is_es_private = matches!( + member.name().ok()??, + AnyJsClassMemberName::JsPrivateClassMemberName(_) + ); + let is_ts_private = match member { + AnyJsClassMember::JsGetterClassMember(member) => member + .modifiers() + .iter() + .filter_map(|x| TsAccessibilityModifier::cast_ref(x.syntax())) + .any(|accessibility| accessibility.is_private()), + AnyJsClassMember::JsMethodClassMember(member) => member + .modifiers() + .iter() + .filter_map(|x| TsAccessibilityModifier::cast_ref(x.syntax())) + .any(|accessibility| accessibility.is_private()), + AnyJsClassMember::JsPropertyClassMember(member) => member + .modifiers() + .iter() + .filter_map(|x| TsAccessibilityModifier::cast_ref(x.syntax())) + .any(|accessibility| accessibility.is_private()), + AnyJsClassMember::JsSetterClassMember(member) => member + .modifiers() + .iter() + .filter_map(|x| TsAccessibilityModifier::cast_ref(x.syntax())) + .any(|accessibility| accessibility.is_private()), + _ => false, + }; + + Some(is_es_private || is_ts_private) + } + AnyMember::TsPropertyParameter(param) => Some( + param + .modifiers() + .iter() + .filter_map(|x| TsAccessibilityModifier::cast_ref(x.syntax())) + .any(|accessibility| accessibility.is_private()), + ), + } + } + + fn property_range(&self) -> Option { + match self { + AnyMember::AnyJsClassMember(member) => match member { + AnyJsClassMember::JsGetterClassMember(member) => Some(member.name().ok()?.range()), + AnyJsClassMember::JsMethodClassMember(member) => Some(member.name().ok()?.range()), + AnyJsClassMember::JsPropertyClassMember(member) => { + Some(member.name().ok()?.range()) + } + AnyJsClassMember::JsSetterClassMember(member) => Some(member.name().ok()?.range()), + _ => None, + }, + AnyMember::TsPropertyParameter(ts_property) => { + match ts_property.formal_parameter().ok()? { + AnyJsFormalParameter::JsBogusParameter(_) => None, + AnyJsFormalParameter::JsFormalParameter(param) => Some( + param + .binding() + .ok()? + .as_any_js_binding()? + .as_js_identifier_binding()? + .name_token() + .ok()? + .text_range(), + ), + } + } + } + } + + fn match_js_name(&self, js_name: &AnyJsName) -> Option { + let value_token = js_name.value_token().ok()?; + let token = value_token.text_trimmed(); + + match self { + AnyMember::AnyJsClassMember(member) => match member { + AnyJsClassMember::JsGetterClassMember(member) => { + Some(member.name().ok()?.name()?.text() == token) + } + AnyJsClassMember::JsMethodClassMember(member) => { + Some(member.name().ok()?.name()?.text() == token) + } + AnyJsClassMember::JsPropertyClassMember(member) => { + Some(member.name().ok()?.name()?.text() == token) + } + AnyJsClassMember::JsSetterClassMember(member) => { + Some(member.name().ok()?.name()?.text() == token) + } + _ => None, + }, + AnyMember::TsPropertyParameter(ts_property) => { + match ts_property.formal_parameter().ok()? { + AnyJsFormalParameter::JsBogusParameter(_) => None, + AnyJsFormalParameter::JsFormalParameter(param) => Some( + param + .binding() + .ok()? + .as_any_js_binding()? + .as_js_identifier_binding()? + .name_token() + .ok()? + .text_trimmed() + == token, + ), + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js new file mode 100644 index 000000000000..a50f161afb3b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js @@ -0,0 +1,54 @@ +class Bioo { + #unusedProperty = 5; + + #unusedMethod() { + + }; +} + +class OnlyWrite { + #usedOnlyInWrite = 5; + + method() { + this.#usedOnlyInWrite = 212; + } +} + +class SelfUpdate { + #usedOnlyToUpdateItself = 5; + + method() { + this.#usedOnlyToUpdateItself++; + } +} + +class Accessor { + get #unusedAccessor() {} + set #unusedAccessor(value) {} +} + +class First { + #unusedMemberInFirstClass = 5; +} + +class Foo { + #usedOnlyInWrite = 5; + method() { + this.#usedOnlyInWrite = 42; + } +} + +class Foo { + #usedOnlyInWriteStatement = 5; + method() { + this.#usedOnlyInWriteStatement += 42; + } +} + +class C { + #usedOnlyInIncrement; + + foo() { + this.#usedOnlyInIncrement++; + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js.snap new file mode 100644 index 000000000000..77556237f904 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.js.snap @@ -0,0 +1,296 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +class Bioo { + #unusedProperty = 5; + + #unusedMethod() { + + }; +} + +class OnlyWrite { + #usedOnlyInWrite = 5; + + method() { + this.#usedOnlyInWrite = 212; + } +} + +class SelfUpdate { + #usedOnlyToUpdateItself = 5; + + method() { + this.#usedOnlyToUpdateItself++; + } +} + +class Accessor { + get #unusedAccessor() {} + set #unusedAccessor(value) {} +} + +class First { + #unusedMemberInFirstClass = 5; +} + +class Foo { + #usedOnlyInWrite = 5; + method() { + this.#usedOnlyInWrite = 42; + } +} + +class Foo { + #usedOnlyInWriteStatement = 5; + method() { + this.#usedOnlyInWriteStatement += 42; + } +} + +class C { + #usedOnlyInIncrement; + + foo() { + this.#usedOnlyInIncrement++; + } +} + +``` + +# Diagnostics +``` +invalid.js:2:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 1 │ class Bioo { + > 2 │ #unusedProperty = 5; + │ ^^^^^^^^^^^^^^^ + 3 │ + 4 │ #unusedMethod() { + + i Unsafe fix: Remove unused declaration. + + 1 1 │ class Bioo { + 2 │ - → #unusedProperty·=·5; + 3 2 │ + 4 3 │ #unusedMethod() { + + +``` + +``` +invalid.js:4:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 2 │ #unusedProperty = 5; + 3 │ + > 4 │ #unusedMethod() { + │ ^^^^^^^^^^^^^ + 5 │ + 6 │ }; + + i Unsafe fix: Remove unused declaration. + + 1 1 │ class Bioo { + 2 │ - → #unusedProperty·=·5; + 3 │ - + 4 │ - → #unusedMethod()·{ + 5 │ - + 6 │ - → }; + 2 │ + → #unusedProperty·=·5;; + 7 3 │ } + 8 4 │ + + +``` + +``` +invalid.js:10:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 9 │ class OnlyWrite { + > 10 │ #usedOnlyInWrite = 5; + │ ^^^^^^^^^^^^^^^^ + 11 │ + 12 │ method() { + + i Unsafe fix: Remove unused declaration. + + 8 8 │ + 9 9 │ class OnlyWrite { + 10 │ - → #usedOnlyInWrite·=·5; + 11 10 │ + 12 11 │ method() { + + +``` + +``` +invalid.js:18:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 17 │ class SelfUpdate { + > 18 │ #usedOnlyToUpdateItself = 5; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 19 │ + 20 │ method() { + + i Unsafe fix: Remove unused declaration. + + 16 16 │ + 17 17 │ class SelfUpdate { + 18 │ - → #usedOnlyToUpdateItself·=·5; + 19 18 │ + 20 19 │ method() { + + +``` + +``` +invalid.js:26:6 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 25 │ class Accessor { + > 26 │ get #unusedAccessor() {} + │ ^^^^^^^^^^^^^^^ + 27 │ set #unusedAccessor(value) {} + 28 │ } + + i Unsafe fix: Remove unused declaration. + + 24 24 │ + 25 25 │ class Accessor { + 26 │ - → get·#unusedAccessor()·{} + 27 │ - → set·#unusedAccessor(value)·{} + 26 │ + → set·#unusedAccessor(value)·{} + 28 27 │ } + 29 28 │ + + +``` + +``` +invalid.js:27:6 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 25 │ class Accessor { + 26 │ get #unusedAccessor() {} + > 27 │ set #unusedAccessor(value) {} + │ ^^^^^^^^^^^^^^^ + 28 │ } + 29 │ + + i Unsafe fix: Remove unused declaration. + + 25 25 │ class Accessor { + 26 26 │ get #unusedAccessor() {} + 27 │ - → set·#unusedAccessor(value)·{} + 28 27 │ } + 29 28 │ + + +``` + +``` +invalid.js:31:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 30 │ class First { + > 31 │ #unusedMemberInFirstClass = 5; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + 32 │ } + 33 │ + + i Unsafe fix: Remove unused declaration. + + 29 29 │ + 30 30 │ class First { + 31 │ - → #unusedMemberInFirstClass·=·5; + 32 31 │ } + 33 32 │ + + +``` + +``` +invalid.js:35:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 34 │ class Foo { + > 35 │ #usedOnlyInWrite = 5; + │ ^^^^^^^^^^^^^^^^ + 36 │ method() { + 37 │ this.#usedOnlyInWrite = 42; + + i Unsafe fix: Remove unused declaration. + + 33 33 │ + 34 34 │ class Foo { + 35 │ - → #usedOnlyInWrite·=·5; + 36 │ - → method()·{ + 35 │ + → method()·{ + 37 36 │ this.#usedOnlyInWrite = 42; + 38 37 │ } + + +``` + +``` +invalid.js:42:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 41 │ class Foo { + > 42 │ #usedOnlyInWriteStatement = 5; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + 43 │ method() { + 44 │ this.#usedOnlyInWriteStatement += 42; + + i Unsafe fix: Remove unused declaration. + + 40 40 │ + 41 41 │ class Foo { + 42 │ - → #usedOnlyInWriteStatement·=·5; + 43 │ - → method()·{ + 42 │ + → method()·{ + 44 43 │ this.#usedOnlyInWriteStatement += 42; + 45 44 │ } + + +``` + +``` +invalid.js:49:2 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 48 │ class C { + > 49 │ #usedOnlyInIncrement; + │ ^^^^^^^^^^^^^^^^^^^^ + 50 │ + 51 │ foo() { + + i Unsafe fix: Remove unused declaration. + + 47 47 │ + 48 48 │ class C { + 49 │ - → #usedOnlyInIncrement; + 50 49 │ + 51 50 │ foo() { + + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts new file mode 100644 index 000000000000..2b9c60cfa6e0 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts @@ -0,0 +1,35 @@ +class TsBioo { + private unusedProperty = 5; + + private unusedMethod() { + + }; +} + +class TSUnusedPrivateConstructor { + constructor(private nusedProperty = 3){ + + } +} + + +class TsOnlyWrite { + private usedOnlyInWrite = 5; + + method() { + this.usedOnlyInWrite = 21; + } +} + +class TsSelfUpdate { + private usedOnlyToUpdateItself = 5; + + method() { + this.usedOnlyToUpdateItself++; + } +} + +class TsAccessor { + private get unusedAccessor() { } + private set unusedAccessor(value) { } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts.snap new file mode 100644 index 000000000000..b04fb90dc0e6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/invalid.ts.snap @@ -0,0 +1,203 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.ts +--- +# Input +```js +class TsBioo { + private unusedProperty = 5; + + private unusedMethod() { + + }; +} + +class TSUnusedPrivateConstructor { + constructor(private nusedProperty = 3){ + + } +} + + +class TsOnlyWrite { + private usedOnlyInWrite = 5; + + method() { + this.usedOnlyInWrite = 21; + } +} + +class TsSelfUpdate { + private usedOnlyToUpdateItself = 5; + + method() { + this.usedOnlyToUpdateItself++; + } +} + +class TsAccessor { + private get unusedAccessor() { } + private set unusedAccessor(value) { } +} + +``` + +# Diagnostics +``` +invalid.ts:2:10 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 1 │ class TsBioo { + > 2 │ private unusedProperty = 5; + │ ^^^^^^^^^^^^^^ + 3 │ + 4 │ private unusedMethod() { + + i Unsafe fix: Remove unused declaration. + + 1 1 │ class TsBioo { + 2 │ - → private·unusedProperty·=·5; + 3 2 │ + 4 3 │ private unusedMethod() { + + +``` + +``` +invalid.ts:4:10 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 2 │ private unusedProperty = 5; + 3 │ + > 4 │ private unusedMethod() { + │ ^^^^^^^^^^^^ + 5 │ + 6 │ }; + + i Unsafe fix: Remove unused declaration. + + 1 1 │ class TsBioo { + 2 │ - → private·unusedProperty·=·5; + 3 │ - + 4 │ - → private·unusedMethod()·{ + 5 │ - + 6 │ - → }; + 2 │ + → private·unusedProperty·=·5;; + 7 3 │ } + 8 4 │ + + +``` + +``` +invalid.ts:10:22 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 9 │ class TSUnusedPrivateConstructor { + > 10 │ constructor(private nusedProperty = 3){ + │ ^^^^^^^^^^^^^^ + 11 │ + 12 │ } + + i Unsafe fix: Remove unused declaration. + + 10 │ → constructor(private·nusedProperty·=·3){ + │ ------------------------- + +``` + +``` +invalid.ts:17:10 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 16 │ class TsOnlyWrite { + > 17 │ private usedOnlyInWrite = 5; + │ ^^^^^^^^^^^^^^^ + 18 │ + 19 │ method() { + + i Unsafe fix: Remove unused declaration. + + 15 15 │ + 16 16 │ class TsOnlyWrite { + 17 │ - → private·usedOnlyInWrite·=·5; + 18 17 │ + 19 18 │ method() { + + +``` + +``` +invalid.ts:25:10 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 24 │ class TsSelfUpdate { + > 25 │ private usedOnlyToUpdateItself = 5; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 26 │ + 27 │ method() { + + i Unsafe fix: Remove unused declaration. + + 23 23 │ + 24 24 │ class TsSelfUpdate { + 25 │ - → private·usedOnlyToUpdateItself·=·5; + 26 25 │ + 27 26 │ method() { + + +``` + +``` +invalid.ts:33:14 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 32 │ class TsAccessor { + > 33 │ private get unusedAccessor() { } + │ ^^^^^^^^^^^^^^ + 34 │ private set unusedAccessor(value) { } + 35 │ } + + i Unsafe fix: Remove unused declaration. + + 31 31 │ + 32 32 │ class TsAccessor { + 33 │ - → private·get·unusedAccessor()·{·} + 34 │ - → private·set·unusedAccessor(value)·{·} + 33 │ + → private·set·unusedAccessor(value)·{·} + 35 34 │ } + 36 35 │ + + +``` + +``` +invalid.ts:34:14 lint/nursery/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 32 │ class TsAccessor { + 33 │ private get unusedAccessor() { } + > 34 │ private set unusedAccessor(value) { } + │ ^^^^^^^^^^^^^^ + 35 │ } + 36 │ + + i Unsafe fix: Remove unused declaration. + + 32 32 │ class TsAccessor { + 33 33 │ private get unusedAccessor() { } + 34 │ - → private·set·unusedAccessor(value)·{·} + 35 34 │ } + 36 35 │ + + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js new file mode 100644 index 000000000000..20f1fdbdc126 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js @@ -0,0 +1,181 @@ +/* should not generate diagnostics */ + +class UsedMember { + #usedMember = 42; + + method() { + return this.#usedMember; + } +} + +class UsedMember { + #usedMethod() { + return 42; + } + + anotherMethod() { + return this.#usedMethod(); + } +} + + +class UsedMember { + get #usedAccessor() {} + set #usedAccessor(value) {} + + method() { + this.#usedAccessor = 42; + } +} + +class UsedMember { + publicMember = 42; +} + +class UsedMember { + #usedMember = 42; + anotherMember = this.#usedMember; +} + +class UsedMember { + #usedMember = 42; + foo() { + this.#usedMember = this.#usedMember; + } +} + +class UsedMember { + #usedMember; + + foo() { + bar(this.#usedMember += 1); + } +} + +class UsedMember { + #usedMember = 42; + method() { + return someGlobalMethod(this.#usedMember); + } +} + +class UsedMember { + #usedInOuterClass; + + foo() { + return class {}; + } + + bar() { + return this.#usedInOuterClass; + } +} + + +class UsedMember { + #usedInForInLoop; + method() { + for (const bar in this.#usedInForInLoop) { + + } + } +} + +class UsedMember { + #usedInForOfLoop; + method() { + for (const bar of this.#usedInForOfLoop) { + + } + } +} + +class UsedMember { + #usedInAssignmentPattern; + method() { + [bar = 1] = this.#usedInAssignmentPattern; + } +} + +class UsedMember { + #usedInArrayPattern; + method() { + [bar] = this.#usedInArrayPattern; + } +} + +class UsedMember { + #usedInAssignmentPattern; + method() { + [bar] = this.#usedInAssignmentPattern; + } +} + +class UsedMember { + #usedInObjectAssignment; + + method() { + ({ [this.#usedInObjectAssignment]: a } = foo); + } +} + +class UsedMember { + set #accessorWithSetterFirst(value) { + doSomething(value); + } + get #accessorWithSetterFirst() { + return something(); + } + method() { + this.#accessorWithSetterFirst += 1; + } +} + +class UsedMember { + set #accessorUsedInMemberAccess(value) {} + + method(a) { + [this.#accessorUsedInMemberAccess] = a; + } +} + +class UsedMember { + get #accessorWithGetterFirst() { + return something(); + } + set #accessorWithGetterFirst(value) { + doSomething(value); + } + method() { + this.#accessorWithGetterFirst += 1; + } +} + +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + } + } +} + +class Foo { + #usedMethod() { + return 42; + } + anotherMethod() { + return this.#usedMethod(); + } +} + +class C { + set #x(value) { + doSomething(value); + } + + foo() { + this.#x = 1; + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js.snap new file mode 100644 index 000000000000..ab69bc891144 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedPrivateClassMembers/valid.js.snap @@ -0,0 +1,191 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +/* should not generate diagnostics */ + +class UsedMember { + #usedMember = 42; + + method() { + return this.#usedMember; + } +} + +class UsedMember { + #usedMethod() { + return 42; + } + + anotherMethod() { + return this.#usedMethod(); + } +} + + +class UsedMember { + get #usedAccessor() {} + set #usedAccessor(value) {} + + method() { + this.#usedAccessor = 42; + } +} + +class UsedMember { + publicMember = 42; +} + +class UsedMember { + #usedMember = 42; + anotherMember = this.#usedMember; +} + +class UsedMember { + #usedMember = 42; + foo() { + this.#usedMember = this.#usedMember; + } +} + +class UsedMember { + #usedMember; + + foo() { + bar(this.#usedMember += 1); + } +} + +class UsedMember { + #usedMember = 42; + method() { + return someGlobalMethod(this.#usedMember); + } +} + +class UsedMember { + #usedInOuterClass; + + foo() { + return class {}; + } + + bar() { + return this.#usedInOuterClass; + } +} + + +class UsedMember { + #usedInForInLoop; + method() { + for (const bar in this.#usedInForInLoop) { + + } + } +} + +class UsedMember { + #usedInForOfLoop; + method() { + for (const bar of this.#usedInForOfLoop) { + + } + } +} + +class UsedMember { + #usedInAssignmentPattern; + method() { + [bar = 1] = this.#usedInAssignmentPattern; + } +} + +class UsedMember { + #usedInArrayPattern; + method() { + [bar] = this.#usedInArrayPattern; + } +} + +class UsedMember { + #usedInAssignmentPattern; + method() { + [bar] = this.#usedInAssignmentPattern; + } +} + +class UsedMember { + #usedInObjectAssignment; + + method() { + ({ [this.#usedInObjectAssignment]: a } = foo); + } +} + +class UsedMember { + set #accessorWithSetterFirst(value) { + doSomething(value); + } + get #accessorWithSetterFirst() { + return something(); + } + method() { + this.#accessorWithSetterFirst += 1; + } +} + +class UsedMember { + set #accessorUsedInMemberAccess(value) {} + + method(a) { + [this.#accessorUsedInMemberAccess] = a; + } +} + +class UsedMember { + get #accessorWithGetterFirst() { + return something(); + } + set #accessorWithGetterFirst(value) { + doSomething(value); + } + method() { + this.#accessorWithGetterFirst += 1; + } +} + +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + } + } +} + +class Foo { + #usedMethod() { + return 42; + } + anotherMethod() { + return this.#usedMethod(); + } +} + +class C { + set #x(value) { + doSomething(value); + } + + foo() { + this.#x = 1; + } +} + +``` + + diff --git a/crates/biome_js_syntax/src/identifier_ext.rs b/crates/biome_js_syntax/src/identifier_ext.rs index ba8a56b3d55b..717a59c830c1 100644 --- a/crates/biome_js_syntax/src/identifier_ext.rs +++ b/crates/biome_js_syntax/src/identifier_ext.rs @@ -1,5 +1,5 @@ use crate::{ - JsIdentifierAssignment, JsLiteralExportName, JsReferenceIdentifier, JsSyntaxToken, + AnyJsName, JsIdentifierAssignment, JsLiteralExportName, JsReferenceIdentifier, JsSyntaxToken, JsxReferenceIdentifier, }; use biome_rowan::{declare_node_union, SyntaxResult}; @@ -23,3 +23,22 @@ impl JsLiteralExportName { Ok(self.value()?.text_trimmed() == "default") } } + +impl AnyJsName { + /// Retrieves the value_token for a given `AnyJsName`. + /// JsName or JsPrivateName + /// ``` + /// use biome_js_syntax::{AnyJsName, JsName, JsPrivateName}; + /// use biome_js_factory::make; + /// + /// let js_name = AnyJsName::JsName(make::js_name(make::ident("request"))); + /// assert!(js_name.value_token().is_ok()); + /// assert_eq!(js_name.value_token().expect("value token text").text(), "request"); + /// ``` + pub fn value_token(&self) -> SyntaxResult { + match self { + AnyJsName::JsName(name) => name.value_token(), + AnyJsName::JsPrivateName(name) => name.value_token(), + } + } +} diff --git a/crates/biome_service/src/configuration/linter/rules.rs b/crates/biome_service/src/configuration/linter/rules.rs index 54003e25c696..7fd594478f9b 100644 --- a/crates/biome_service/src/configuration/linter/rules.rs +++ b/crates/biome_service/src/configuration/linter/rules.rs @@ -2665,6 +2665,15 @@ pub struct Nursery { #[bpaf(long("no-unused-imports"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub no_unused_imports: Option, + #[doc = "Disallow unused private class members"] + #[bpaf( + long("no-unused-private-class-members"), + argument("on|off|warn"), + optional, + hide + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unused_private_class_members: Option, #[doc = "Disallow else block when the if block breaks early."] #[bpaf(long("no-useless-else"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] @@ -2758,6 +2767,9 @@ impl MergeWith for Nursery { if let Some(no_unused_imports) = other.no_unused_imports { self.no_unused_imports = Some(no_unused_imports); } + if let Some(no_unused_private_class_members) = other.no_unused_private_class_members { + self.no_unused_private_class_members = Some(no_unused_private_class_members); + } if let Some(no_useless_else) = other.no_useless_else { self.no_useless_else = Some(no_useless_else); } @@ -2797,7 +2809,7 @@ impl MergeWith for Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 18] = [ + pub(crate) const GROUP_RULES: [&'static str; 19] = [ "noApproximativeNumericConstant", "noDuplicateJsonKeys", "noEmptyBlockStatements", @@ -2808,6 +2820,7 @@ impl Nursery { "noMisrefactoredShorthandAssign", "noThisInStatic", "noUnusedImports", + "noUnusedPrivateClassMembers", "noUselessElse", "noUselessLoneBlockStatements", "useAriaActivedescendantWithTabindex", @@ -2832,12 +2845,12 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 18] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 19] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2856,6 +2869,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { @@ -2922,46 +2936,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_useless_else.as_ref() { + if let Some(rule) = self.no_unused_private_class_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { + if let Some(rule) = self.no_useless_else.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.use_aria_activedescendant_with_tabindex.as_ref() { + if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.use_aria_activedescendant_with_tabindex.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.use_as_const_assertion.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_as_const_assertion.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.use_shorthand_assign.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } + if let Some(rule) = self.use_shorthand_assign.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3016,46 +3035,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_useless_else.as_ref() { + if let Some(rule) = self.no_unused_private_class_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { + if let Some(rule) = self.no_useless_else.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.use_aria_activedescendant_with_tabindex.as_ref() { + if let Some(rule) = self.no_useless_lone_block_statements.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.use_aria_activedescendant_with_tabindex.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.use_as_const_assertion.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_as_const_assertion.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.use_shorthand_assign.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } + if let Some(rule) = self.use_shorthand_assign.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3069,7 +3093,7 @@ impl Nursery { pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 8] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 18] { + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 19] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] @@ -3104,6 +3128,7 @@ impl Nursery { "noMisrefactoredShorthandAssign" => self.no_misrefactored_shorthand_assign.as_ref(), "noThisInStatic" => self.no_this_in_static.as_ref(), "noUnusedImports" => self.no_unused_imports.as_ref(), + "noUnusedPrivateClassMembers" => self.no_unused_private_class_members.as_ref(), "noUselessElse" => self.no_useless_else.as_ref(), "noUselessLoneBlockStatements" => self.no_useless_lone_block_statements.as_ref(), "useAriaActivedescendantWithTabindex" => { diff --git a/crates/biome_service/src/configuration/parse/json/rules.rs b/crates/biome_service/src/configuration/parse/json/rules.rs index e742233cfb19..c3c2f3aedcf8 100644 --- a/crates/biome_service/src/configuration/parse/json/rules.rs +++ b/crates/biome_service/src/configuration/parse/json/rules.rs @@ -848,6 +848,15 @@ impl VisitNode for Nursery { configuration.map_rule_configuration(&value, "noUnusedImports", diagnostics)?; self.no_unused_imports = Some(configuration); } + "noUnusedPrivateClassMembers" => { + let mut configuration = RuleConfiguration::default(); + configuration.map_rule_configuration( + &value, + "noUnusedPrivateClassMembers", + diagnostics, + )?; + self.no_unused_private_class_members = Some(configuration); + } "noUselessElse" => { let mut configuration = RuleConfiguration::default(); configuration.map_rule_configuration(&value, "noUselessElse", diagnostics)?; @@ -920,6 +929,7 @@ impl VisitNode for Nursery { "noMisrefactoredShorthandAssign", "noThisInStatic", "noUnusedImports", + "noUnusedPrivateClassMembers", "noUselessElse", "noUselessLoneBlockStatements", "useAriaActivedescendantWithTabindex", diff --git a/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap b/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap index 579f2259cfa6..8494dfc6cfa0 100644 --- a/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap +++ b/crates/biome_service/tests/invalid/hooks_incorrect_options.json.snap @@ -27,6 +27,7 @@ hooks_incorrect_options.json:6:5 deserialize ━━━━━━━━━━━ - noMisrefactoredShorthandAssign - noThisInStatic - noUnusedImports + - noUnusedPrivateClassMembers - noUselessElse - noUselessLoneBlockStatements - useAriaActivedescendantWithTabindex diff --git a/crates/biome_service/tests/invalid/hooks_missing_name.json.snap b/crates/biome_service/tests/invalid/hooks_missing_name.json.snap index b67aeb71517a..11e430f0ad70 100644 --- a/crates/biome_service/tests/invalid/hooks_missing_name.json.snap +++ b/crates/biome_service/tests/invalid/hooks_missing_name.json.snap @@ -27,6 +27,7 @@ hooks_missing_name.json:6:5 deserialize ━━━━━━━━━━━━━ - noMisrefactoredShorthandAssign - noThisInStatic - noUnusedImports + - noUnusedPrivateClassMembers - noUselessElse - noUselessLoneBlockStatements - useAriaActivedescendantWithTabindex diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 84b0a407e17f..6995a842c7d0 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -1100,6 +1100,13 @@ { "type": "null" } ] }, + "noUnusedPrivateClassMembers": { + "description": "Disallow unused private class members", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUselessElse": { "description": "Disallow else block when the if block breaks early.", "anyOf": [ diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 413e878d27b1..6a85ba53eecf 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -772,6 +772,10 @@ export interface Nursery { * Disallow unused imports. */ noUnusedImports?: RuleConfiguration; + /** + * Disallow unused private class members + */ + noUnusedPrivateClassMembers?: RuleConfiguration; /** * Disallow else block when the if block breaks early. */ @@ -1442,6 +1446,7 @@ export type Category = | "lint/nursery/noMisrefactoredShorthandAssign" | "lint/nursery/noThisInStatic" | "lint/nursery/noUnusedImports" + | "lint/nursery/noUnusedPrivateClassMembers" | "lint/nursery/noUselessElse" | "lint/nursery/noUselessLoneBlockStatements" | "lint/nursery/useAriaActivedescendantWithTabindex" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 84b0a407e17f..6995a842c7d0 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1100,6 +1100,13 @@ { "type": "null" } ] }, + "noUnusedPrivateClassMembers": { + "description": "Disallow unused private class members", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUselessElse": { "description": "Disallow else block when the if block breaks early.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index ddca54d93573..24b833c9c0eb 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 171 rules

\ No newline at end of file +

Biome's linter has a total of 172 rules

\ No newline at end of file diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx index 56d2a8c89c89..8ecf2440e5e6 100644 --- a/website/src/content/docs/internals/changelog.mdx +++ b/website/src/content/docs/internals/changelog.mdx @@ -102,6 +102,13 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom - Fix [#591](https://github.com/biomejs/biome/issues/591) which made [noRedeclare](https://biomejs.dev/linter/rules/no-redeclare) report type parameters with identical names but in different method signatures. Contributed by @Conaclos - Support more a11y roles and fix some methods for a11y lint rules Contributed @nissy-dev - Fix [#609](https://github.com/biomejs/biome/issues/609) `useExhaustiveDependencies`, by removing `useContext`, `useId` and `useSyncExternalStore` from the known hooks. Contributed by @msdlisper +- Fix `useExhaustiveDependencies`, by removing `useContext`, `useId` and `useSyncExternalStore` from the known hooks. Contributed by @msdlisper + +#### New features + +- Add [noUnusedPrivateClassMembers](https://biomejs.dev/linter/rules/no-unused-private-class-members) rule. + The rule disallow unused private class members. + Contributed by @victor-teles ### Parser diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index 08c2e69d2b45..608e0f5bc7cd 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -225,6 +225,7 @@ Rules that belong to this group are not subject to semantic version⚠️ | | [noThisInStatic](/linter/rules/no-this-in-static) | Disallow this and super in static contexts. | ⚠️ | | [noUnusedImports](/linter/rules/no-unused-imports) | Disallow unused imports. | 🔧 | +| [noUnusedPrivateClassMembers](/linter/rules/no-unused-private-class-members) | Disallow unused private class members | ⚠️ | | [noUselessElse](/linter/rules/no-useless-else) | Disallow else block when the if block breaks early. | ⚠️ | | [noUselessLoneBlockStatements](/linter/rules/no-useless-lone-block-statements) | Disallow unnecessary nested block statements. | ⚠️ | | [useAriaActivedescendantWithTabindex](/linter/rules/use-aria-activedescendant-with-tabindex) | Enforce that tabIndex is assigned to non-interactive HTML elements with aria-activedescendant. | | diff --git a/website/src/content/docs/linter/rules/no-unused-private-class-members.md b/website/src/content/docs/linter/rules/no-unused-private-class-members.md new file mode 100644 index 000000000000..3c0cac341bee --- /dev/null +++ b/website/src/content/docs/linter/rules/no-unused-private-class-members.md @@ -0,0 +1,116 @@ +--- +title: noUnusedPrivateClassMembers (since vnext) +--- + +**Diagnostic Category: `lint/nursery/noUnusedPrivateClassMembers`** + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Disallow unused private class members + +Private class members that are declared and not used anywhere in the code are most likely an error due to incomplete refactoring. +Such class members take up space in the code and can lead to confusion by readers. + +Source: https://eslint.org/docs/latest/rules/no-unused-private-class-members/ + +## Examples + +### Invalid + +```jsx +class OnlyWrite { + #usedOnlyInWrite = 5; + + method() { + this.#usedOnlyInWrite = 212; + } +} +``` + +

nursery/noUnusedPrivateClassMembers.js:2:2 lint/nursery/noUnusedPrivateClassMembers  FIXABLE  ━━━━━━━━━━
+
+   This private class member is defined but never used.
+  
+    1 │ class OnlyWrite {
+  > 2 │  #usedOnlyInWrite = 5;
+    ^^^^^^^^^^^^^^^^
+    3 │ 
+    4 │  method() {
+  
+   Unsafe fix: Remove unused declaration.
+  
+    1 1  class OnlyWrite {
+    2  - ·#usedOnlyInWrite·=·5;
+    3 2  
+    4 3   method() {
+  
+
+ +```ts + class TsBioo { + private unusedProperty = 5; + + private unusedMethod() { + }; + } +``` + +
nursery/noUnusedPrivateClassMembers.js:2:12 lint/nursery/noUnusedPrivateClassMembers  FIXABLE  ━━━━━━━━━━
+
+   This private class member is defined but never used.
+  
+    1 │  class TsBioo {
+  > 2 │    private unusedProperty = 5;
+              ^^^^^^^^^^^^^^
+    3 │ 
+    4 │    private unusedMethod() {
+  
+   Unsafe fix: Remove unused declaration.
+  
+    1 1   class TsBioo {
+    2  - ···private·unusedProperty·=·5;
+    3 2  
+    4 3     private unusedMethod() {
+  
+nursery/noUnusedPrivateClassMembers.js:4:12 lint/nursery/noUnusedPrivateClassMembers  FIXABLE  ━━━━━━━━━━
+
+   This private class member is defined but never used.
+  
+    2 │    private unusedProperty = 5;
+    3 │ 
+  > 4 │    private unusedMethod() {
+              ^^^^^^^^^^^^
+    5 │    };
+    6 │  }
+  
+   Unsafe fix: Remove unused declaration.
+  
+    1 1   class TsBioo {
+    2  - ···private·unusedProperty·=·5;
+    3  - 
+    4  - ···private·unusedMethod()·{
+    5  - ···};
+      2+ ···private·unusedProperty·=·5;;
+    6 3   }
+    7 4  
+  
+
+ +## Valid + +```jsx +class UsedMember { + #usedMember = 42; + + method() { + return this.#usedMember; + } +} +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)