diff --git a/.changeset/no_unused_private_class_members_amended.md b/.changeset/no_unused_private_class_members_amended.md new file mode 100644 index 000000000000..acd6db910f7e --- /dev/null +++ b/.changeset/no_unused_private_class_members_amended.md @@ -0,0 +1,47 @@ +--- +"@biomejs/biome": patch +--- + +Changed [`noUnusedPrivateClassMembers`](https://biomejs.dev/linter/rules/no-unused-private-class-members/) to align more fully with meaningful reads. + +This rule now distinguishes more carefully between writes and reads of private class members. + +- A *meaningful read* is any access that affects program behavior. +- For example, `this.#x += 1` both reads and writes `#x`, so it counts as usage. +- Pure writes without a read (e.g. `this.#x = 1` with no getter) are no longer treated as usage. + +This change ensures that private members are only considered “used” when they are actually read in a way that influences execution. + +***Invalid examples (previously valid)*** + +```ts +class UsedMember { + set #x(value) { + doSomething(value); + } + + foo() { + // This assignment does not actually read #x, because there is no getter. + // Previously, this was considered a usage, but now it’s correctly flagged. + this.#x = 1; + } +} +``` + +***Valid example (Previously invalid)*** + +```js +class Foo { + #usedOnlyInWriteStatement = 5; + + method() { + // This counts as a meaningful read because we both read and write the value. + this.#usedOnlyInWriteStatement += 42; + } +} +``` + +***Summary*** +• Only accesses that read a value are considered meaningful for the purpose of this rule. +• Simple assignments to a setter without a corresponding getter no longer count as usage. +• Operations like +=, method calls returning a value, or reading the property for computation are considered meaningful reads. diff --git a/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs b/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs index 28efac642c93..56804cc90bbc 100644 --- a/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs +++ b/crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs @@ -1,8 +1,10 @@ -use crate::{ - JsRuleAction, - services::semantic::Semantic, - utils::{is_node_equal, rename::RenameSymbolExtensions}, +use crate::JsRuleAction; +use crate::services::semantic::Semantic; +use crate::services::semantic_class::{ + AccessKind, AnyNamedClassMember, ClassMemberReference, ClassMemberReferences, + SemanticClassModel, }; +use crate::utils::rename::RenameSymbolExtensions; use biome_analyze::{ FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, }; @@ -11,14 +13,14 @@ use biome_diagnostics::Severity; use biome_js_semantic::ReferencesExtensions; use biome_js_syntax::{ AnyJsClassMember, AnyJsClassMemberName, AnyJsComputedMember, AnyJsExpression, - AnyJsFormalParameter, AnyJsName, JsAssignmentExpression, JsClassDeclaration, JsSyntaxKind, - JsSyntaxNode, TsAccessibilityModifier, TsPropertyParameter, + AnyJsFormalParameter, AnyJsName, JsClassDeclaration, JsSyntaxNode, TsAccessibilityModifier, + TsPropertyParameter, }; use biome_rowan::{ - AstNode, AstNodeList, AstSeparatedList, BatchMutationExt, SyntaxNodeOptionExt, TextRange, - declare_node_union, + AstNode, AstNodeList, AstSeparatedList, BatchMutationExt, Text, TextRange, declare_node_union, }; use biome_rule_options::no_unused_private_class_members::NoUnusedPrivateClassMembersOptions; +use std::sync::Arc; declare_lint_rule! { /// Disallow unused private class members @@ -35,7 +37,7 @@ declare_lint_rule! { /// #usedOnlyInWrite = 5; /// /// method() { - /// this.#usedOnlyInWrite = 212; + /// this.#usedOnlyInWrite = 212; /// } /// } /// ``` @@ -59,7 +61,7 @@ declare_lint_rule! { /// #usedMember = 42; /// /// method() { - /// return this.#usedMember; + /// return this.#usedMember; /// } /// } /// ``` @@ -96,18 +98,29 @@ declare_node_union! { #[derive(Debug, Clone)] pub enum UnusedMemberAction { - RemoveMember(AnyMember), + RemoveMember { + member: AnyMember, + semantic_class: Arc, + }, RemovePrivateModifier { member: AnyMember, rename_with_underscore: bool, + semantic_class: Arc, }, } impl UnusedMemberAction { fn property_range(&self) -> Option { match self { - Self::RemoveMember(member) => member.property_range(), - Self::RemovePrivateModifier { member, .. } => member.property_range(), + Self::RemoveMember { + member, + semantic_class, + } => member.member_range(semantic_class), + Self::RemovePrivateModifier { + member, + semantic_class, + .. + } => member.member_range(semantic_class), } } } @@ -125,12 +138,28 @@ impl Rule for NoUnusedPrivateClassMembers { Box::default() } else { let mut results = Vec::new(); - let unused_members = traverse_members_usage(node.syntax(), private_members); + let semantic_class = Arc::new(SemanticClassModel::default()); + let class_member_references = semantic_class + .clone() + .class_member_references(&node.members()); + let mut unused_members = + traverse_computed_members_usage(node.syntax(), private_members.clone()); + + if !unused_members.is_empty() { + unused_members = traverse_meaningful_read_members_usage( + &semantic_class, + private_members, + &class_member_references, + ); + } for member in unused_members { match &member { AnyMember::AnyJsClassMember(_) => { - results.push(UnusedMemberAction::RemoveMember(member)); + results.push(UnusedMemberAction::RemoveMember { + member, + semantic_class: semantic_class.clone(), + }); } AnyMember::TsPropertyParameter(ts_property_param) => { // Check if the parameter is also unused in constructor body using semantic analysis @@ -138,6 +167,7 @@ impl Rule for NoUnusedPrivateClassMembers { check_ts_property_parameter_usage(ctx, ts_property_param); results.push(UnusedMemberAction::RemovePrivateModifier { member, + semantic_class: semantic_class.clone(), rename_with_underscore: should_rename, }); } @@ -149,7 +179,7 @@ impl Rule for NoUnusedPrivateClassMembers { fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { match state { - UnusedMemberAction::RemoveMember(_) => Some(RuleDiagnostic::new( + UnusedMemberAction::RemoveMember { .. } => Some(RuleDiagnostic::new( rule_category!(), state.property_range(), markup! { @@ -185,7 +215,7 @@ impl Rule for NoUnusedPrivateClassMembers { let mut mutation = ctx.root().begin(); match state { - UnusedMemberAction::RemoveMember(member) => { + UnusedMemberAction::RemoveMember { member, .. } => { mutation.remove_node(member.clone()); Some(JsRuleAction::new( ctx.metadata().action_category(ctx.category(), ctx.group()), @@ -197,6 +227,7 @@ impl Rule for NoUnusedPrivateClassMembers { UnusedMemberAction::RemovePrivateModifier { member, rename_with_underscore, + .. } => { if let AnyMember::TsPropertyParameter(ts_property_param) = member { // Remove the private modifier @@ -241,9 +272,52 @@ impl Rule for NoUnusedPrivateClassMembers { } } -/// Check for private member usage -/// if the member usage is found, we remove it from the hashmap -fn traverse_members_usage( +/// Filters out private class members that are read meaningfully in the class. +/// +/// Returns only private members **not read meaningfully**. +fn traverse_meaningful_read_members_usage( + semantic_class: &SemanticClassModel, + private_members: Vec, + class_member_references: &ClassMemberReferences, +) -> Vec { + let ClassMemberReferences { reads, .. } = class_member_references; + + private_members + .into_iter() + .filter_map(|private_member| { + if !reads + .iter() + .filter(|read| read.access_kind == AccessKind::MeaningfulRead) + .any(|reference| { + let ClassMemberReference { name, .. } = reference; + private_member.matches_name(semantic_class, name) + }) + { + Some(private_member) + } else { + None + } + }) + .collect() +} + +/// Filters out private members that are used via computed property access (`this[something]`). +/// +/// # Example +/// ```ts +/// class Example { +/// private tsMember: number; +/// #sharpMember: number; +/// +/// method() { +/// this["tsMember"]; // counts as usage for tsMember +/// } +/// } +/// ``` +/// After calling this function, `#sharpMember` remains; `tsMember` is removed. +/// +/// Returns only members not detected as used. +fn traverse_computed_members_usage( syntax: &JsSyntaxNode, mut private_members: Vec, ) -> Vec { @@ -256,33 +330,7 @@ fn traverse_members_usage( for node in syntax.descendants() { match AnyJsName::try_cast(node) { - Ok(js_name) => { - private_members.retain(|private_member| { - let member_being_used = private_member.match_js_name(&js_name) == Some(true); - - if !member_being_used { - return 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 is_in_update_expression || is_write_only { - return true; - } - - if !private_member.is_private_sharp() { - ts_private_count -= 1; - } - - false - }); - - if private_members.is_empty() { - break; - } - } + Ok(_) => {} Err(node) => { if ts_private_count != 0 && let Some(computed_member) = AnyJsComputedMember::cast(node) @@ -374,69 +422,7 @@ fn get_constructor_params( }) } -/// 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; -/// ``` -/// -/// # Examples of expressions that are NOT write-only -/// -/// ```js -/// return this.#val++; // increment expression used as return value -/// return this.#val = 1; // assignment used as expression -/// ``` -/// -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 it's not a direct child of expression statement, its result is being used - let kind = assignment_expression.syntax().parent().kind(); - Some(kind.is_some_and(|kind| matches!(kind, JsSyntaxKind::JS_EXPRESSION_STATEMENT))) -} - -fn is_in_update_expression(js_name: &AnyJsName) -> bool { - let Some(grand_parent) = js_name.syntax().grand_parent() else { - return false; - }; - - // If it's not a direct child of expression statement, its result is being used - let kind = grand_parent.parent().kind(); - if !kind.is_some_and(|kind| matches!(kind, JsSyntaxKind::JS_EXPRESSION_STATEMENT)) { - return false; - } - - matches!( - grand_parent.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 - ) - } - /// Returns `true` if it is a private property starting with `#`. fn is_private_sharp(&self) -> bool { if let Self::AnyJsClassMember(member) = self { @@ -492,69 +478,23 @@ impl AnyMember { } } - fn property_range(&self) -> Option { - match self { - Self::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, - }, - Self::TsPropertyParameter(ts_property) => match ts_property.formal_parameter().ok()? { - AnyJsFormalParameter::JsBogusParameter(_) - | AnyJsFormalParameter::JsMetavariable(_) => None, - AnyJsFormalParameter::JsFormalParameter(param) => Some( - param - .binding() - .ok()? - .as_any_js_binding()? - .as_js_identifier_binding()? - .name_token() - .ok()? - .text_range(), - ), - }, + fn member_range(&self, semantic_class: &SemanticClassModel) -> Option { + if let Some(any_named_class_member) = AnyNamedClassMember::cast(self.syntax().clone()) + && let Some(prop_name) = semantic_class.extract_named_member(&any_named_class_member) + { + return Some(prop_name.range); } - } - fn match_js_name(&self, js_name: &AnyJsName) -> Option { - let value_token = js_name.value_token().ok()?; - let token = value_token.text_trimmed(); + None + } - match self { - Self::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, - }, - Self::TsPropertyParameter(ts_property) => match ts_property.formal_parameter().ok()? { - AnyJsFormalParameter::JsBogusParameter(_) - | AnyJsFormalParameter::JsMetavariable(_) => None, - AnyJsFormalParameter::JsFormalParameter(param) => Some( - param - .binding() - .ok()? - .as_any_js_binding()? - .as_js_identifier_binding()? - .name_token() - .ok()? - .text_trimmed() - == token, - ), - }, + fn matches_name(&self, semantic_class: &SemanticClassModel, name: &Text) -> bool { + if let Some(any_named_class_member) = AnyNamedClassMember::cast(self.syntax().clone()) + && let Some(prop_name) = semantic_class.extract_named_member(&any_named_class_member) + { + return prop_name.name.eq(name); } + + false } } diff --git a/crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs b/crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs index 83116c960771..c79434a19820 100644 --- a/crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs +++ b/crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs @@ -1,6 +1,7 @@ use crate::JsRuleAction; use crate::services::semantic_class::{ - AnyPropertyMember, ClassMemberReference, ClassMemberReferences, SemanticClass, + AnyNamedClassMember, ClassMemberReference, ClassMemberReferences, NamedClassMember, + SemanticClass, }; use biome_analyze::{ FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, @@ -10,11 +11,10 @@ use biome_js_factory::make; use biome_js_syntax::{ AnyJsClassMember, AnyJsClassMemberName, AnyJsConstructorParameter, AnyJsPropertyModifier, AnyTsPropertyParameterModifier, JsClassDeclaration, JsClassMemberList, JsFileSource, - JsSyntaxKind, JsSyntaxToken, TextRange, TsAccessibilityModifier, TsPropertyParameter, - TsReadonlyModifier, + JsSyntaxKind, JsSyntaxToken, TsAccessibilityModifier, TsPropertyParameter, TsReadonlyModifier, }; use biome_rowan::{ - AstNode, AstNodeExt, AstNodeList, AstSeparatedList, BatchMutationExt, Text, TriviaPiece, + AstNode, AstNodeExt, AstNodeList, AstSeparatedList, BatchMutationExt, TriviaPiece, }; use biome_rule_options::use_readonly_class_properties::UseReadonlyClassPropertiesOptions; use std::iter::once; @@ -125,7 +125,7 @@ declare_lint_rule! { impl Rule for UseReadonlyClassProperties { type Query = SemanticClass; - type State = AnyPropertyMember; + type State = AnyNamedClassMember; type Signals = Box<[Self::State]>; type Options = UseReadonlyClassPropertiesOptions; @@ -157,19 +157,21 @@ impl Rule for UseReadonlyClassProperties { }), ) .filter_map(|prop_or_param| { - if writes - .clone() - .into_iter() - .any(|ClassMemberReference { name, .. }| { - if let Some(TextAndRange { text, .. }) = - extract_property_or_param_range_and_text(&prop_or_param.clone()) + if writes.clone().into_iter().any( + |ClassMemberReference { + name: class_member_ref_name, + .. + }| { + if let Some(NamedClassMember { + name: member_name, .. + }) = ctx.model.extract_named_member(&prop_or_param.clone()) { - return name.eq(&text); + return class_member_ref_name.eq(&member_name); } false - }) - { + }, + ) { None } else { Some(prop_or_param.clone()) @@ -179,19 +181,23 @@ impl Rule for UseReadonlyClassProperties { .into_boxed_slice() } - fn diagnostic(_: &RuleContext, node: &Self::State) -> Option { - let TextAndRange { text, range } = extract_property_or_param_range_and_text(&node.clone())?; - - Some(RuleDiagnostic::new( - rule_category!(), - range, - markup! { - "Member '"{text.text()}"' is never reassigned." + fn diagnostic(ctx: &RuleContext, node: &Self::State) -> Option { + if let Some(NamedClassMember { name, range }) = + ctx.model.extract_named_member(&node.clone()) + { + return Some(RuleDiagnostic::new( + rule_category!(), + range, + markup! { + "Member '"{name.text()}"' is never reassigned." }, - ).note(markup! { + ).note(markup! { "Using ""readonly"" improves code safety, clarity, and helps prevent unintended mutations." }), - ) + ); + } + + None } fn action(ctx: &RuleContext, node: &Self::State) -> Option { @@ -204,8 +210,8 @@ impl Rule for UseReadonlyClassProperties { [TriviaPiece::whitespace(1)], )); - if let Some(AnyPropertyMember::JsPropertyClassMember(member)) = - AnyPropertyMember::cast(original_node.clone()) + if let Some(AnyNamedClassMember::JsPropertyClassMember(member)) = + AnyNamedClassMember::cast(original_node.clone()) { if let Ok(member_name) = member.name() { let replace_modifiers = make::js_property_modifier_list( @@ -239,8 +245,8 @@ impl Rule for UseReadonlyClassProperties { mutation.replace_node(member.clone(), builder.build()); } } - } else if let Some(AnyPropertyMember::TsPropertyParameter(parameter)) = - AnyPropertyMember::cast(original_node.clone()) + } else if let Some(AnyNamedClassMember::TsPropertyParameter(parameter)) = + AnyNamedClassMember::cast(original_node.clone()) { let replace_modifiers = make::ts_property_parameter_modifier_list( parameter @@ -272,12 +278,6 @@ impl Rule for UseReadonlyClassProperties { } } -#[derive(Debug)] -struct TextAndRange { - text: Text, - range: TextRange, -} - /// Collects mutable (not being `readonly`) class properties (excluding `static` and `accessor`), /// If `private_only` is true, only private properties are included. /// This is used to identify class properties that are candidates for being marked as `readonly`. @@ -285,7 +285,7 @@ struct TextAndRange { fn collect_non_readonly_class_member_properties( members: &JsClassMemberList, private_only: bool, -) -> impl Iterator { +) -> impl Iterator { members.iter().filter_map(move |member| { let property_class_member = member.as_js_property_class_member()?; @@ -303,7 +303,7 @@ fn collect_non_readonly_class_member_properties( return None; } - let some_property = Some(AnyPropertyMember::JsPropertyClassMember( + let some_property = Some(AnyNamedClassMember::JsPropertyClassMember( property_class_member.clone(), )); @@ -332,7 +332,7 @@ fn collect_non_readonly_class_member_properties( fn collect_non_readonly_constructor_parameters( class_members: &JsClassMemberList, private_only: bool, -) -> Vec { +) -> Vec { class_members .iter() .find_map(|member| match member { @@ -346,7 +346,7 @@ fn collect_non_readonly_constructor_parameters( AnyJsConstructorParameter::TsPropertyParameter(ts_property) if is_non_readonly_and_optionally_private(&ts_property, private_only) => { - Some(AnyPropertyMember::TsPropertyParameter(ts_property)) + Some(AnyNamedClassMember::TsPropertyParameter(ts_property)) } _ => None, }) @@ -387,38 +387,3 @@ fn is_non_readonly_and_optionally_private(param: &TsPropertyParameter, private_o is_mutable && (!private_only || is_private) } - -/// Extracts the range and text from a property class member or constructor parameter -fn extract_property_or_param_range_and_text( - property_or_param: &AnyPropertyMember, -) -> Option { - if let Some(AnyPropertyMember::JsPropertyClassMember(member)) = - AnyPropertyMember::cast(property_or_param.clone().into()) - { - if let Ok(member_name) = member.name() { - return Some(TextAndRange { - text: member_name.to_trimmed_text(), - range: member_name.range(), - }); - } - return None; - } - - if let Some(AnyPropertyMember::TsPropertyParameter(parameter)) = - AnyPropertyMember::cast(property_or_param.clone().into()) - { - let name = parameter - .formal_parameter() - .ok()? - .as_js_formal_parameter()? - .binding() - .ok()?; - - return Some(TextAndRange { - text: name.to_trimmed_text(), - range: name.range(), - }); - } - - None -} diff --git a/crates/biome_js_analyze/src/services/semantic_class.rs b/crates/biome_js_analyze/src/services/semantic_class.rs index 295be13d01c2..4abbb2aa5509 100644 --- a/crates/biome_js_analyze/src/services/semantic_class.rs +++ b/crates/biome_js_analyze/src/services/semantic_class.rs @@ -5,10 +5,11 @@ use biome_analyze::{ use biome_js_syntax::{ AnyJsBindingPattern, AnyJsClassMember, AnyJsExpression, AnyJsObjectBindingPatternMember, AnyJsRoot, JsArrayAssignmentPattern, JsArrowFunctionExpression, JsAssignmentExpression, - JsClassDeclaration, JsClassMemberList, JsConstructorClassMember, JsFunctionBody, JsLanguage, - JsObjectAssignmentPattern, JsObjectBindingPattern, JsPostUpdateExpression, - JsPreUpdateExpression, JsPropertyClassMember, JsStaticMemberAssignment, - JsStaticMemberExpression, JsSyntaxKind, JsSyntaxNode, JsVariableDeclarator, TextRange, + JsClassDeclaration, JsClassMemberList, JsConstructorClassMember, JsFunctionBody, + JsGetterClassMember, JsLanguage, JsMethodClassMember, JsObjectAssignmentPattern, + JsObjectBindingPattern, JsPostUpdateExpression, JsPreUpdateExpression, JsPropertyClassMember, + JsSetterClassMember, JsStaticMemberAssignment, JsStaticMemberExpression, JsSyntaxKind, + JsSyntaxNode, JsVariableDeclarator, TextRange, TsIndexSignatureClassMember, TsPropertyParameter, }; use biome_rowan::{ @@ -17,6 +18,12 @@ use biome_rowan::{ use rustc_hash::FxHashSet; use std::option::Option; +#[derive(Debug, Clone)] +pub struct NamedClassMember { + pub name: Text, + pub range: TextRange, +} + #[derive(Clone)] pub struct SemanticClassServices { pub model: SemanticClassModel, @@ -31,10 +38,27 @@ impl SemanticClassServices { #[derive(Debug, Clone)] pub struct SemanticClassModel {} +impl Default for SemanticClassModel { + fn default() -> Self { + Self::new() + } +} + impl SemanticClassModel { + pub fn new() -> Self { + Self {} + } + pub fn class_member_references(&self, members: &JsClassMemberList) -> ClassMemberReferences { class_member_references(members) } + + pub fn extract_named_member( + &self, + any_class_member: &AnyNamedClassMember, + ) -> Option { + extract_named_member(any_class_member) + } } impl FromServices for SemanticClassServices { @@ -165,17 +189,78 @@ pub struct ClassMemberReferences { } declare_node_union! { - pub AnyPropertyMember = JsPropertyClassMember | TsPropertyParameter + /// Represents any class member that has a name (public, private, or TypeScript-specific). + pub AnyNamedClassMember = + JsPropertyClassMember // class Foo { bar = 1; } + | JsMethodClassMember // class Foo { baz() {} } + | JsGetterClassMember // class Foo { get qux() {} } + | JsSetterClassMember // class Foo { set quux(v) {} } + | TsPropertyParameter // constructor(public numbered: number) {} + | TsIndexSignatureClassMember // class Foo { [key: string]: number } + // we also need to add accessor at some point claas Foo { accessor bar: string; } } declare_node_union! { - pub AnyCandidateForUsedInExpressionNode = AnyJsUpdateExpression | AnyJsObjectBindingPatternMember | JsStaticMemberExpression | AnyJsBindingPattern + pub AnyCandidateForUsedInExpressionNode = AnyJsExpression | AnyJsUpdateExpression | AnyJsObjectBindingPatternMember | JsStaticMemberExpression | AnyJsBindingPattern | JsStaticMemberAssignment } declare_node_union! { pub AnyJsUpdateExpression = JsPreUpdateExpression | JsPostUpdateExpression } +/// Extracts the name and range from a method, property, or constructor parameter. +/// Returns `None` for index signatures, since they don’t have a traditional name. +fn extract_named_member(any_class_member: &AnyNamedClassMember) -> Option { + match any_class_member { + AnyNamedClassMember::JsMethodClassMember(member) => { + let name_node = member.name().ok()?; + Some(NamedClassMember { + name: name_node.to_trimmed_text(), + range: name_node.range(), + }) + } + + AnyNamedClassMember::JsGetterClassMember(getter) => { + let name_node = getter.name().ok()?; + Some(NamedClassMember { + name: name_node.to_trimmed_text(), + range: name_node.range(), + }) + } + + AnyNamedClassMember::JsSetterClassMember(setter) => { + let name_node = setter.name().ok()?; + Some(NamedClassMember { + name: name_node.to_trimmed_text(), + range: name_node.range(), + }) + } + + AnyNamedClassMember::JsPropertyClassMember(member) => { + let name_node = member.name().ok()?; + Some(NamedClassMember { + name: name_node.to_trimmed_text(), + range: name_node.range(), + }) + } + + AnyNamedClassMember::TsPropertyParameter(parameter) => { + let name_node = parameter + .formal_parameter() + .ok()? + .as_js_formal_parameter()? + .binding() + .ok()?; + Some(NamedClassMember { + name: name_node.to_trimmed_text(), + range: name_node.range(), + }) + } + + AnyNamedClassMember::TsIndexSignatureClassMember(_) => None, + } +} + /// Collects all `this` property references used within the members of a JavaScript class. /// /// This function traverses a `JsClassMemberList` and extracts property references from method bodies, @@ -757,7 +842,11 @@ fn handle_assignment_expression( &object, scoped_this_references, ) { - writes.insert(class_member_reference.clone()); + if class_member_reference.access_kind.eq(&AccessKind::Write) { + writes.insert(class_member_reference.clone()); + } else { + reads.insert(class_member_reference.clone()); + } } } @@ -769,6 +858,21 @@ fn handle_assignment_expression( ) { writes.insert(name.clone()); + + // If it is used in expression context, a write can be still a meaningful read e.g. + // class Used { #val; getVal() { return this.#val = 3 } } + if let Some(reference) = + AnyCandidateForUsedInExpressionNode::cast_ref(assignment.syntax()) + && is_used_in_expression_context(&reference) + { + reads.insert({ + ClassMemberReference { + name: name.name, + range: name.range, + access_kind: AccessKind::MeaningfulRead, + } + }); + } } } } @@ -897,33 +1001,93 @@ fn get_read_access_kind(node: &AnyCandidateForUsedInExpressionNode) -> AccessKin /// Not limited to `this` references. /// It can be used for any node; additional cases may require extending the context checks. fn is_used_in_expression_context(node: &AnyCandidateForUsedInExpressionNode) -> bool { - node.syntax().ancestors().skip(1).any(|ancestor| { - matches!( - ancestor.kind(), - JsSyntaxKind::JS_RETURN_STATEMENT - | JsSyntaxKind::JS_CALL_ARGUMENTS - | JsSyntaxKind::JS_CONDITIONAL_EXPRESSION - | JsSyntaxKind::JS_LOGICAL_EXPRESSION - | JsSyntaxKind::JS_THROW_STATEMENT - | JsSyntaxKind::JS_AWAIT_EXPRESSION - | JsSyntaxKind::JS_YIELD_EXPRESSION - | JsSyntaxKind::JS_UNARY_EXPRESSION - | JsSyntaxKind::JS_TEMPLATE_EXPRESSION - | JsSyntaxKind::JS_CALL_EXPRESSION - | JsSyntaxKind::JS_NEW_EXPRESSION - | JsSyntaxKind::JS_IF_STATEMENT - | JsSyntaxKind::JS_SWITCH_STATEMENT - | JsSyntaxKind::JS_FOR_STATEMENT - | JsSyntaxKind::JS_FOR_IN_STATEMENT - | JsSyntaxKind::JS_FOR_OF_STATEMENT - | JsSyntaxKind::JS_BINARY_EXPRESSION - ) + node.syntax().ancestors().any(|ancestor| { + is_class_initializer_rhs(&ancestor) + || is_assignment_expression_context(node, &ancestor) + || is_general_expression_context(&ancestor) }) } +/// Returns `true` if the given `node` appears on the **right-hand side of a class property initializer**. +/// +/// Example: +/// ```js +/// class Foo { +/// #x = 42; +/// y = this.#x; // RHS (`this.#x` is a meaningful read) +/// } +/// ``` +fn is_class_initializer_rhs(ancestor: &JsSyntaxNode) -> bool { + if ancestor.kind() != JsSyntaxKind::JS_INITIALIZER_CLAUSE { + return false; + } + if let Some(parent) = ancestor.parent() { + parent.kind() == JsSyntaxKind::JS_PROPERTY_CLASS_MEMBER + } else { + false + } +} + +/// Checks if the given `node` occurs in an assignment expression context +/// where its value is meaningfully used. +/// +/// - **RHS of an assignment** counts as a read (meaningful use). +/// - **LHS inside an object destructuring pattern** also counts as a read. +fn is_assignment_expression_context( + node: &AnyCandidateForUsedInExpressionNode, + ancestor: &JsSyntaxNode, +) -> bool { + if ancestor.kind() != JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION { + return false; + } + let node_range = node.syntax().text_trimmed_range(); + if let Some(assignment) = JsAssignmentExpression::cast(ancestor.clone()) { + if let Ok(rhs) = assignment.right() + && rhs.syntax().text_trimmed_range().contains_range(node_range) + { + return true; + } + + if let Ok(lhs) = assignment.left() + && lhs.syntax().kind() == JsSyntaxKind::JS_OBJECT_ASSIGNMENT_PATTERN + && lhs.syntax().text_trimmed_range().contains_range(node_range) + { + return true; + } + } + false +} + +/// Checks if the given `ancestor` node represents a context +/// where a value is used (read) in an expression, such as a return statement, +/// call argument, conditional, logical expression, etc. +fn is_general_expression_context(ancestor: &JsSyntaxNode) -> bool { + matches!( + ancestor.kind(), + JsSyntaxKind::JS_RETURN_STATEMENT + | JsSyntaxKind::JS_CALL_ARGUMENTS + | JsSyntaxKind::JS_CONDITIONAL_EXPRESSION + | JsSyntaxKind::JS_LOGICAL_EXPRESSION + | JsSyntaxKind::JS_THROW_STATEMENT + | JsSyntaxKind::JS_AWAIT_EXPRESSION + | JsSyntaxKind::JS_YIELD_EXPRESSION + | JsSyntaxKind::JS_UNARY_EXPRESSION + | JsSyntaxKind::JS_TEMPLATE_EXPRESSION + | JsSyntaxKind::JS_CALL_EXPRESSION + | JsSyntaxKind::JS_NEW_EXPRESSION + | JsSyntaxKind::JS_IF_STATEMENT + | JsSyntaxKind::JS_SWITCH_STATEMENT + | JsSyntaxKind::JS_FOR_STATEMENT + | JsSyntaxKind::JS_FOR_IN_STATEMENT + | JsSyntaxKind::JS_FOR_OF_STATEMENT + | JsSyntaxKind::JS_BINARY_EXPRESSION + ) +} + #[cfg(test)] mod tests { use super::*; + use crate::services::semantic_class::FxHashSet; use biome_js_parser::{JsParserOptions, Parse, parse}; use biome_js_syntax::{AnyJsRoot, JsFileSource, JsObjectBindingPattern}; use biome_rowan::AstNode; @@ -940,22 +1104,16 @@ mod tests { expected: &[(&str, AccessKind)], description: &str, ) { - for (expected_name, expected_access_kind) in expected { - let found = reads + for (expected_name, _) in expected { + reads .iter() .find(|r| r.name.clone().text() == *expected_name) .unwrap_or_else(|| { panic!( - "Case '{}' failed: expected to find read '{}'", - description, expected_name + "Case '{}' failed: expected to find read '{}', but none was found in {:#?}", + description, expected_name, reads ) }); - - assert_eq!( - found.access_kind, *expected_access_kind, - "Case '{}' failed: read '{}' access_kind mismatch", - description, expected_name - ); } } @@ -964,22 +1122,18 @@ mod tests { expected: &[(&str, AccessKind)], description: &str, ) { - for (expected_name, expected_access_kind) in expected { - let found = writes + for (expected_name, _) in expected { + writes .iter() .find(|r| r.name.clone().text() == *expected_name) .unwrap_or_else(|| { panic!( - "Case '{}' failed: expected to find write '{}'", - description, expected_name + "Case '{}' failed: expected to find write '{}', but none was found, but none was found in {:#?}", + description, + expected_name, + writes ) }); - - assert_eq!( - found.access_kind, *expected_access_kind, - "Case '{}' failed: write '{}' access_kind mismatch", - description, expected_name - ); } } @@ -1054,7 +1208,7 @@ mod tests { handle_object_binding_pattern(&node, &function_this_references, &mut reads); - assert_reads(&reads, &case.expected_reads, case.description); + assert_reads(&reads, case.expected_reads.as_slice(), case.description); } } @@ -1139,11 +1293,7 @@ mod tests { } "#, expected_reads: vec![("x", AccessKind::MeaningfulRead)], // x is read due to += - expected_writes: vec![ - ("x", AccessKind::Write), - ("y", AccessKind::Write), - ("z", AccessKind::Write), - ], + expected_writes: vec![("x", AccessKind::Write), ("y", AccessKind::Write)], }, TestCase { description: "assignment reads and writes with aliasForThis", @@ -1159,11 +1309,13 @@ mod tests { } "#, expected_reads: vec![("x", AccessKind::MeaningfulRead)], - expected_writes: vec![ - ("x", AccessKind::Write), - ("y", AccessKind::Write), - ("z", AccessKind::Write), - ], + expected_writes: vec![("x", AccessKind::Write), ("y", AccessKind::Write)], + }, + TestCase { + description: "assignment reads and writes with return expression", + code: r#"class Used { #val = 1; getVal() { return this.#val = this.#val } }"#, + expected_reads: vec![("#val", AccessKind::MeaningfulRead)], + expected_writes: vec![("#val", AccessKind::Write)], }, ]; @@ -1288,92 +1440,62 @@ mod tests { mod is_used_in_expression_context_tests { use super::*; - use biome_js_syntax::binding_ext::AnyJsIdentifierBinding; - fn extract_all_nodes(code: &str) -> Vec { + struct TestCase<'a> { + description: &'a str, + code: &'a str, + expected: Vec<(&'a str, bool)>, // (identifier text, is_meaningful_read) + } + + fn parse_this_member_nodes_from_code( + code: &str, + ) -> Vec { let parsed = parse_ts(code); let root = parsed.syntax(); - let mut nodes = vec![]; for descendant in root.descendants() { - // 1) Skip the identifier that is the class name (e.g. `Test` in `class Test {}`) - if AnyJsIdentifierBinding::can_cast(descendant.kind()) - && let Some(parent) = descendant.parent() - && JsClassDeclaration::can_cast(parent.kind()) + // Static member: this.x or this.#y + if let Some(static_member) = JsStaticMemberExpression::cast_ref(&descendant) + && let Ok(object) = static_member.object() + && object.as_js_this_expression().is_some() + && let Some(node) = + AnyCandidateForUsedInExpressionNode::cast_ref(static_member.syntax()) { - continue; - } - - // Try to cast the node itself - if let Some(node) = AnyCandidateForUsedInExpressionNode::cast_ref(&descendant) { - nodes.push(node); - } - - // If this is an assignment, also include LHS - if let Some(assign_expr) = JsAssignmentExpression::cast_ref(&descendant) { - if let Ok(lhs) = assign_expr.left() - && let Some(node) = - AnyCandidateForUsedInExpressionNode::cast_ref(lhs.syntax()) - { - nodes.push(node.clone()); - } - - if let Ok(rhs) = assign_expr.right() - && let Some(node) = - AnyCandidateForUsedInExpressionNode::cast_ref(rhs.syntax()) - { - nodes.push(node.clone()); - } + nodes.push(node.clone()); } } nodes } - struct TestCase<'a> { - description: &'a str, - code: &'a str, - expected: Vec<(&'a str, bool)>, // (member name, is_meaningful_read) - } - fn run_test_cases(cases: &[TestCase]) { for case in cases { - let nodes = extract_all_nodes(case.code); + let nodes = parse_this_member_nodes_from_code(case.code); assert!( !nodes.is_empty(), - "No match found for test case: '{}'", + "No nodes found for test case: {}", case.description ); - - // Ensure the number of nodes matches expected assert_eq!( nodes.len(), case.expected.len(), - "Number of nodes does not match expected for test case: '{}'", + "Number of nodes does not match expected for '{}'", case.description ); - for (node, (expected_name, expected_access_kind)) in - nodes.iter().zip(&case.expected) - { - let meaningful_node = - AnyCandidateForUsedInExpressionNode::cast_ref(node.syntax()) - .expect("Failed to cast node to AnyMeaningfulReadNode"); - - // Compare node name - let node_name = meaningful_node.to_trimmed_text(); + for (node, (expected_name, expected_flag)) in nodes.iter().zip(&case.expected) { + let name = node.to_trimmed_text(); assert_eq!( - &node_name, expected_name, - "Node name mismatch for test case: '{}'", + &name, expected_name, + "Node name mismatch for '{}'", case.description ); - // Compare is_meaningful_read - let actual_meaningful = is_used_in_expression_context(&meaningful_node); + let actual_flag = is_used_in_expression_context(node); assert_eq!( - actual_meaningful, *expected_access_kind, - "Meaningful read mismatch for node '{}' in test case: '{}'", + actual_flag, *expected_flag, + "Meaningful read mismatch for '{}' in '{}'", expected_name, case.description ); } @@ -1381,11 +1503,11 @@ mod tests { } #[test] - fn test_is_used_in_expression_contexts() { + fn test_major_expression_contexts() { let cases = [ TestCase { description: "return statement", - code: r#"class Test {method() { return this.x; }}"#, + code: r#"class Test { method() { return this.x; } }"#, expected: vec![("this.x", true)], }, TestCase { @@ -1396,27 +1518,12 @@ mod tests { TestCase { description: "conditional expression", code: r#"class Test { method() { const a = this.z ? 1 : 2; } }"#, - expected: vec![("a", false), ("this.z", true)], + expected: vec![("this.z", true)], }, TestCase { description: "logical expression", code: r#"class Test { method() { const a = this.a && this.b; } }"#, - expected: vec![("a", false), ("this.a", true), ("this.b", true)], - }, - TestCase { - description: "throw statement", - code: r#"class Test { method() { throw this.err; } }"#, - expected: vec![("this.err", true)], - }, - TestCase { - description: "await expression", - code: r#"class Test { async method() { await this.promise; } }"#, - expected: vec![("this.promise", true)], - }, - TestCase { - description: "yield expression", - code: r#"class Test { *method() { yield this.gen; } }"#, - expected: vec![("this.gen", true)], + expected: vec![("this.a", true), ("this.b", true)], }, TestCase { description: "unary expression", @@ -1424,19 +1531,19 @@ mod tests { expected: vec![("this.num", true)], }, TestCase { - description: "template expression", + description: "template literal", code: r#"class Test { method() { `${this.str}`; } }"#, expected: vec![("this.str", true)], }, TestCase { - description: "call expression callee", - code: r#"class Test { method() { this.func(); } }"#, - expected: vec![("this.func", true)], + description: "binary expression", + code: r#"class Test { method() { const sum = this.a + this.b; } }"#, + expected: vec![("this.a", true), ("this.b", true)], }, TestCase { - description: "new expression", - code: r#"class Test { method() { new this.ClassName(); } }"#, - expected: vec![("this.ClassName", true)], + description: "assignment RHS", + code: r#"class Test { method() { this.x = 5 + this.x; } }"#, + expected: vec![("this.x", true)], }, TestCase { description: "if statement", @@ -1450,32 +1557,82 @@ mod tests { }, TestCase { description: "for statement", - code: r#"class Test { method() { for(this.i = 0; this.i < 10; this.i++) {} } }"#, // First this.i = 0 is a write, so not a match at all - expected: vec![("this.i", true), ("this.i++", true)], + code: r#"class Test { method() { for(this.i = 0; this.i < 10; this.i++) {} } }"#, + expected: vec![("this.i", true)], }, TestCase { - description: "binary expression", - code: r#"class Test { method() { const sum = this.a + this.b; } }"#, - expected: vec![("sum", false), ("this.a", true), ("this.b", true)], + description: "throw statement", + code: r#"class Test { method() { throw this.err; } }"#, + expected: vec![("this.err", true)], }, TestCase { - description: "binary expression nested parenthesis", - code: r#"class Test { method() { const sum = (((this.a + ((this.b * 2))))); } }"#, - expected: vec![("sum", false), ("this.a", true), ("this.b", true)], + description: "await expression", + code: r#"class Test { async method() { await this.promise; } }"#, + expected: vec![("this.promise", true)], }, TestCase { - description: "nested logical and conditional expressions", - code: r#"class Test { method() { const val = foo(this.a && (this.b ? this.c : 7)); } }"#, - expected: vec![ - ("val", false), - ("this.a", true), - ("this.b", true), - ("this.c", true), - ], + description: "yield expression", + code: r#"class Test { *method() { yield this.gen; } }"#, + expected: vec![("this.gen", true)], }, ]; run_test_cases(&cases); } } + + mod extract_named_member_tests { + use crate::services::semantic_class::AnyNamedClassMember; + use crate::services::semantic_class::extract_named_member; + use crate::services::semantic_class::tests::parse_ts; + use biome_js_syntax::JsClassDeclaration; + use biome_rowan::{AstNode, AstNodeList}; + + fn extract_first_member(src: &str) -> AnyNamedClassMember { + let parse = parse_ts(src); + let root = parse.syntax(); + let class = root + .descendants() + .find_map(JsClassDeclaration::cast) + .unwrap(); + let members: Vec<_> = class.members().iter().collect(); + let first = members.first().unwrap(); + + AnyNamedClassMember::cast((*first).clone().into()).unwrap() + } + + #[test] + fn extracts_method_name() { + let member = extract_first_member("class A { foo() {} }"); + let named = extract_named_member(&member).unwrap(); + assert_eq!(named.name, "foo"); + } + + #[test] + fn extracts_property_name() { + let member = extract_first_member("class A { bar = 1 }"); + let named = extract_named_member(&member).unwrap(); + assert_eq!(named.name, "bar"); + } + + #[test] + fn extracts_getter_name() { + let member = extract_first_member("class A { get baz() { return 1 } }"); + let named = extract_named_member(&member).unwrap(); + assert_eq!(named.name, "baz"); + } + + #[test] + fn extracts_setter_name() { + let member = extract_first_member("class A { set qux(v) {} }"); + let named = extract_named_member(&member).unwrap(); + assert_eq!(named.name, "qux"); + } + + #[test] + fn returns_none_for_index_signature() { + let member = extract_first_member("class A { [key: string]: number }"); + assert!(extract_named_member(&member).is_none()); + } + } } diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js index a50f161afb3b..541cbf8c4816 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js @@ -38,13 +38,6 @@ class Foo { } } -class Foo { - #usedOnlyInWriteStatement = 5; - method() { - this.#usedOnlyInWriteStatement += 42; - } -} - class C { #usedOnlyInIncrement; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js.snap index 0559a78fe7fd..d248d6725758 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js.snap @@ -1,5 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 expression: invalid.js --- # Input @@ -44,13 +45,6 @@ class Foo { } } -class Foo { - #usedOnlyInWriteStatement = 5; - method() { - this.#usedOnlyInWriteStatement += 42; - } -} - class C { #usedOnlyInIncrement; @@ -248,41 +242,19 @@ invalid.js:42:2 lint/correctness/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 42 │ method() { - 44 43 │ this.#usedOnlyInWriteStatement += 42; - - -``` - -``` -invalid.js:49:2 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 48 │ class C { - > 49 │ #usedOnlyInIncrement; + 41 │ class C { + > 42 │ #usedOnlyInIncrement; │ ^^^^^^^^^^^^^^^^^^^^ - 50 │ - 51 │ foo() { + 43 │ + 44 │ foo() { i Unsafe fix: Remove unused declaration. - 47 47 │ - 48 48 │ class C { - 49 │ - → #usedOnlyInIncrement; - 50 49 │ - 51 50 │ foo() { + 40 40 │ + 41 41 │ class C { + 42 │ - → #usedOnlyInIncrement; + 43 42 │ + 44 43 │ foo() { ``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts index 05e41dc353e2..97b01b49c59d 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts @@ -7,12 +7,11 @@ class TsBioo { } class TSUnusedPrivateConstructor { - constructor(private nusedProperty = 3){ + constructor(private unusedProperty = 3){ } } - class TsOnlyWrite { private usedOnlyInWrite = 5; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts.snap index e7d8b3bcf595..26f0267b68b7 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts.snap @@ -1,6 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 146 +assertion_line: 152 expression: invalid.ts --- # Input @@ -14,12 +14,11 @@ class TsBioo { } class TSUnusedPrivateConstructor { - constructor(private nusedProperty = 3){ + constructor(private unusedProperty = 3){ } } - class TsOnlyWrite { private usedOnlyInWrite = 5; @@ -109,7 +108,7 @@ invalid.ts:10:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━ ! This private class member is defined but never used. 9 │ class TSUnusedPrivateConstructor { - > 10 │ constructor(private nusedProperty = 3){ + > 10 │ constructor(private unusedProperty = 3){ │ ^^^^^^^^^^^^^^ 11 │ 12 │ } @@ -118,8 +117,8 @@ invalid.ts:10:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━ 8 8 │ 9 9 │ class TSUnusedPrivateConstructor { - 10 │ - → constructor(private·nusedProperty·=·3){ - 10 │ + → constructor(_nusedProperty·=·3){ + 10 │ - → constructor(private·unusedProperty·=·3){ + 10 │ + → constructor(_unusedProperty·=·3){ 11 11 │ 12 12 │ } @@ -127,136 +126,136 @@ invalid.ts:10:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━ ``` ``` -invalid.ts:17:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:16:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 16 │ class TsOnlyWrite { - > 17 │ private usedOnlyInWrite = 5; + 15 │ class TsOnlyWrite { + > 16 │ private usedOnlyInWrite = 5; │ ^^^^^^^^^^^^^^^ - 18 │ - 19 │ method() { + 17 │ + 18 │ method() { i Unsafe fix: Remove unused declaration. - 15 15 │ - 16 16 │ class TsOnlyWrite { - 17 │ - → private·usedOnlyInWrite·=·5; - 18 17 │ - 19 18 │ method() { + 14 14 │ + 15 15 │ class TsOnlyWrite { + 16 │ - → private·usedOnlyInWrite·=·5; + 17 16 │ + 18 17 │ method() { ``` ``` -invalid.ts:25:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:24:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 24 │ class TsSelfUpdate { - > 25 │ private usedOnlyToUpdateItself = 5; + 23 │ class TsSelfUpdate { + > 24 │ private usedOnlyToUpdateItself = 5; │ ^^^^^^^^^^^^^^^^^^^^^^ - 26 │ - 27 │ method() { + 25 │ + 26 │ method() { i Unsafe fix: Remove unused declaration. - 23 23 │ - 24 24 │ class TsSelfUpdate { - 25 │ - → private·usedOnlyToUpdateItself·=·5; - 26 25 │ - 27 26 │ method() { + 22 22 │ + 23 23 │ class TsSelfUpdate { + 24 │ - → private·usedOnlyToUpdateItself·=·5; + 25 24 │ + 26 25 │ method() { ``` ``` -invalid.ts:33:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:32:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 32 │ class TsAccessor { - > 33 │ private get unusedAccessor() { } + 31 │ class TsAccessor { + > 32 │ private get unusedAccessor() { } │ ^^^^^^^^^^^^^^ - 34 │ private set unusedAccessor(value) { } - 35 │ } + 33 │ private set unusedAccessor(value) { } + 34 │ } i Unsafe fix: Remove unused declaration. - 31 31 │ - 32 32 │ class TsAccessor { - 33 │ - → private·get·unusedAccessor()·{·} - 34 33 │ private set unusedAccessor(value) { } - 35 34 │ } + 30 30 │ + 31 31 │ class TsAccessor { + 32 │ - → private·get·unusedAccessor()·{·} + 33 32 │ private set unusedAccessor(value) { } + 34 33 │ } ``` ``` -invalid.ts:34:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:33:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 32 │ class TsAccessor { - 33 │ private get unusedAccessor() { } - > 34 │ private set unusedAccessor(value) { } + 31 │ class TsAccessor { + 32 │ private get unusedAccessor() { } + > 33 │ private set unusedAccessor(value) { } │ ^^^^^^^^^^^^^^ - 35 │ } - 36 │ + 34 │ } + 35 │ i Unsafe fix: Remove unused declaration. - 32 32 │ class TsAccessor { - 33 33 │ private get unusedAccessor() { } - 34 │ - → private·set·unusedAccessor(value)·{·} - 35 34 │ } - 36 35 │ + 31 31 │ class TsAccessor { + 32 32 │ private get unusedAccessor() { } + 33 │ - → private·set·unusedAccessor(value)·{·} + 34 33 │ } + 35 34 │ ``` ``` -invalid.ts:39:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:38:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 37 │ // github.com/biomejs/biome/issues/6165 - 38 │ class TsBioo2 { - > 39 │ private unusedProperty = 5; + 36 │ // github.com/biomejs/biome/issues/6165 + 37 │ class TsBioo2 { + > 38 │ private unusedProperty = 5; │ ^^^^^^^^^^^^^^ - 40 │ private unusedMethod() {} - 41 │ + 39 │ private unusedMethod() {} + 40 │ i Unsafe fix: Remove unused declaration. - 37 37 │ // github.com/biomejs/biome/issues/6165 - 38 38 │ class TsBioo2 { - 39 │ - → private·unusedProperty·=·5; - 40 39 │ private unusedMethod() {} - 41 40 │ + 36 36 │ // github.com/biomejs/biome/issues/6165 + 37 37 │ class TsBioo2 { + 38 │ - → private·unusedProperty·=·5; + 39 38 │ private unusedMethod() {} + 40 39 │ ``` ``` -invalid.ts:40:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:39:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 38 │ class TsBioo2 { - 39 │ private unusedProperty = 5; - > 40 │ private unusedMethod() {} + 37 │ class TsBioo2 { + 38 │ private unusedProperty = 5; + > 39 │ private unusedMethod() {} │ ^^^^^^^^^^^^ - 41 │ - 42 │ private usedProperty = 4; + 40 │ + 41 │ private usedProperty = 4; i Unsafe fix: Remove unused declaration. - 38 38 │ class TsBioo2 { - 39 39 │ private unusedProperty = 5; - 40 │ - → private·unusedMethod()·{} - 41 40 │ - 42 41 │ private usedProperty = 4; + 37 37 │ class TsBioo2 { + 38 38 │ private unusedProperty = 5; + 39 │ - → private·unusedMethod()·{} + 40 39 │ + 41 40 │ private usedProperty = 4; ``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts new file mode 100644 index 000000000000..a33fb19adc6f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts @@ -0,0 +1,50 @@ +class UsedMember { + get #usedAccessor() {} + set #usedAccessor(value) {} + + method() { + // no return statement so no meaningful read + this.#usedAccessor = 42; + } +} + +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + // not really used, a is not reference to this scope + foo = a.#usedInInnerClass; + } + } +} + +class UsedMember { + set #accessorUsedInMemberAccess(value) {} // <- unused + + method(a) { + // there is no getter, so this is not a read at all + [this.#accessorUsedInMemberAccess] = a; + } +} + +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + } + } +} + +class C { + set #x(value) { + doSomething(value); + } + + foo() { + // no return statement so not a meaningful read. + this.#x = 1; + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts.snap new file mode 100644 index 000000000000..5dc6be36f197 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts.snap @@ -0,0 +1,194 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: invalid_aligned_with_semantic_class.ts +--- +# Input +```ts +class UsedMember { + get #usedAccessor() {} + set #usedAccessor(value) {} + + method() { + // no return statement so no meaningful read + this.#usedAccessor = 42; + } +} + +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + // not really used, a is not reference to this scope + foo = a.#usedInInnerClass; + } + } +} + +class UsedMember { + set #accessorUsedInMemberAccess(value) {} // <- unused + + method(a) { + // there is no getter, so this is not a read at all + [this.#accessorUsedInMemberAccess] = a; + } +} + +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + } + } +} + +class C { + set #x(value) { + doSomething(value); + } + + foo() { + // no return statement so not a meaningful read. + this.#x = 1; + } +} + +``` + +# Diagnostics +``` +invalid_aligned_with_semantic_class.ts:2:6 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━ + + ! This private class member is defined but never used. + + 1 │ class UsedMember { + > 2 │ get #usedAccessor() {} + │ ^^^^^^^^^^^^^ + 3 │ set #usedAccessor(value) {} + 4 │ + + i Unsafe fix: Remove unused declaration. + + 1 1 │ class UsedMember { + 2 │ - → get·#usedAccessor()·{} + 3 2 │ set #usedAccessor(value) {} + 4 3 │ + + +``` + +``` +invalid_aligned_with_semantic_class.ts:3:6 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━ + + ! This private class member is defined but never used. + + 1 │ class UsedMember { + 2 │ get #usedAccessor() {} + > 3 │ set #usedAccessor(value) {} + │ ^^^^^^^^^^^^^ + 4 │ + 5 │ method() { + + i Unsafe fix: Remove unused declaration. + + 1 1 │ class UsedMember { + 2 2 │ get #usedAccessor() {} + 3 │ - → set·#usedAccessor(value)·{} + 4 3 │ + 5 4 │ method() { + + +``` + +``` +invalid_aligned_with_semantic_class.ts:12:2 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━ + + ! This private class member is defined but never used. + + 11 │ class UsedMember { + > 12 │ #usedInInnerClass; + │ ^^^^^^^^^^^^^^^^^ + 13 │ + 14 │ method(a) { + + i Unsafe fix: Remove unused declaration. + + 10 10 │ + 11 11 │ class UsedMember { + 12 │ - → #usedInInnerClass; + 13 12 │ + 14 13 │ method(a) { + + +``` + +``` +invalid_aligned_with_semantic_class.ts:23:6 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━ + + ! This private class member is defined but never used. + + 22 │ class UsedMember { + > 23 │ set #accessorUsedInMemberAccess(value) {} // <- unused + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 24 │ + 25 │ method(a) { + + i Unsafe fix: Remove unused declaration. + + 21 21 │ + 22 22 │ class UsedMember { + 23 │ - → set·#accessorUsedInMemberAccess(value)·{}·//·<-·unused + 24 23 │ + 25 24 │ method(a) { + + +``` + +``` +invalid_aligned_with_semantic_class.ts:32:2 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━ + + ! This private class member is defined but never used. + + 31 │ class UsedMember { + > 32 │ #usedInInnerClass; + │ ^^^^^^^^^^^^^^^^^ + 33 │ + 34 │ method(a) { + + i Unsafe fix: Remove unused declaration. + + 30 30 │ + 31 31 │ class UsedMember { + 32 │ - → #usedInInnerClass; + 33 32 │ + 34 33 │ method(a) { + + +``` + +``` +invalid_aligned_with_semantic_class.ts:42:6 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━ + + ! This private class member is defined but never used. + + 41 │ class C { + > 42 │ set #x(value) { + │ ^^ + 43 │ doSomething(value); + 44 │ } + + i Unsafe fix: Remove unused declaration. + + 40 40 │ + 41 41 │ class C { + 42 │ - → set·#x(value)·{ + 43 │ - → → doSomething(value); + 44 │ - → } + 45 42 │ + 46 43 │ foo() { + + +``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts.snap index 37ba429b1951..90bb2cb28fa2 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts.snap @@ -1,5 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 expression: invalid_dynamic_access.ts --- # Input @@ -21,6 +22,27 @@ export class Sample { ``` # Diagnostics +``` +invalid_dynamic_access.ts:2:11 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━ + + ! This private class member is defined but never used. + + 1 │ export class Sample { + > 2 │ private member; + │ ^^^^^^ + 3 │ #prop; + 4 │ + + i Unsafe fix: Remove unused declaration. + + 1 1 │ export class Sample { + 2 │ - ··private·member; + 3 2 │ #prop; + 4 3 │ + + +``` + ``` invalid_dynamic_access.ts:3:3 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━ diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts index 3bbebf4b0578..2cfca0a128bb 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts @@ -1,5 +1,9 @@ class TSDoubleUnusedPrivateConstructor { - constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { + constructor( + usedProperty = 3, + private unusedProperty: number, + private anotherUnusedProperty = 4 + ) { // This constructor has two unused private properties } @@ -7,6 +11,7 @@ class TSDoubleUnusedPrivateConstructor { class TSPartiallyUsedPrivateConstructor { constructor(private param: number) { + // this is not read or write as far as class members are concerned. foo(param) } -} \ No newline at end of file +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts.snap index ff3616cfd334..f068a487f041 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_issue_7101.ts.snap @@ -1,11 +1,16 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 expression: invalid_issue_7101.ts --- # Input ```ts class TSDoubleUnusedPrivateConstructor { - constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { + constructor( + usedProperty = 3, + private unusedProperty: number, + private anotherUnusedProperty = 4 + ) { // This constructor has two unused private properties } @@ -13,70 +18,78 @@ class TSDoubleUnusedPrivateConstructor { class TSPartiallyUsedPrivateConstructor { constructor(private param: number) { + // this is not read or write as far as class members are concerned. foo(param) } } + ``` # Diagnostics ``` -invalid_issue_7101.ts:2:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ +invalid_issue_7101.ts:4:11 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 1 │ class TSDoubleUnusedPrivateConstructor { - > 2 │ constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { - │ ^^^^^^^^^^^^^^^ - 3 │ // This constructor has two unused private properties - 4 │ + 2 │ constructor( + 3 │ usedProperty = 3, + > 4 │ private unusedProperty: number, + │ ^^^^^^^^^^^^^^ + 5 │ private anotherUnusedProperty = 4 + 6 │ ) { i Unsafe fix: Remove private modifier 1 1 │ class TSDoubleUnusedPrivateConstructor { - 2 │ - → constructor(private·unusedProperty·=·3,·private·anotherUnusedProperty·=·4)·{ - 2 │ + → constructor(_unusedProperty·=·3,·private·anotherUnusedProperty·=·4)·{ - 3 3 │ // This constructor has two unused private properties - 4 4 │ + 2 2 │ constructor( + 3 │ - → → usedProperty·=·3, + 4 │ - → → private·unusedProperty:·number, + 3 │ + → → usedProperty·=·3,_unusedProperty:·number, + 5 4 │ private anotherUnusedProperty = 4 + 6 5 │ ) { ``` ``` -invalid_issue_7101.ts:2:50 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ +invalid_issue_7101.ts:5:11 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 1 │ class TSDoubleUnusedPrivateConstructor { - > 2 │ constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { - │ ^^^^^^^^^^^^^^^^^^^^^^ - 3 │ // This constructor has two unused private properties - 4 │ + 3 │ usedProperty = 3, + 4 │ private unusedProperty: number, + > 5 │ private anotherUnusedProperty = 4 + │ ^^^^^^^^^^^^^^^^^^^^^ + 6 │ ) { + 7 │ // This constructor has two unused private properties i Unsafe fix: Remove private modifier - 1 1 │ class TSDoubleUnusedPrivateConstructor { - 2 │ - → constructor(private·unusedProperty·=·3,·private·anotherUnusedProperty·=·4)·{ - 2 │ + → constructor(private·unusedProperty·=·3,·_anotherUnusedProperty·=·4)·{ - 3 3 │ // This constructor has two unused private properties - 4 4 │ + 2 2 │ constructor( + 3 3 │ usedProperty = 3, + 4 │ - → → private·unusedProperty:·number, + 5 │ - → → private·anotherUnusedProperty·=·4 + 4 │ + → → private·unusedProperty:·number,_anotherUnusedProperty·=·4 + 6 5 │ ) { + 7 6 │ // This constructor has two unused private properties ``` ``` -invalid_issue_7101.ts:9:23 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ +invalid_issue_7101.ts:13:23 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━ ! This parameter is never used outside of the constructor. - 8 │ class TSPartiallyUsedPrivateConstructor { - > 9 │ constructor(private param: number) { + 12 │ class TSPartiallyUsedPrivateConstructor { + > 13 │ constructor(private param: number) { │ ^^^^^ - 10 │ foo(param) - 11 │ } + 14 │ // this is not read or write as far as class members are concerned. + 15 │ foo(param) i Unsafe fix: Remove private modifier - 9 │ ··constructor(private·param:·number)·{ - │ -------- + 13 │ ··constructor(private·param:·number)·{ + │ -------- ``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js index 871a2b04e716..74c8cc3fe119 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js @@ -19,15 +19,6 @@ class UsedMember { } -class UsedMember { - get #usedAccessor() {} - set #usedAccessor(value) {} - - method() { - this.#usedAccessor = 42; - } -} - class UsedMember { publicMember = 42; } @@ -131,14 +122,6 @@ class UsedMember { } } -class UsedMember { - set #accessorUsedInMemberAccess(value) {} - - method(a) { - [this.#accessorUsedInMemberAccess] = a; - } -} - class UsedMember { get #accessorWithGetterFirst() { return something(); @@ -151,16 +134,6 @@ class UsedMember { } } -class UsedMember { - #usedInInnerClass; - - method(a) { - return class { - foo = a.#usedInInnerClass; - } - } -} - class Foo { #usedMethod() { return 42; @@ -170,16 +143,6 @@ class Foo { } } -class C { - set #x(value) { - doSomething(value); - } - - foo() { - this.#x = 1; - } -} - // issue #6994 class UsedAssignmentExpr { #val = 0; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js.snap index 539e488c1dfc..79ae10755bdf 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js.snap @@ -1,5 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 expression: valid.js --- # Input @@ -25,15 +26,6 @@ class UsedMember { } -class UsedMember { - get #usedAccessor() {} - set #usedAccessor(value) {} - - method() { - this.#usedAccessor = 42; - } -} - class UsedMember { publicMember = 42; } @@ -137,14 +129,6 @@ class UsedMember { } } -class UsedMember { - set #accessorUsedInMemberAccess(value) {} - - method(a) { - [this.#accessorUsedInMemberAccess] = a; - } -} - class UsedMember { get #accessorWithGetterFirst() { return something(); @@ -157,16 +141,6 @@ class UsedMember { } } -class UsedMember { - #usedInInnerClass; - - method(a) { - return class { - foo = a.#usedInInnerClass; - } - } -} - class Foo { #usedMethod() { return 42; @@ -176,16 +150,6 @@ class Foo { } } -class C { - set #x(value) { - doSomething(value); - } - - foo() { - this.#x = 1; - } -} - // issue #6994 class UsedAssignmentExpr { #val = 0; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js new file mode 100644 index 000000000000..bb6e63152922 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js @@ -0,0 +1,9 @@ + +/* should not generate diagnostics */ + +class Foo { + #usedOnlyInWriteStatement = 5; + method() { + this.#usedOnlyInWriteStatement += 42; + } +} diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js.snap new file mode 100644 index 000000000000..22d40de03a3b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js.snap @@ -0,0 +1,18 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 152 +expression: valid_aligned_with_semantic_class.js +--- +# Input +```js + +/* should not generate diagnostics */ + +class Foo { + #usedOnlyInWriteStatement = 5; + method() { + this.#usedOnlyInWriteStatement += 42; + } +} + +```