From cd5fa03e6631eb9b488ab7156037f86b9053dab5 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 19 Nov 2025 21:22:45 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Revert=20"feat(biome=5Fjs=5Fanalyze):=20ali?= =?UTF-8?q?gn=20no=5Funused=5Fprivate=5Fclass=5Fmembers=5Fwith=5Fse?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f4433b34e3ad9686bdde08727453e3caf0409412. --- .../no_unused_private_class_members.rs | 295 +++-- .../style/use_readonly_class_properties.rs | 117 +- .../src/lint/suspicious/no_class_assign.rs | 1 + .../src/services/semantic_class.rs | 1028 ++++++----------- .../noUnusedPrivateClassMembers/invalid.js | 7 + .../invalid.js.snap | 48 +- .../noUnusedPrivateClassMembers/invalid.ts | 3 +- .../invalid.ts.snap | 137 +-- .../invalid_aligned_with_semantic_class.ts | 50 - ...nvalid_aligned_with_semantic_class.ts.snap | 194 ---- .../invalid_dynamic_access.ts | 78 +- .../invalid_dynamic_access.ts.snap | 366 +----- .../invalid_issue_7101.ts | 9 +- .../invalid_issue_7101.ts.snap | 71 +- .../noUnusedPrivateClassMembers/valid.js | 37 + .../noUnusedPrivateClassMembers/valid.js.snap | 38 +- .../valid_aligned_with_semantic_class.js | 9 - .../valid_aligned_with_semantic_class.js.snap | 18 - .../valid_dynamic_access.ts | 14 +- .../valid_dynamic_access.ts.snap | 14 +- 20 files changed, 906 insertions(+), 1628 deletions(-) delete mode 100644 crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts delete mode 100644 crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts.snap delete mode 100644 crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js delete mode 100644 crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js.snap 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 cf8d69bd7b30..28efac642c93 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,9 +1,8 @@ -use crate::JsRuleAction; -use crate::services::semantic_class::{ - AccessKind, AnyNamedClassMember, ClassMemberReference, ClassMemberReferences, SemanticClass, - SemanticClassModel, +use crate::{ + JsRuleAction, + services::semantic::Semantic, + utils::{is_node_equal, rename::RenameSymbolExtensions}, }; -use crate::utils::rename::RenameSymbolExtensions; use biome_analyze::{ FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, }; @@ -11,11 +10,13 @@ use biome_console::markup; use biome_diagnostics::Severity; use biome_js_semantic::ReferencesExtensions; use biome_js_syntax::{ - AnyJsClassMember, AnyJsClassMemberName, AnyJsFormalParameter, JsClassDeclaration, - TsAccessibilityModifier, TsPropertyParameter, + AnyJsClassMember, AnyJsClassMemberName, AnyJsComputedMember, AnyJsExpression, + AnyJsFormalParameter, AnyJsName, JsAssignmentExpression, JsClassDeclaration, JsSyntaxKind, + JsSyntaxNode, TsAccessibilityModifier, TsPropertyParameter, }; use biome_rowan::{ - AstNode, AstNodeList, AstSeparatedList, BatchMutationExt, Text, TextRange, declare_node_union, + AstNode, AstNodeList, AstSeparatedList, BatchMutationExt, SyntaxNodeOptionExt, TextRange, + declare_node_union, }; use biome_rule_options::no_unused_private_class_members::NoUnusedPrivateClassMembersOptions; @@ -34,7 +35,7 @@ declare_lint_rule! { /// #usedOnlyInWrite = 5; /// /// method() { - /// this.#usedOnlyInWrite = 212; + /// this.#usedOnlyInWrite = 212; /// } /// } /// ``` @@ -58,7 +59,7 @@ declare_lint_rule! { /// #usedMember = 42; /// /// method() { - /// return this.#usedMember; + /// return this.#usedMember; /// } /// } /// ``` @@ -71,10 +72,9 @@ declare_lint_rule! { /// ```ts /// class TsBioo { /// private member: number; - /// private anotherMember: number; /// - /// set_with_name(name: 'member' | 'anotherMember', value: number) { - /// this[name](); + /// set_with_name(name: string, value: number) { + /// this[name] = value; /// } /// } /// ``` @@ -96,9 +96,7 @@ declare_node_union! { #[derive(Debug, Clone)] pub enum UnusedMemberAction { - RemoveMember { - member: AnyMember, - }, + RemoveMember(AnyMember), RemovePrivateModifier { member: AnyMember, rename_with_underscore: bool, @@ -106,16 +104,16 @@ pub enum UnusedMemberAction { } impl UnusedMemberAction { - fn property_range(&self, semantic_class: &SemanticClassModel) -> Option { + fn property_range(&self) -> Option { match self { - Self::RemoveMember { member } => member.member_range(semantic_class), - Self::RemovePrivateModifier { member, .. } => member.member_range(semantic_class), + Self::RemoveMember(member) => member.property_range(), + Self::RemovePrivateModifier { member, .. } => member.property_range(), } } } impl Rule for NoUnusedPrivateClassMembers { - type Query = SemanticClass; + type Query = Semantic; type State = UnusedMemberAction; type Signals = Box<[Self::State]>; type Options = NoUnusedPrivateClassMembersOptions; @@ -127,20 +125,12 @@ impl Rule for NoUnusedPrivateClassMembers { Box::default() } else { let mut results = Vec::new(); - let semantic_class = ctx.semantic_class(); - - let class_member_references = semantic_class.class_member_references(&node.members()); - - let unused_members = traverse_meaningful_read_members_usage( - semantic_class, - private_members, - &class_member_references, - ); + let unused_members = traverse_members_usage(node.syntax(), private_members); for member in unused_members { match &member { AnyMember::AnyJsClassMember(_) => { - results.push(UnusedMemberAction::RemoveMember { member }); + results.push(UnusedMemberAction::RemoveMember(member)); } AnyMember::TsPropertyParameter(ts_property_param) => { // Check if the parameter is also unused in constructor body using semantic analysis @@ -157,11 +147,11 @@ impl Rule for NoUnusedPrivateClassMembers { } } - fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { match state { - UnusedMemberAction::RemoveMember { .. } => Some(RuleDiagnostic::new( + UnusedMemberAction::RemoveMember(_) => Some(RuleDiagnostic::new( rule_category!(), - state.property_range(ctx.semantic_class()), + state.property_range(), markup! { "This private class member is defined but never used." }, @@ -173,7 +163,7 @@ impl Rule for NoUnusedPrivateClassMembers { if *rename_with_underscore { Some(RuleDiagnostic::new( rule_category!(), - state.property_range(ctx.semantic_class()), + state.property_range(), markup! { "This private class member is defined but never used." }, @@ -181,7 +171,7 @@ impl Rule for NoUnusedPrivateClassMembers { } else { Some(RuleDiagnostic::new( rule_category!(), - state.property_range(ctx.semantic_class()), + state.property_range(), markup! { "This parameter is never used outside of the constructor." }, @@ -195,7 +185,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()), @@ -207,7 +197,6 @@ impl Rule for NoUnusedPrivateClassMembers { UnusedMemberAction::RemovePrivateModifier { member, rename_with_underscore, - .. } => { if let AnyMember::TsPropertyParameter(ts_property_param) = member { // Remove the private modifier @@ -233,7 +222,7 @@ impl Rule for NoUnusedPrivateClassMembers { let name_trimmed = name_token.text_trimmed(); let new_name = format!("_{name_trimmed}"); if !mutation.rename_node_declaration( - ctx.semantic(), + ctx.model(), identifier_binding, &new_name, ) { @@ -252,33 +241,65 @@ impl Rule for NoUnusedPrivateClassMembers { } } -/// 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, +/// 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: Vec, ) -> Vec { - let ClassMemberReferences { reads, .. } = class_member_references; + // `true` is at least one member is a TypeScript private member like `private member`. + // The other private members are sharp members `#member`. + let mut ts_private_count = private_members + .iter() + .filter(|member| !member.is_private_sharp()) + .count(); - 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 + 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; + } } - }) - .collect() + Err(node) => { + if ts_private_count != 0 + && let Some(computed_member) = AnyJsComputedMember::cast(node) + && matches!( + computed_member.object(), + Ok(AnyJsExpression::JsThisExpression(_)) + ) + { + // We consider that all TypeScript private members are used in expressions like `this[something]`. + private_members.retain(|private_member| private_member.is_private_sharp()); + ts_private_count = 0; + } + } + } + } + + private_members } /// Check if a TsPropertyParameter is also unused as a function parameter @@ -304,7 +325,7 @@ fn check_ts_property_parameter_usage( } if identifier_binding - .all_references(ctx.semantic()) + .all_references(ctx.model()) .next() .is_some() { @@ -353,7 +374,81 @@ 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 { + matches!( + member.name(), + Ok(Some(AnyJsClassMemberName::JsPrivateClassMemberName(_))) + ) + } else { + false + } + } + fn is_private(&self) -> Option { match self { Self::AnyJsClassMember(member) => { @@ -397,23 +492,69 @@ impl AnyMember { } } - 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 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(), + ), + }, } - - None } - 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); - } + fn match_js_name(&self, js_name: &AnyJsName) -> Option { + let value_token = js_name.value_token().ok()?; + let token = value_token.text_trimmed(); - false + 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, + ), + }, + } } } 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 6d2ebf36c304..ac4b770d14b8 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,7 +1,6 @@ use crate::JsRuleAction; use crate::services::semantic_class::{ - AnyNamedClassMember, ClassMemberReference, ClassMemberReferences, NamedClassMember, - SemanticClass, + AnyPropertyMember, ClassMemberReference, ClassMemberReferences, SemanticClass, }; use biome_analyze::{ FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, @@ -11,10 +10,11 @@ use biome_js_factory::make; use biome_js_syntax::{ AnyJsClassMember, AnyJsClassMemberName, AnyJsConstructorParameter, AnyJsPropertyModifier, AnyTsPropertyParameterModifier, JsClassDeclaration, JsClassMemberList, JsFileSource, - JsSyntaxKind, JsSyntaxToken, TsAccessibilityModifier, TsPropertyParameter, TsReadonlyModifier, + JsSyntaxKind, JsSyntaxToken, TextRange, TsAccessibilityModifier, TsPropertyParameter, + TsReadonlyModifier, }; use biome_rowan::{ - AstNode, AstNodeExt, AstNodeList, AstSeparatedList, BatchMutationExt, TriviaPiece, + AstNode, AstNodeExt, AstNodeList, AstSeparatedList, BatchMutationExt, Text, 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 = AnyNamedClassMember; + type State = AnyPropertyMember; type Signals = Box<[Self::State]>; type Options = UseReadonlyClassPropertiesOptions; @@ -138,8 +138,7 @@ impl Rule for UseReadonlyClassProperties { let root = ctx.query(); let members = root.members(); - let ClassMemberReferences { writes, .. } = - ctx.semantic_class().class_member_references(&members); + let ClassMemberReferences { writes, .. } = ctx.model.class_member_references(&members); let private_only = !ctx.options().check_all_properties(); let constructor_params: Vec<_> = @@ -158,23 +157,19 @@ impl Rule for UseReadonlyClassProperties { }), ) .filter_map(|prop_or_param| { - if writes.clone().into_iter().any( - |ClassMemberReference { - name: class_member_ref_name, - .. - }| { - if let Some(NamedClassMember { - name: member_name, .. - }) = ctx - .semantic_class() - .extract_named_member(&prop_or_param.clone()) + if writes + .clone() + .into_iter() + .any(|ClassMemberReference { name, .. }| { + if let Some(TextAndRange { text, .. }) = + extract_property_or_param_range_and_text(&prop_or_param.clone()) { - return class_member_ref_name.eq(&member_name); + return name.eq(&text); } false - }, - ) { + }) + { None } else { Some(prop_or_param.clone()) @@ -184,24 +179,19 @@ impl Rule for UseReadonlyClassProperties { .into_boxed_slice() } - fn diagnostic(ctx: &RuleContext, node: &Self::State) -> Option { - let semantic_class = ctx.semantic_class(); - if let Some(NamedClassMember { name, range }) = - semantic_class.extract_named_member(&node.clone()) - { - return Some(RuleDiagnostic::new( - rule_category!(), - range, - markup! { - "Member '"{name.text()}"' is never reassigned." + 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." }, - ).note(markup! { + ).note(markup! { "Using ""readonly"" improves code safety, clarity, and helps prevent unintended mutations." }), - ); - } - - None + ) } fn action(ctx: &RuleContext, node: &Self::State) -> Option { @@ -214,8 +204,8 @@ impl Rule for UseReadonlyClassProperties { [TriviaPiece::whitespace(1)], )); - if let Some(AnyNamedClassMember::JsPropertyClassMember(member)) = - AnyNamedClassMember::cast(original_node.clone()) + if let Some(AnyPropertyMember::JsPropertyClassMember(member)) = + AnyPropertyMember::cast(original_node.clone()) { if let Ok(member_name) = member.name() { let replace_modifiers = make::js_property_modifier_list( @@ -249,8 +239,8 @@ impl Rule for UseReadonlyClassProperties { mutation.replace_node(member.clone(), builder.build()); } } - } else if let Some(AnyNamedClassMember::TsPropertyParameter(parameter)) = - AnyNamedClassMember::cast(original_node.clone()) + } else if let Some(AnyPropertyMember::TsPropertyParameter(parameter)) = + AnyPropertyMember::cast(original_node.clone()) { let replace_modifiers = make::ts_property_parameter_modifier_list( parameter @@ -282,6 +272,12 @@ 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`. @@ -289,7 +285,7 @@ impl Rule for UseReadonlyClassProperties { 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()?; @@ -307,7 +303,7 @@ fn collect_non_readonly_class_member_properties( return None; } - let some_property = Some(AnyNamedClassMember::JsPropertyClassMember( + let some_property = Some(AnyPropertyMember::JsPropertyClassMember( property_class_member.clone(), )); @@ -336,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 { @@ -350,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(AnyNamedClassMember::TsPropertyParameter(ts_property)) + Some(AnyPropertyMember::TsPropertyParameter(ts_property)) } _ => None, }) @@ -398,3 +394,38 @@ 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/lint/suspicious/no_class_assign.rs b/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs index 264a4eea9fe8..6855b85a051c 100644 --- a/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs +++ b/crates/biome_js_analyze/src/lint/suspicious/no_class_assign.rs @@ -98,6 +98,7 @@ impl Rule for NoClassAssign { Vec::new().into_boxed_slice() } + fn diagnostic(ctx: &RuleContext, reference: &Self::State) -> Option { let binding = ctx .query() diff --git a/crates/biome_js_analyze/src/services/semantic_class.rs b/crates/biome_js_analyze/src/services/semantic_class.rs index be2a53771e3d..295be13d01c2 100644 --- a/crates/biome_js_analyze/src/services/semantic_class.rs +++ b/crates/biome_js_analyze/src/services/semantic_class.rs @@ -1,82 +1,39 @@ use biome_analyze::{ AddVisitor, FromServices, Phase, Phases, QueryKey, QueryMatch, Queryable, RuleKey, - RuleMetadata, ServiceBag, ServicesDiagnostic, SyntaxVisitor, Visitor, VisitorContext, - VisitorFinishContext, + RuleMetadata, ServiceBag, ServicesDiagnostic, Visitor, VisitorContext, VisitorFinishContext, }; -use biome_js_semantic::{SemanticEventExtractor, SemanticModel, SemanticModelBuilder}; use biome_js_syntax::{ - AnyJsBindingPattern, AnyJsClassMember, AnyJsComputedMember, AnyJsExpression, - AnyJsObjectBindingPatternMember, AnyJsRoot, AnyTsType, JsArrayAssignmentPattern, - JsArrowFunctionExpression, JsAssignmentExpression, JsClassDeclaration, JsClassMemberList, - JsConstructorClassMember, JsFormalParameter, JsFunctionBody, JsGetterClassMember, - JsIdentifierExpression, JsLanguage, JsMethodClassMember, JsObjectAssignmentPattern, - JsObjectBindingPattern, JsPostUpdateExpression, JsPreUpdateExpression, JsPropertyClassMember, - JsSetterClassMember, JsStaticMemberAssignment, JsStaticMemberExpression, JsSyntaxKind, - JsSyntaxNode, JsThisExpression, JsVariableDeclarator, TextRange, TsIndexSignatureClassMember, - TsPropertyParameter, TsReferenceType, TsStringLiteralType, TsTypeAliasDeclaration, TsUnionType, + AnyJsBindingPattern, AnyJsClassMember, AnyJsExpression, AnyJsObjectBindingPatternMember, + AnyJsRoot, JsArrayAssignmentPattern, JsArrowFunctionExpression, JsAssignmentExpression, + JsClassDeclaration, JsClassMemberList, JsConstructorClassMember, JsFunctionBody, JsLanguage, + JsObjectAssignmentPattern, JsObjectBindingPattern, JsPostUpdateExpression, + JsPreUpdateExpression, JsPropertyClassMember, JsStaticMemberAssignment, + JsStaticMemberExpression, JsSyntaxKind, JsSyntaxNode, JsVariableDeclarator, TextRange, + TsPropertyParameter, }; use biome_rowan::{ - AstNode, AstNodeList, AstSeparatedList, SyntaxNode, SyntaxNodePtr, Text, WalkEvent, - declare_node_union, + AstNode, AstNodeList, AstSeparatedList, SyntaxNode, Text, WalkEvent, declare_node_union, }; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::cell::RefCell; +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 { - semantic_class: SemanticClassModel, - semantic: SemanticModel, + pub model: SemanticClassModel, } impl SemanticClassServices { - pub fn semantic_class(&self) -> &SemanticClassModel { - &self.semantic_class - } - - pub fn semantic(&self) -> &SemanticModel { - &self.semantic + pub fn model(&self) -> &SemanticClassModel { + &self.model } } #[derive(Debug, Clone)] -pub struct SemanticClassModel { - pub semantic: SemanticModel, - named_members_cache: RefCell, Option>>, -} +pub struct SemanticClassModel {} impl SemanticClassModel { - pub fn new(semantic: SemanticModel) -> Self { - Self { - semantic, - named_members_cache: RefCell::new(Default::default()), - } - } - pub fn class_member_references(&self, members: &JsClassMemberList) -> ClassMemberReferences { - class_member_references(&self.semantic, members) - } - - pub fn extract_named_member( - &self, - any_class_member: &AnyNamedClassMember, - ) -> Option { - let ptr = SyntaxNodePtr::new(any_class_member.syntax()); - if let Some(cached) = self.named_members_cache.borrow().get(&ptr) { - return cached.clone(); - } - - let result = extract_named_member(any_class_member); - self.named_members_cache - .borrow_mut() - .insert(ptr, result.clone()); - result + class_member_references(members) } } @@ -90,8 +47,7 @@ impl FromServices for SemanticClassServices { ServicesDiagnostic::new(rule_key.rule_name(), &["SemanticClassModel"]) })?; Ok(Self { - semantic_class: service.clone(), - semantic: service.semantic.clone(), + model: service.clone(), }) } } @@ -102,44 +58,38 @@ impl Phase for SemanticClassServices { } } -pub struct SyntaxClassMemberReferencesVisitor { - extractor: SemanticEventExtractor, - builder: SemanticModelBuilder, -} - -impl SyntaxClassMemberReferencesVisitor { - pub(crate) fn new(root: AnyJsRoot) -> Self { - Self { - extractor: SemanticEventExtractor::default(), - builder: SemanticModelBuilder::new(root), - } - } -} +pub struct SyntaxClassMemberReferencesVisitor {} impl Visitor for SyntaxClassMemberReferencesVisitor { type Language = JsLanguage; - fn visit(&mut self, event: &WalkEvent, _ctx: VisitorContext) { - match event { - WalkEvent::Enter(node) => { - self.builder.push_node(node); - self.extractor.enter(node); - } - WalkEvent::Leave(node) => { - self.extractor.leave(node); - } - } - - while let Some(e) = self.extractor.pop() { - self.builder.push_event(e); - } + fn visit( + &mut self, + _event: &WalkEvent, + mut _ctx: VisitorContext<'_, '_, JsLanguage>, + ) { } fn finish(self: Box, ctx: VisitorFinishContext) { - let semantic = self.builder.build(); + ctx.services.insert_service(SemanticClassModel {}); + } +} + +pub struct SemanticClassMemberReferencesVisitor {} + +impl Visitor for SemanticClassMemberReferencesVisitor { + type Language = JsLanguage; - ctx.services - .insert_service(SemanticClassModel::new(semantic)); + fn visit( + &mut self, + event: &WalkEvent, + mut ctx: VisitorContext<'_, '_, JsLanguage>, + ) { + if let WalkEvent::Enter(node) = event + && JsClassDeclaration::can_cast(node.kind()) + { + ctx.match_query(node.clone()); + } } } @@ -162,11 +112,9 @@ where type Language = JsLanguage; type Services = SemanticClassServices; - fn build_visitor(analyzer: &mut impl AddVisitor, root: &AnyJsRoot) { - analyzer.add_visitor(Phases::Syntax, || { - SyntaxClassMemberReferencesVisitor::new(root.clone()) - }); - analyzer.add_visitor(Phases::Semantic, SyntaxVisitor::default); + fn build_visitor(analyzer: &mut impl AddVisitor, _root: &AnyJsRoot) { + analyzer.add_visitor(Phases::Syntax, || SyntaxClassMemberReferencesVisitor {}); + analyzer.add_visitor(Phases::Semantic, || SemanticClassMemberReferencesVisitor {}); } fn key() -> QueryKey { @@ -217,70 +165,17 @@ pub struct ClassMemberReferences { } declare_node_union! { - /// 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; } + pub AnyPropertyMember = JsPropertyClassMember | TsPropertyParameter } declare_node_union! { - pub AnyCandidateForUsedInExpressionNode = AnyJsExpression | AnyJsUpdateExpression | AnyJsObjectBindingPatternMember | JsStaticMemberExpression | AnyJsBindingPattern | JsStaticMemberAssignment | AnyJsComputedMember + pub AnyCandidateForUsedInExpressionNode = AnyJsUpdateExpression | AnyJsObjectBindingPatternMember | JsStaticMemberExpression | AnyJsBindingPattern } declare_node_union! { pub AnyJsUpdateExpression = JsPreUpdateExpression | JsPostUpdateExpression } -fn to_named(name: &impl AstNode) -> Option { - Some(NamedClassMember { - name: name.to_trimmed_text(), - range: name.range(), - }) -} - -/// 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()?; - to_named(&name_node) - } - - AnyNamedClassMember::JsGetterClassMember(getter) => { - let name_node = getter.name().ok()?; - to_named(&name_node) - } - - AnyNamedClassMember::JsSetterClassMember(setter) => { - let name_node = setter.name().ok()?; - to_named(&name_node) - } - - AnyNamedClassMember::JsPropertyClassMember(member) => { - let name_node = member.name().ok()?; - to_named(&name_node) - } - - AnyNamedClassMember::TsPropertyParameter(parameter) => { - let name_node = parameter - .formal_parameter() - .ok()? - .as_js_formal_parameter()? - .binding() - .ok()?; - to_named(&name_node) - } - - 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, @@ -288,76 +183,83 @@ fn extract_named_member(any_class_member: &AnyNamedClassMember) -> Option ClassMemberReferences { - list.iter() +fn class_member_references(list: &JsClassMemberList) -> ClassMemberReferences { + let all_references: Vec = list + .iter() .filter_map(|member| match member { AnyJsClassMember::JsMethodClassMember(method) => method .body() .ok() - .and_then(|body| collect_references_from_body(semantic, method.syntax(), &body)), + .and_then(|body| collect_references_from_body(method.syntax(), &body)), AnyJsClassMember::JsSetterClassMember(setter) => setter .body() .ok() - .and_then(|body| collect_references_from_body(semantic, setter.syntax(), &body)), + .and_then(|body| collect_references_from_body(setter.syntax(), &body)), AnyJsClassMember::JsGetterClassMember(getter) => getter .body() .ok() - .and_then(|body| collect_references_from_body(semantic, getter.syntax(), &body)), + .and_then(|body| collect_references_from_body(getter.syntax(), &body)), AnyJsClassMember::JsPropertyClassMember(property) => { - property.value()?.expression().ok().and_then(|expr| { - if let Some(arrow) = JsArrowFunctionExpression::cast(expr.syntax().clone()) { - arrow.body().ok()?.as_js_function_body().and_then(|body| { - collect_references_from_body(semantic, arrow.syntax(), body) - }) - } else { - expr.as_js_static_member_expression().map(|static_member| { - collect_class_property_reads_from_static_member(static_member) - }) + if let Ok(expression) = property.value()?.expression() { + if let Some(arrow_function) = + JsArrowFunctionExpression::cast(expression.clone().into_syntax()) + { + if let Ok(any_js_body) = arrow_function.body() + && let Some(body) = any_js_body.as_js_function_body() + { + return collect_references_from_body(arrow_function.syntax(), body); + } + } else if let Some(static_member_expression) = + expression.as_js_static_member_expression() + { + return collect_class_property_reads_from_static_member( + static_member_expression, + ); } - }) + }; + None } AnyJsClassMember::JsConstructorClassMember(constructor) => constructor .body() .ok() - .map(|body| collect_references_from_constructor(semantic, &body)), + .map(|body| collect_references_from_constructor(&body)), _ => None, }) - .fold( - ClassMemberReferences { - reads: FxHashSet::default(), - writes: FxHashSet::default(), - }, - |mut acc, refs| { - acc.reads.extend(refs.reads); - acc.writes.extend(refs.writes); - acc - }, - ) + .collect(); + + let mut combined_reads = FxHashSet::default(); + let mut combined_writes = FxHashSet::default(); + + for refs in all_references { + combined_reads.extend(refs.reads); + combined_writes.extend(refs.writes); + } + + ClassMemberReferences { + reads: combined_reads, + writes: combined_writes, + } } /// Represents a function body and all `this` references (including aliases) valid within its lexical scope. #[derive(Clone, Debug)] -struct FunctionThisAliases { +struct FunctionThisReferences { scope: JsFunctionBody, - this_aliases: FxHashSet, + this_references: FxHashSet, } /// A visitor that collects `this` references in nested function scopes, /// while skipping class expressions and tracking inherited this references. -struct ThisScopeVisitor { +struct ThisScopeVisitor<'a> { skipped_ranges: Vec, - inherited_this_aliases: FxHashSet, - current_this_scopes: Vec, + inherited_this_references: &'a [ClassMemberReference], + current_this_scopes: Vec, } // Can not implement `Visitor` directly because it requires a new ctx that can not be created here -impl ThisScopeVisitor { +impl ThisScopeVisitor<'_> { fn visit(&mut self, event: &WalkEvent>) { match event { WalkEvent::Enter(node) => { - // Skip nodes inside already-handled ranges (e.g., nested classes) if self .skipped_ranges .iter() @@ -366,60 +268,53 @@ impl ThisScopeVisitor { return; } - match node.kind() { - // Skip nested classes entirely - JsSyntaxKind::JS_CLASS_EXPRESSION | JsSyntaxKind::JS_CLASS_DECLARATION => { - self.skipped_ranges.push(node.text_range()); - } + if node.kind() == JsSyntaxKind::JS_CLASS_EXPRESSION { + self.skipped_ranges.push(node.text_range()); + return; + } - // Regular function body (non-constructor) - JsSyntaxKind::JS_FUNCTION_BODY => { - if let Some(body) = JsFunctionBody::cast_ref(node) { - let is_constructor = node - .parent() - .and_then(JsConstructorClassMember::cast) - .is_some(); - - if !is_constructor { - let current_scope = - ThisScopeReferences::new(&body).local_this_aliases; - let mut scoped_this_references = FxHashSet::default(); - scoped_this_references - .extend(self.inherited_this_aliases.iter().cloned()); - scoped_this_references.extend(current_scope); - - self.current_this_scopes.push(FunctionThisAliases { - scope: body.clone(), - this_aliases: scoped_this_references, - }); - } - } - } + if node.kind() == JsSyntaxKind::JS_CLASS_DECLARATION { + self.skipped_ranges.push(node.text_range()); + return; + } - // Arrow functions - JsSyntaxKind::JS_ARROW_FUNCTION_EXPRESSION => { - if let Some(func_expr) = JsArrowFunctionExpression::cast_ref(node) - && let Some(body) = func_expr - .body() - .ok() - .and_then(|b| b.as_js_function_body().cloned()) - { - let current_scope_aliases = - ThisScopeReferences::new(&body).local_this_aliases; - let mut scoped_this_references = FxHashSet::default(); - scoped_this_references - .extend(self.inherited_this_aliases.iter().cloned()); - scoped_this_references.extend(current_scope_aliases.clone()); - - self.current_this_scopes.push(FunctionThisAliases { - scope: body.clone(), - this_aliases: scoped_this_references, - }); - } + if let Some(body) = JsFunctionBody::cast_ref(node) { + // Only process if not part of constructor + let is_constructor = node + .parent() + .and_then(JsConstructorClassMember::cast) + .is_some(); + + if !is_constructor { + let current_scope = ThisScopeReferences::new(&body).local_this_references; + let mut scoped_this_references = FxHashSet::default(); + scoped_this_references + .extend(self.inherited_this_references.iter().cloned()); + scoped_this_references.extend(current_scope); + + self.current_this_scopes.push(FunctionThisReferences { + scope: body.clone(), + this_references: scoped_this_references, + }); } + } - // Everything else: do nothing - _ => {} + if let Some(func_expr) = JsArrowFunctionExpression::cast_ref(node) + && let Some(body) = func_expr + .body() + .ok() + .and_then(|b| b.as_js_function_body().cloned()) + { + let current_scope_aliases = + ThisScopeReferences::new(&body).local_this_references; + let mut scoped_this_references = FxHashSet::default(); + scoped_this_references.extend(self.inherited_this_references.iter().cloned()); + scoped_this_references.extend(current_scope_aliases.clone()); + + self.current_this_scopes.push(FunctionThisReferences { + scope: body.clone(), + this_references: scoped_this_references, + }); } } @@ -439,25 +334,25 @@ struct ThisScopeReferences { /// Any js function body body: JsFunctionBody, /// this scope references found within the immediate function scope body, excludes nested scopes - local_this_aliases: FxHashSet, + local_this_references: Vec, } impl ThisScopeReferences { fn new(body: &JsFunctionBody) -> Self { Self { body: body.clone(), - local_this_aliases: Self::collect_local_this_aliases(body), + local_this_references: Self::collect_local_this_references(body), } } /// Collects all `this` scope references in the function body and nested /// functions using `ThisScopeVisitor`, combining local and inherited ones - /// into a list of `FunctionThisAliases`. - fn collect_function_this_aliases(&self) -> Vec { + /// into a list of `FunctionThisReferences`. + fn collect_function_this_references(&self) -> Vec { let mut visitor = ThisScopeVisitor { skipped_ranges: vec![], current_this_scopes: vec![], - inherited_this_aliases: self.local_this_aliases.clone(), + inherited_this_references: self.local_this_references.as_slice(), }; let iter = self.body.syntax().preorder(); @@ -468,8 +363,8 @@ impl ThisScopeReferences { visitor.current_this_scopes } - /// Collects local this aliases of `this` in a function body. - fn collect_local_this_aliases(body: &JsFunctionBody) -> FxHashSet { + /// Collects local references of `this` in a function body. + fn collect_local_this_references(body: &JsFunctionBody) -> Vec { body.statements() .iter() .filter_map(|node| node.as_js_variable_statement().cloned()) @@ -483,13 +378,15 @@ impl ThisScopeReferences { let id = fields.id.ok()?; let expr = fields.initializer?.expression().ok()?; let unwrapped = &expr.omit_parentheses(); - - // Only direct `this` assignments (not this.prop) - if JsThisExpression::can_cast(unwrapped.syntax().kind()) { - Some(id.syntax().text_trimmed().into_text()) - } else { - None - } + (unwrapped.syntax().first_token()?.text_trimmed() == "this").then(|| { + ClassMemberReference { + name: id.to_trimmed_text().clone(), + range: id.syntax().text_trimmed_range(), + access_kind: get_read_access_kind( + &AnyCandidateForUsedInExpressionNode::from(id), + ), + } + }) }) .collect() } @@ -498,17 +395,18 @@ impl ThisScopeReferences { /// Checks if a given expression is a reference to `this` or any of its aliases. fn is_this_reference( js_expression: &AnyJsExpression, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], ) -> bool { - // Direct `this` expression if let Some(this_expr) = js_expression.as_js_this_expression() { let syntax = this_expr.syntax(); - return scoped_this_references.iter().any(|func_scope| { - is_within_scope_without_shadowing(syntax, func_scope.scope.syntax()) - }); + + return scoped_this_references + .iter() + .any(|FunctionThisReferences { scope, .. }| { + is_within_scope_without_shadowing(syntax, scope.syntax()) + }); } - // Identifier alias if let Some(js_identifier_expression) = js_expression.as_js_identifier_expression() && let Ok(name) = js_identifier_expression.name() && let Ok(value_token) = name.value_token() @@ -516,15 +414,21 @@ fn is_this_reference( let name_syntax = name.syntax(); scoped_this_references.iter().any( - |FunctionThisAliases { + |FunctionThisReferences { + this_references, scope, - this_aliases, }| { - if !this_aliases.contains(value_token.token_text_trimmed().text()) { - return false; // not an alias → skip expensive scope check - } + let is_alias = this_references.iter().any(|mutation| { + mutation + .name + .text() + .eq(value_token.token_text_trimmed().text()) + }); + + let is_within_scope = + is_within_scope_without_shadowing(name_syntax, scope.syntax()); - is_within_scope_without_shadowing(name_syntax, scope.syntax()) + is_alias && is_within_scope }, ) } else { @@ -540,13 +444,13 @@ impl ThisPatternResolver { /// Only applicable to writes. fn collect_array_assignment_names( array_assignment_pattern: &JsArrayAssignmentPattern, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], ) -> Vec { array_assignment_pattern .elements() .iter() .filter_map(|element| { - let element = element.as_ref().ok()?; + let element = element.clone().ok()?; // [this.#value] if let Some(pattern_element) = element.as_js_array_assignment_pattern_element() { @@ -588,7 +492,7 @@ impl ThisPatternResolver { /// Only applicable to writes. fn collect_object_assignment_names( assignment: &JsObjectAssignmentPattern, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], ) -> Vec { assignment .properties() @@ -596,7 +500,7 @@ impl ThisPatternResolver { .filter_map(|prop| { if let Some(rest_params) = prop .node - .as_ref() + .clone() .ok()? .as_js_object_assignment_pattern_rest() { @@ -608,7 +512,7 @@ impl ThisPatternResolver { } if let Some(property) = prop .node - .as_ref() + .clone() .ok()? .as_js_object_assignment_pattern_property() { @@ -637,7 +541,7 @@ impl ThisPatternResolver { /// Returns a `ClassMemberReference` containing the member name and its range. fn extract_this_member_reference( operand: Option<&JsStaticMemberAssignment>, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], access_kind: AccessKind, ) -> Option { operand.and_then(|assignment| { @@ -672,21 +576,15 @@ impl ThisPatternResolver { /// Collects `this`-based member references from a class method or property initializer body. /// Gathers reads and writes by analyzing the function body and its `this` references (and its aliases). fn collect_references_from_body( - semantic: &SemanticModel, member: &JsSyntaxNode, body: &JsFunctionBody, ) -> Option { - let scoped_this_references = ThisScopeReferences::new(body).collect_function_this_aliases(); + let scoped_this_references = ThisScopeReferences::new(body).collect_function_this_references(); + let mut reads = FxHashSet::default(); let mut writes = FxHashSet::default(); - visit_references_in_body( - semantic, - member, - &scoped_this_references, - &mut writes, - &mut reads, - ); + visit_references_in_body(member, &scoped_this_references, &mut writes, &mut reads); Some(ClassMemberReferences { reads, writes }) } @@ -698,9 +596,8 @@ fn collect_references_from_body( /// - Reads via `this.prop`, `this.#prop`, and compound assignments (e.g., `this.prop += 1`) /// - Writes via assignments and destructuring patterns involving `this` or its aliases fn visit_references_in_body( - semantic: &SemanticModel, method_body_element: &JsSyntaxNode, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], writes: &mut FxHashSet, reads: &mut FxHashSet, ) { @@ -709,38 +606,16 @@ fn visit_references_in_body( for event in iter { match event { WalkEvent::Enter(node) => { - match node.kind() { - JsSyntaxKind::JS_OBJECT_BINDING_PATTERN => { - handle_object_binding_pattern(&node, scoped_this_references, reads); - } - JsSyntaxKind::JS_COMPUTED_MEMBER_EXPRESSION - | JsSyntaxKind::JS_COMPUTED_MEMBER_ASSIGNMENT => { - handle_dynamic_member_expression( - &node, - scoped_this_references, - semantic, - reads, - ); - } - JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION => { - handle_assignment_expression(&node, scoped_this_references, reads, writes); - } - JsSyntaxKind::JS_STATIC_MEMBER_EXPRESSION => { - handle_static_member_expression(&node, scoped_this_references, reads); - } - JsSyntaxKind::JS_PRE_UPDATE_EXPRESSION - | JsSyntaxKind::JS_POST_UPDATE_EXPRESSION => { - // Handle both ++a and a++ in the same handler - if let Some(update_expr) = AnyJsUpdateExpression::cast_ref(&node) { - handle_pre_or_post_update_expression( - &update_expr, - scoped_this_references, - reads, - writes, - ); - } - } - _ => {} + handle_object_binding_pattern(&node, scoped_this_references, reads); + handle_static_member_expression(&node, scoped_this_references, reads); + handle_assignment_expression(&node, scoped_this_references, reads, writes); + if let Some(js_update_expression) = AnyJsUpdateExpression::cast_ref(&node) { + handle_pre_or_post_update_expression( + &js_update_expression, + scoped_this_references, + reads, + writes, + ); } } WalkEvent::Leave(_) => {} @@ -765,7 +640,7 @@ fn visit_references_in_body( /// ``` fn handle_object_binding_pattern( node: &SyntaxNode, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], reads: &mut FxHashSet, ) { if let Some(binding) = JsObjectBindingPattern::cast_ref(node) @@ -778,14 +653,10 @@ fn handle_object_binding_pattern( if let Some(declarator) = declarator.ok() && is_this_reference(&expression, scoped_this_references) { - let name = declarator.to_trimmed_text(); // allocate only the text - let range = declarator.syntax().text_trimmed_range(); reads.insert(ClassMemberReference { - name, - range, - access_kind: get_read_access_kind(&AnyCandidateForUsedInExpressionNode::from( - declarator.clone(), - )), + name: declarator.clone().to_trimmed_text(), + range: declarator.clone().syntax().text_trimmed_range(), + access_kind: get_read_access_kind(&declarator.clone().into()), }); } } @@ -810,7 +681,7 @@ fn handle_object_binding_pattern( /// ``` fn handle_static_member_expression( node: &SyntaxNode, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], reads: &mut FxHashSet, ) { if let Some(static_member) = JsStaticMemberExpression::cast_ref(node) @@ -826,40 +697,6 @@ fn handle_static_member_expression( } } -/// we assume that any usage in an expression context is meaningful read, and writes are much less likely -/// so skip the dynamic writes -fn handle_dynamic_member_expression( - node: &SyntaxNode, - scoped_this_references: &[FunctionThisAliases], - semantic: &SemanticModel, - reads: &mut FxHashSet, -) { - if let Some(dynamic_member) = AnyJsComputedMember::cast(node.clone()) - && let Ok(object) = dynamic_member.object() - && is_this_reference(&object, scoped_this_references) - && let Ok(member_expr) = dynamic_member.member() - && let Some(id_expr) = JsIdentifierExpression::cast_ref(member_expr.syntax()) - && let Some(ty) = resolve_formal_param_type(semantic, &id_expr) - && let Some(ts_union_type) = TsUnionType::cast(ty.syntax().clone()) - .or_else(|| resolve_reference_to_union(semantic, &ty)) - { - let items: Vec<_> = extract_literal_types(&ts_union_type); - - for item in items.iter() { - reads.insert(ClassMemberReference { - // we keep the range of the dynamic accessed member - range: member_expr.range(), - // swap the name for the actual resolved type - name: item.clone(), - - access_kind: get_read_access_kind(&AnyCandidateForUsedInExpressionNode::from( - dynamic_member.clone(), - )), - }); - } - } -} - /// Detects reads and writes to `this` properties inside assignment expressions. /// /// - Compound assignments like `this.x += 1` produce a read and a write. @@ -879,14 +716,13 @@ fn handle_dynamic_member_expression( /// ``` fn handle_assignment_expression( node: &SyntaxNode, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], reads: &mut FxHashSet, writes: &mut FxHashSet, ) { if let Some(assignment) = JsAssignmentExpression::cast_ref(node) && let Ok(left) = assignment.left() { - // Compound assignment -> meaningful read if let Ok(operator) = assignment.operator_token() && let Some(operand) = left.as_any_js_assignment() && matches!( @@ -905,32 +741,27 @@ fn handle_assignment_expression( AccessKind::MeaningfulRead, ) { - reads.insert(name); + reads.insert(name.clone()); } - // Array assignment pattern - if let Some(array) = left.as_js_array_assignment_pattern() { + if let Some(array) = left.as_js_array_assignment_pattern().cloned() { for class_member_reference in - ThisPatternResolver::collect_array_assignment_names(array, scoped_this_references) + ThisPatternResolver::collect_array_assignment_names(&array, scoped_this_references) { - writes.insert(class_member_reference); + writes.insert(class_member_reference.clone()); } } - // Object assignment pattern - if let Some(object) = left.as_js_object_assignment_pattern() { - for class_member_reference in - ThisPatternResolver::collect_object_assignment_names(object, scoped_this_references) - { - match class_member_reference.access_kind { - AccessKind::Write => writes.insert(class_member_reference), - _ => reads.insert(class_member_reference), - }; + if let Some(object) = left.as_js_object_assignment_pattern().cloned() { + for class_member_reference in ThisPatternResolver::collect_object_assignment_names( + &object, + scoped_this_references, + ) { + writes.insert(class_member_reference.clone()); } } - // Plain assignment - if let Some(assignment) = left.as_any_js_assignment() + if let Some(assignment) = left.as_any_js_assignment().cloned() && let Some(name) = ThisPatternResolver::extract_this_member_reference( assignment.as_js_static_member_assignment(), scoped_this_references, @@ -938,21 +769,6 @@ 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, - } - }); - } } } } @@ -973,7 +789,7 @@ fn handle_assignment_expression( /// ``` fn handle_pre_or_post_update_expression( js_update_expression: &AnyJsUpdateExpression, - scoped_this_references: &[FunctionThisAliases], + scoped_this_references: &[FunctionThisReferences], reads: &mut FxHashSet, writes: &mut FxHashSet, ) { @@ -1002,18 +818,14 @@ fn handle_pre_or_post_update_expression( /// Collects read and write references to `this` members within a class constructor body, /// including any nested functions that capture `this` via aliasing. -fn collect_references_from_constructor( - semantic: &SemanticModel, - constructor_body: &JsFunctionBody, -) -> ClassMemberReferences { +fn collect_references_from_constructor(constructor_body: &JsFunctionBody) -> ClassMemberReferences { let all_descendants_fn_bodies_and_this_scopes: Vec<_> = - ThisScopeReferences::new(constructor_body).collect_function_this_aliases(); + ThisScopeReferences::new(constructor_body).collect_function_this_references(); let mut reads = FxHashSet::default(); let mut writes = FxHashSet::default(); for this_scope in all_descendants_fn_bodies_and_this_scopes.iter() { visit_references_in_body( - semantic, this_scope.scope.syntax(), std::slice::from_ref(this_scope), &mut writes, @@ -1032,7 +844,7 @@ fn collect_references_from_constructor( /// No write references are collected. fn collect_class_property_reads_from_static_member( static_member: &JsStaticMemberExpression, -) -> ClassMemberReferences { +) -> Option { let mut reads = FxHashSet::default(); let writes = FxHashSet::default(); @@ -1047,7 +859,7 @@ fn collect_class_property_reads_from_static_member( }); } - ClassMemberReferences { reads, writes } + Some(ClassMemberReferences { reads, writes }) } /// Checks whether a name is within its correct scope @@ -1055,9 +867,8 @@ fn is_within_scope_without_shadowing( name_syntax: &SyntaxNode, scope: &SyntaxNode, ) -> bool { - let scope_key = scope.key(); for ancestor in name_syntax.ancestors() { - if ancestor.key() == scope_key { + if ancestor.key() == scope.key() { return true; } @@ -1086,153 +897,33 @@ 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 { - let node_syntax = node.syntax(); - node_syntax.ancestors().any(|ancestor| { - is_class_initializer_rhs(&ancestor) - || is_assignment_expression_context(node, &ancestor) - || is_general_expression_context(&ancestor) + 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 + ) }) } -/// 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_ref(ancestor) { - 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 - ) -} - -/// Extracts the immediate string literal types only from a union like `A | B | C`. -/// Filters out any non string literal type. -/// Does not recurse into nested unions. -fn extract_literal_types(union: &TsUnionType) -> Vec { - extract_shallow_union_members(union) - .iter() - .filter_map(|item| { - if let Some(literal_type) = TsStringLiteralType::cast(item.syntax().clone()) { - return Some(Text::new_owned(Box::from( - literal_type - .to_trimmed_text() - .trim_matches(&['"', '\''][..]), - ))); - } - - None - }) - .collect() -} - -/// Extracts the immediate types from a union like `A | B | C`. -/// Does not recurse into nested unions. -fn extract_shallow_union_members(union: &TsUnionType) -> Vec { - union.types().into_iter().flatten().collect() -} - -/// Attempts to resolve the type annotation of a formal parameter for the given identifier expression. -/// - Looks up the binding for the identifier expression in the semantic model. -/// - Checks if the binding corresponds to a `JsFormalParameter`. -/// - If so, extracts and returns its type annotation. -fn resolve_formal_param_type( - model: &SemanticModel, - id_expr: &JsIdentifierExpression, -) -> Option { - let ref_ident = id_expr.name().ok()?; - let binding = model.binding(&ref_ident)?; - let parent_node = binding.syntax().parent()?; - - // Only proceed if parent is a formal parameter - let js_param = JsFormalParameter::cast_ref(&parent_node)?; - let type_annotation = js_param.type_annotation()?; - type_annotation.ty().ok() -} - -/// Resolves a type reference to its aliased union type, if the reference points to a union. -fn resolve_reference_to_union(model: &SemanticModel, ty: &AnyTsType) -> Option { - let ts_reference_type = TsReferenceType::cast_ref(ty.syntax())?; - let ref_name = ts_reference_type.name().ok()?; - let ref_ident = ref_name.as_js_reference_identifier()?; - - let binding = model.binding(ref_ident)?; - let parent_node = binding.syntax().parent()?; - - let type_alias = TsTypeAliasDeclaration::cast_ref(&parent_node)?; - let ty = type_alias.ty().ok()?; - - TsUnionType::cast_ref(ty.syntax()) -} - #[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; @@ -1249,16 +940,22 @@ mod tests { expected: &[(&str, AccessKind)], description: &str, ) { - for (expected_name, _) in expected { - reads + for (expected_name, expected_access_kind) in expected { + let found = reads .iter() .find(|r| r.name.clone().text() == *expected_name) .unwrap_or_else(|| { panic!( - "Case '{}' failed: expected to find read '{}', but none was found in {:#?}", - description, expected_name, reads + "Case '{}' failed: expected to find read '{}'", + description, expected_name ) }); + + assert_eq!( + found.access_kind, *expected_access_kind, + "Case '{}' failed: read '{}' access_kind mismatch", + description, expected_name + ); } } @@ -1267,16 +964,22 @@ mod tests { expected: &[(&str, AccessKind)], description: &str, ) { - for (expected_name, _) in expected { - writes + for (expected_name, expected_access_kind) in expected { + let found = writes .iter() .find(|r| r.name.clone().text() == *expected_name) .unwrap_or_else(|| { panic!( - "Case '{}' failed: expected to find write '{}' in {:#?}", - description, expected_name, writes + "Case '{}' failed: expected to find write '{}'", + description, expected_name ) }); + + assert_eq!( + found.access_kind, *expected_access_kind, + "Case '{}' failed: write '{}' access_kind mismatch", + description, expected_name + ); } } @@ -1345,13 +1048,13 @@ mod tests { .expect("No function body found"); let function_this_references = - ThisScopeReferences::new(&body).collect_function_this_aliases(); + ThisScopeReferences::new(&body).collect_function_this_references(); let node = parse_first_object_binding(body.syntax()); let mut reads = FxHashSet::default(); handle_object_binding_pattern(&node, &function_this_references, &mut reads); - assert_reads(&reads, case.expected_reads.as_slice(), case.description); + assert_reads(&reads, &case.expected_reads, case.description); } } @@ -1402,7 +1105,7 @@ mod tests { .expect("No function body found"); let function_this_references = - ThisScopeReferences::new(&body).collect_function_this_aliases(); + ThisScopeReferences::new(&body).collect_function_this_references(); let mut reads = FxHashSet::default(); @@ -1436,7 +1139,11 @@ mod tests { } "#, expected_reads: vec![("x", AccessKind::MeaningfulRead)], // x is read due to += - expected_writes: vec![("x", AccessKind::Write), ("y", AccessKind::Write)], + expected_writes: vec![ + ("x", AccessKind::Write), + ("y", AccessKind::Write), + ("z", AccessKind::Write), + ], }, TestCase { description: "assignment reads and writes with aliasForThis", @@ -1452,13 +1159,11 @@ mod tests { } "#, expected_reads: vec![("x", AccessKind::MeaningfulRead)], - 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)], + expected_writes: vec![ + ("x", AccessKind::Write), + ("y", AccessKind::Write), + ("z", AccessKind::Write), + ], }, ]; @@ -1471,7 +1176,7 @@ mod tests { .expect("No function body found"); let function_this_references = - ThisScopeReferences::new(&body).collect_function_this_aliases(); + ThisScopeReferences::new(&body).collect_function_this_references(); let mut reads = FxHashSet::default(); let mut writes = FxHashSet::default(); @@ -1560,7 +1265,7 @@ mod tests { .expect("No function body found"); let function_this_references = - ThisScopeReferences::new(&body).collect_function_this_aliases(); + ThisScopeReferences::new(&body).collect_function_this_references(); let mut reads = FxHashSet::default(); let mut writes = FxHashSet::default(); @@ -1583,62 +1288,92 @@ mod tests { mod is_used_in_expression_context_tests { use super::*; + use biome_js_syntax::binding_ext::AnyJsIdentifierBinding; - 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 { + fn extract_all_nodes(code: &str) -> Vec { let parsed = parse_ts(code); let root = parsed.syntax(); + let mut nodes = vec![]; for descendant in root.descendants() { - // 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()) + // 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()) { - nodes.push(node.clone()); + 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 } + 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 = parse_this_member_nodes_from_code(case.code); + let nodes = extract_all_nodes(case.code); assert!( !nodes.is_empty(), - "No nodes found for test case: {}", + "No match 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 '{}'", + "Number of nodes does not match expected for test case: '{}'", case.description ); - for (node, (expected_name, expected_flag)) in nodes.iter().zip(&case.expected) { - let name = node.to_trimmed_text(); + 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(); assert_eq!( - &name, expected_name, - "Node name mismatch for '{}'", + &node_name, expected_name, + "Node name mismatch for test case: '{}'", case.description ); - let actual_flag = is_used_in_expression_context(node); + // Compare is_meaningful_read + let actual_meaningful = is_used_in_expression_context(&meaningful_node); assert_eq!( - actual_flag, *expected_flag, - "Meaningful read mismatch for '{}' in '{}'", + actual_meaningful, *expected_access_kind, + "Meaningful read mismatch for node '{}' in test case: '{}'", expected_name, case.description ); } @@ -1646,11 +1381,11 @@ mod tests { } #[test] - fn test_major_expression_contexts() { + fn test_is_used_in_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 { @@ -1661,12 +1396,27 @@ mod tests { TestCase { description: "conditional expression", code: r#"class Test { method() { const a = this.z ? 1 : 2; } }"#, - expected: vec![("this.z", true)], + expected: vec![("a", false), ("this.z", true)], }, TestCase { description: "logical expression", code: r#"class Test { method() { const a = this.a && this.b; } }"#, - expected: vec![("this.a", true), ("this.b", true)], + 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)], }, TestCase { description: "unary expression", @@ -1674,19 +1424,19 @@ mod tests { expected: vec![("this.num", true)], }, TestCase { - description: "template literal", + description: "template expression", code: r#"class Test { method() { `${this.str}`; } }"#, expected: vec![("this.str", true)], }, TestCase { - description: "binary expression", - code: r#"class Test { method() { const sum = this.a + this.b; } }"#, - expected: vec![("this.a", true), ("this.b", true)], + description: "call expression callee", + code: r#"class Test { method() { this.func(); } }"#, + expected: vec![("this.func", true)], }, TestCase { - description: "assignment RHS", - code: r#"class Test { method() { this.x = 5 + this.x; } }"#, - expected: vec![("this.x", true)], + description: "new expression", + code: r#"class Test { method() { new this.ClassName(); } }"#, + expected: vec![("this.ClassName", true)], }, TestCase { description: "if statement", @@ -1700,82 +1450,32 @@ mod tests { }, TestCase { description: "for statement", - code: r#"class Test { method() { for(this.i = 0; this.i < 10; this.i++) {} } }"#, - expected: vec![("this.i", true)], + 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)], }, TestCase { - description: "throw statement", - code: r#"class Test { method() { throw this.err; } }"#, - expected: vec![("this.err", true)], + description: "binary expression", + code: r#"class Test { method() { const sum = this.a + this.b; } }"#, + expected: vec![("sum", false), ("this.a", true), ("this.b", true)], }, TestCase { - description: "await expression", - code: r#"class Test { async method() { await this.promise; } }"#, - expected: vec![("this.promise", true)], + 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)], }, TestCase { - description: "yield expression", - code: r#"class Test { *method() { yield this.gen; } }"#, - expected: vec![("this.gen", true)], + 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), + ], }, ]; 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 541cbf8c4816..a50f161afb3b 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js @@ -38,6 +38,13 @@ 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 d248d6725758..0559a78fe7fd 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,6 +1,5 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 152 expression: invalid.js --- # Input @@ -45,6 +44,13 @@ class Foo { } } +class Foo { + #usedOnlyInWriteStatement = 5; + method() { + this.#usedOnlyInWriteStatement += 42; + } +} + class C { #usedOnlyInIncrement; @@ -242,19 +248,41 @@ invalid.js:42:2 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━ ! This private class member is defined but never used. - 41 │ class C { - > 42 │ #usedOnlyInIncrement; - │ ^^^^^^^^^^^^^^^^^^^^ - 43 │ - 44 │ foo() { + 41 │ class Foo { + > 42 │ #usedOnlyInWriteStatement = 5; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + 43 │ method() { + 44 │ this.#usedOnlyInWriteStatement += 42; i Unsafe fix: Remove unused declaration. 40 40 │ - 41 41 │ class C { - 42 │ - → #usedOnlyInIncrement; - 43 42 │ - 44 43 │ foo() { + 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; + │ ^^^^^^^^^^^^^^^^^^^^ + 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/correctness/noUnusedPrivateClassMembers/invalid.ts b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts index 97b01b49c59d..05e41dc353e2 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts @@ -7,11 +7,12 @@ class TsBioo { } class TSUnusedPrivateConstructor { - constructor(private unusedProperty = 3){ + constructor(private nusedProperty = 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 26f0267b68b7..e7d8b3bcf595 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: 152 +assertion_line: 146 expression: invalid.ts --- # Input @@ -14,11 +14,12 @@ class TsBioo { } class TSUnusedPrivateConstructor { - constructor(private unusedProperty = 3){ + constructor(private nusedProperty = 3){ } } + class TsOnlyWrite { private usedOnlyInWrite = 5; @@ -108,7 +109,7 @@ invalid.ts:10:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━ ! This private class member is defined but never used. 9 │ class TSUnusedPrivateConstructor { - > 10 │ constructor(private unusedProperty = 3){ + > 10 │ constructor(private nusedProperty = 3){ │ ^^^^^^^^^^^^^^ 11 │ 12 │ } @@ -117,8 +118,8 @@ invalid.ts:10:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━ 8 8 │ 9 9 │ class TSUnusedPrivateConstructor { - 10 │ - → constructor(private·unusedProperty·=·3){ - 10 │ + → constructor(_unusedProperty·=·3){ + 10 │ - → constructor(private·nusedProperty·=·3){ + 10 │ + → constructor(_nusedProperty·=·3){ 11 11 │ 12 12 │ } @@ -126,136 +127,136 @@ invalid.ts:10:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━ ``` ``` -invalid.ts:16:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:17:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 15 │ class TsOnlyWrite { - > 16 │ private usedOnlyInWrite = 5; + 16 │ class TsOnlyWrite { + > 17 │ private usedOnlyInWrite = 5; │ ^^^^^^^^^^^^^^^ - 17 │ - 18 │ method() { + 18 │ + 19 │ method() { i Unsafe fix: Remove unused declaration. - 14 14 │ - 15 15 │ class TsOnlyWrite { - 16 │ - → private·usedOnlyInWrite·=·5; - 17 16 │ - 18 17 │ method() { + 15 15 │ + 16 16 │ class TsOnlyWrite { + 17 │ - → private·usedOnlyInWrite·=·5; + 18 17 │ + 19 18 │ method() { ``` ``` -invalid.ts:24:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:25:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 23 │ class TsSelfUpdate { - > 24 │ private usedOnlyToUpdateItself = 5; + 24 │ class TsSelfUpdate { + > 25 │ private usedOnlyToUpdateItself = 5; │ ^^^^^^^^^^^^^^^^^^^^^^ - 25 │ - 26 │ method() { + 26 │ + 27 │ method() { i Unsafe fix: Remove unused declaration. - 22 22 │ - 23 23 │ class TsSelfUpdate { - 24 │ - → private·usedOnlyToUpdateItself·=·5; - 25 24 │ - 26 25 │ method() { + 23 23 │ + 24 24 │ class TsSelfUpdate { + 25 │ - → private·usedOnlyToUpdateItself·=·5; + 26 25 │ + 27 26 │ method() { ``` ``` -invalid.ts:32:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:33:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 31 │ class TsAccessor { - > 32 │ private get unusedAccessor() { } + 32 │ class TsAccessor { + > 33 │ private get unusedAccessor() { } │ ^^^^^^^^^^^^^^ - 33 │ private set unusedAccessor(value) { } - 34 │ } + 34 │ private set unusedAccessor(value) { } + 35 │ } i Unsafe fix: Remove unused declaration. - 30 30 │ - 31 31 │ class TsAccessor { - 32 │ - → private·get·unusedAccessor()·{·} - 33 32 │ private set unusedAccessor(value) { } - 34 33 │ } + 31 31 │ + 32 32 │ class TsAccessor { + 33 │ - → private·get·unusedAccessor()·{·} + 34 33 │ private set unusedAccessor(value) { } + 35 34 │ } ``` ``` -invalid.ts:33:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:34:14 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 31 │ class TsAccessor { - 32 │ private get unusedAccessor() { } - > 33 │ private set unusedAccessor(value) { } + 32 │ class TsAccessor { + 33 │ private get unusedAccessor() { } + > 34 │ private set unusedAccessor(value) { } │ ^^^^^^^^^^^^^^ - 34 │ } - 35 │ + 35 │ } + 36 │ i Unsafe fix: Remove unused declaration. - 31 31 │ class TsAccessor { - 32 32 │ private get unusedAccessor() { } - 33 │ - → private·set·unusedAccessor(value)·{·} - 34 33 │ } - 35 34 │ + 32 32 │ class TsAccessor { + 33 33 │ private get unusedAccessor() { } + 34 │ - → private·set·unusedAccessor(value)·{·} + 35 34 │ } + 36 35 │ ``` ``` -invalid.ts:38:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:39:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 36 │ // github.com/biomejs/biome/issues/6165 - 37 │ class TsBioo2 { - > 38 │ private unusedProperty = 5; + 37 │ // github.com/biomejs/biome/issues/6165 + 38 │ class TsBioo2 { + > 39 │ private unusedProperty = 5; │ ^^^^^^^^^^^^^^ - 39 │ private unusedMethod() {} - 40 │ + 40 │ private unusedMethod() {} + 41 │ i Unsafe fix: Remove unused declaration. - 36 36 │ // github.com/biomejs/biome/issues/6165 - 37 37 │ class TsBioo2 { - 38 │ - → private·unusedProperty·=·5; - 39 38 │ private unusedMethod() {} - 40 39 │ + 37 37 │ // github.com/biomejs/biome/issues/6165 + 38 38 │ class TsBioo2 { + 39 │ - → private·unusedProperty·=·5; + 40 39 │ private unusedMethod() {} + 41 40 │ ``` ``` -invalid.ts:39:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.ts:40:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 37 │ class TsBioo2 { - 38 │ private unusedProperty = 5; - > 39 │ private unusedMethod() {} + 38 │ class TsBioo2 { + 39 │ private unusedProperty = 5; + > 40 │ private unusedMethod() {} │ ^^^^^^^^^^^^ - 40 │ - 41 │ private usedProperty = 4; + 41 │ + 42 │ private usedProperty = 4; i Unsafe fix: Remove unused declaration. - 37 37 │ class TsBioo2 { - 38 38 │ private unusedProperty = 5; - 39 │ - → private·unusedMethod()·{} - 40 39 │ - 41 40 │ private usedProperty = 4; + 38 38 │ class TsBioo2 { + 39 39 │ private unusedProperty = 5; + 40 │ - → private·unusedMethod()·{} + 41 40 │ + 42 41 │ 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 deleted file mode 100644 index a33fb19adc6f..000000000000 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 5dc6be36f197..000000000000 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_aligned_with_semantic_class.ts.snap +++ /dev/null @@ -1,194 +0,0 @@ ---- -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 b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts index 3fea3a591260..c777df2e9a9e 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid_dynamic_access.ts @@ -1,73 +1,13 @@ export class Sample { - private member; - #prop; + private member; + #prop; - constructor() { - this.#prop = 0; - this.member = 0; - } + constructor() { + this.#prop = 0; + this.member = 0; + } - method(name) { - return this[name]; - } + method(name) { + return this[name]; + } } - -export class SampleAddRemove { - private add: () => void; - private append: () => void; // <- unused - - constructor(private remove: () => void) { - this.add = () => { - }; - this.remove = () => { - }; - } - - on(action: "add" | "remove"): void { - this[action](); - } -} - -// will only make a match on the string literals and ignore anything else -type YesNo = "yes" | "no" | { ignored: number }; - -export class SampleYesNo { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; // <- unused - - on(action: YesNo): void { - this[action](); - } -} - -export class SampleYesNoString { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; - - on(action: string): void { - this[action](); - } -} - -export class SampleYesNoAny { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; - - on(action: any): void { - this[action](); - } -} - -export class SampleYesNoUnknown { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; - - on(action: unknown): void { - this[action](); - } -} - 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 b51384ff6123..37ba429b1951 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,377 +1,45 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 152 expression: invalid_dynamic_access.ts --- # Input ```ts export class Sample { - private member; - #prop; + private member; + #prop; - constructor() { - this.#prop = 0; - this.member = 0; - } + constructor() { + this.#prop = 0; + this.member = 0; + } - method(name) { - return this[name]; - } + method(name) { + return this[name]; + } } -export class SampleAddRemove { - private add: () => void; - private append: () => void; // <- unused - - constructor(private remove: () => void) { - this.add = () => { - }; - this.remove = () => { - }; - } - - on(action: "add" | "remove"): void { - this[action](); - } -} - -// will only make a match on the string literals and ignore anything else -type YesNo = "yes" | "no" | { ignored: number }; - -export class SampleYesNo { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; // <- unused - - on(action: YesNo): void { - this[action](); - } -} - -export class SampleYesNoString { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; - - on(action: string): void { - this[action](); - } -} - -export class SampleYesNoAny { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; - - on(action: any): void { - this[action](); - } -} - -export class SampleYesNoUnknown { - private yes: () => void; - private no: () => void; - private dontKnow: () => void; - - on(action: unknown): void { - this[action](); - } -} - - ``` # Diagnostics ``` -invalid_dynamic_access.ts:2:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━ +invalid_dynamic_access.ts:3:3 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━ ! This private class member is defined but never used. 1 │ export class Sample { - > 2 │ private member; - │ ^^^^^^ - 3 │ #prop; + 2 │ private member; + > 3 │ #prop; + │ ^^^^^ 4 │ + 5 │ constructor() { i Unsafe fix: Remove unused declaration. 1 1 │ export class Sample { - 2 │ - → private·member; - 3 2 │ #prop; + 2 2 │ private member; + 3 │ - ··#prop; 4 3 │ - - -``` - -``` -invalid_dynamic_access.ts:3:2 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 1 │ export class Sample { - 2 │ private member; - > 3 │ #prop; - │ ^^^^^ - 4 │ - 5 │ constructor() { - - i Unsafe fix: Remove unused declaration. - - 1 1 │ export class Sample { - 2 2 │ private member; - 3 │ - → #prop; - 4 3 │ - 5 4 │ constructor() { - - -``` - -``` -invalid_dynamic_access.ts:17:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 15 │ export class SampleAddRemove { - 16 │ private add: () => void; - > 17 │ private append: () => void; // <- unused - │ ^^^^^^ - 18 │ - 19 │ constructor(private remove: () => void) { - - i Unsafe fix: Remove unused declaration. - - 15 15 │ export class SampleAddRemove { - 16 16 │ private add: () => void; - 17 │ - → private·append:·()·=>·void;·//·<-·unused - 18 17 │ - 19 18 │ constructor(private remove: () => void) { - - -``` - -``` -invalid_dynamic_access.ts:37:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 35 │ private yes: () => void; - 36 │ private no: () => void; - > 37 │ private dontKnow: () => void; // <- unused - │ ^^^^^^^^ - 38 │ - 39 │ on(action: YesNo): void { - - i Unsafe fix: Remove unused declaration. - - 35 35 │ private yes: () => void; - 36 36 │ private no: () => void; - 37 │ - → private·dontKnow:·()·=>·void;·//·<-·unused - 38 37 │ - 39 38 │ on(action: YesNo): void { - - -``` - -``` -invalid_dynamic_access.ts:45:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 44 │ export class SampleYesNoString { - > 45 │ private yes: () => void; - │ ^^^ - 46 │ private no: () => void; - 47 │ private dontKnow: () => void; - - i Unsafe fix: Remove unused declaration. - - 43 43 │ - 44 44 │ export class SampleYesNoString { - 45 │ - → private·yes:·()·=>·void; - 46 45 │ private no: () => void; - 47 46 │ private dontKnow: () => void; - - -``` - -``` -invalid_dynamic_access.ts:46:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 44 │ export class SampleYesNoString { - 45 │ private yes: () => void; - > 46 │ private no: () => void; - │ ^^ - 47 │ private dontKnow: () => void; - 48 │ - - i Unsafe fix: Remove unused declaration. - - 44 44 │ export class SampleYesNoString { - 45 45 │ private yes: () => void; - 46 │ - → private·no:·()·=>·void; - 47 46 │ private dontKnow: () => void; - 48 47 │ - - -``` - -``` -invalid_dynamic_access.ts:47:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 45 │ private yes: () => void; - 46 │ private no: () => void; - > 47 │ private dontKnow: () => void; - │ ^^^^^^^^ - 48 │ - 49 │ on(action: string): void { - - i Unsafe fix: Remove unused declaration. - - 45 45 │ private yes: () => void; - 46 46 │ private no: () => void; - 47 │ - → private·dontKnow:·()·=>·void; - 48 47 │ - 49 48 │ on(action: string): void { - - -``` - -``` -invalid_dynamic_access.ts:55:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 54 │ export class SampleYesNoAny { - > 55 │ private yes: () => void; - │ ^^^ - 56 │ private no: () => void; - 57 │ private dontKnow: () => void; - - i Unsafe fix: Remove unused declaration. - - 53 53 │ - 54 54 │ export class SampleYesNoAny { - 55 │ - → private·yes:·()·=>·void; - 56 55 │ private no: () => void; - 57 56 │ private dontKnow: () => void; - - -``` - -``` -invalid_dynamic_access.ts:56:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 54 │ export class SampleYesNoAny { - 55 │ private yes: () => void; - > 56 │ private no: () => void; - │ ^^ - 57 │ private dontKnow: () => void; - 58 │ - - i Unsafe fix: Remove unused declaration. - - 54 54 │ export class SampleYesNoAny { - 55 55 │ private yes: () => void; - 56 │ - → private·no:·()·=>·void; - 57 56 │ private dontKnow: () => void; - 58 57 │ - - -``` - -``` -invalid_dynamic_access.ts:57:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 55 │ private yes: () => void; - 56 │ private no: () => void; - > 57 │ private dontKnow: () => void; - │ ^^^^^^^^ - 58 │ - 59 │ on(action: any): void { - - i Unsafe fix: Remove unused declaration. - - 55 55 │ private yes: () => void; - 56 56 │ private no: () => void; - 57 │ - → private·dontKnow:·()·=>·void; - 58 57 │ - 59 58 │ on(action: any): void { - - -``` - -``` -invalid_dynamic_access.ts:65:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 64 │ export class SampleYesNoUnknown { - > 65 │ private yes: () => void; - │ ^^^ - 66 │ private no: () => void; - 67 │ private dontKnow: () => void; - - i Unsafe fix: Remove unused declaration. - - 63 63 │ - 64 64 │ export class SampleYesNoUnknown { - 65 │ - → private·yes:·()·=>·void; - 66 65 │ private no: () => void; - 67 66 │ private dontKnow: () => void; - - -``` - -``` -invalid_dynamic_access.ts:66:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 64 │ export class SampleYesNoUnknown { - 65 │ private yes: () => void; - > 66 │ private no: () => void; - │ ^^ - 67 │ private dontKnow: () => void; - 68 │ - - i Unsafe fix: Remove unused declaration. - - 64 64 │ export class SampleYesNoUnknown { - 65 65 │ private yes: () => void; - 66 │ - → private·no:·()·=>·void; - 67 66 │ private dontKnow: () => void; - 68 67 │ - - -``` - -``` -invalid_dynamic_access.ts:67:10 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━ - - ! This private class member is defined but never used. - - 65 │ private yes: () => void; - 66 │ private no: () => void; - > 67 │ private dontKnow: () => void; - │ ^^^^^^^^ - 68 │ - 69 │ on(action: unknown): void { - - i Unsafe fix: Remove unused declaration. - - 65 65 │ private yes: () => void; - 66 66 │ private no: () => void; - 67 │ - → private·dontKnow:·()·=>·void; - 68 67 │ - 69 68 │ on(action: unknown): void { + 5 4 │ constructor() { ``` 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 2cfca0a128bb..3bbebf4b0578 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,9 +1,5 @@ class TSDoubleUnusedPrivateConstructor { - constructor( - usedProperty = 3, - private unusedProperty: number, - private anotherUnusedProperty = 4 - ) { + constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { // This constructor has two unused private properties } @@ -11,7 +7,6 @@ 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 f068a487f041..ff3616cfd334 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,16 +1,11 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 152 expression: invalid_issue_7101.ts --- # Input ```ts class TSDoubleUnusedPrivateConstructor { - constructor( - usedProperty = 3, - private unusedProperty: number, - private anotherUnusedProperty = 4 - ) { + constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { // This constructor has two unused private properties } @@ -18,78 +13,70 @@ 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:4:11 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ +invalid_issue_7101.ts:2:22 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 2 │ constructor( - 3 │ usedProperty = 3, - > 4 │ private unusedProperty: number, - │ ^^^^^^^^^^^^^^ - 5 │ private anotherUnusedProperty = 4 - 6 │ ) { + 1 │ class TSDoubleUnusedPrivateConstructor { + > 2 │ constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { + │ ^^^^^^^^^^^^^^^ + 3 │ // This constructor has two unused private properties + 4 │ i Unsafe fix: Remove private modifier 1 1 │ class TSDoubleUnusedPrivateConstructor { - 2 2 │ constructor( - 3 │ - → → usedProperty·=·3, - 4 │ - → → private·unusedProperty:·number, - 3 │ + → → usedProperty·=·3,_unusedProperty:·number, - 5 4 │ private anotherUnusedProperty = 4 - 6 5 │ ) { + 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 │ ``` ``` -invalid_issue_7101.ts:5:11 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ +invalid_issue_7101.ts:2:50 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ ! This private class member is defined but never used. - 3 │ usedProperty = 3, - 4 │ private unusedProperty: number, - > 5 │ private anotherUnusedProperty = 4 - │ ^^^^^^^^^^^^^^^^^^^^^ - 6 │ ) { - 7 │ // This constructor has two unused private properties + 1 │ class TSDoubleUnusedPrivateConstructor { + > 2 │ constructor(private unusedProperty = 3, private anotherUnusedProperty = 4) { + │ ^^^^^^^^^^^^^^^^^^^^^^ + 3 │ // This constructor has two unused private properties + 4 │ i Unsafe fix: Remove private modifier - 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 + 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 │ ``` ``` -invalid_issue_7101.ts:13:23 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━ +invalid_issue_7101.ts:9:23 lint/correctness/noUnusedPrivateClassMembers FIXABLE ━━━━━━━━━━━━━━━━━━ ! This parameter is never used outside of the constructor. - 12 │ class TSPartiallyUsedPrivateConstructor { - > 13 │ constructor(private param: number) { + 8 │ class TSPartiallyUsedPrivateConstructor { + > 9 │ constructor(private param: number) { │ ^^^^^ - 14 │ // this is not read or write as far as class members are concerned. - 15 │ foo(param) + 10 │ foo(param) + 11 │ } i Unsafe fix: Remove private modifier - 13 │ ··constructor(private·param:·number)·{ - │ -------- + 9 │ ··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 74c8cc3fe119..871a2b04e716 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js @@ -19,6 +19,15 @@ class UsedMember { } +class UsedMember { + get #usedAccessor() {} + set #usedAccessor(value) {} + + method() { + this.#usedAccessor = 42; + } +} + class UsedMember { publicMember = 42; } @@ -122,6 +131,14 @@ class UsedMember { } } +class UsedMember { + set #accessorUsedInMemberAccess(value) {} + + method(a) { + [this.#accessorUsedInMemberAccess] = a; + } +} + class UsedMember { get #accessorWithGetterFirst() { return something(); @@ -134,6 +151,16 @@ class UsedMember { } } +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + } + } +} + class Foo { #usedMethod() { return 42; @@ -143,6 +170,16 @@ 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 79ae10755bdf..539e488c1dfc 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,6 +1,5 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs -assertion_line: 152 expression: valid.js --- # Input @@ -26,6 +25,15 @@ class UsedMember { } +class UsedMember { + get #usedAccessor() {} + set #usedAccessor(value) {} + + method() { + this.#usedAccessor = 42; + } +} + class UsedMember { publicMember = 42; } @@ -129,6 +137,14 @@ class UsedMember { } } +class UsedMember { + set #accessorUsedInMemberAccess(value) {} + + method(a) { + [this.#accessorUsedInMemberAccess] = a; + } +} + class UsedMember { get #accessorWithGetterFirst() { return something(); @@ -141,6 +157,16 @@ class UsedMember { } } +class UsedMember { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + } + } +} + class Foo { #usedMethod() { return 42; @@ -150,6 +176,16 @@ 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 deleted file mode 100644 index bb6e63152922..000000000000 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js +++ /dev/null @@ -1,9 +0,0 @@ - -/* 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 deleted file mode 100644 index 22d40de03a3b..000000000000 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_aligned_with_semantic_class.js.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -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; - } -} - -``` diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts index 39e120b0707a..3b9e0a19f166 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts @@ -1,6 +1,6 @@ /* should not generate diagnostics */ -export class SampleAddRemove { +export class Sample { private add: () => void; constructor(private remove: () => void) { @@ -12,15 +12,3 @@ export class SampleAddRemove { this[action](); } } - -type YesNo = "yes" | "no"; - -export class SampleYesNo { - private yes: () => void; - private no: () => void; - - on(action: YesNo): void { - this[action](); - } -} - diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts.snap index 345263bf17e3..d21c29a9fef5 100644 --- a/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts.snap +++ b/crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid_dynamic_access.ts.snap @@ -6,7 +6,7 @@ expression: valid_dynamic_access.ts ```ts /* should not generate diagnostics */ -export class SampleAddRemove { +export class Sample { private add: () => void; constructor(private remove: () => void) { @@ -19,16 +19,4 @@ export class SampleAddRemove { } } -type YesNo = "yes" | "no"; - -export class SampleYesNo { - private yes: () => void; - private no: () => void; - - on(action: YesNo): void { - this[action](); - } -} - - ``` From 1cbd0e416b18174f6c6378ea3d13ea212e875048 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Fri, 21 Nov 2025 08:10:13 +0000 Subject: [PATCH 2/2] changest --- .changeset/stale-jokes-turn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-jokes-turn.md diff --git a/.changeset/stale-jokes-turn.md b/.changeset/stale-jokes-turn.md new file mode 100644 index 000000000000..60ffa76d538a --- /dev/null +++ b/.changeset/stale-jokes-turn.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#8138](https://github.com/biomejs/biome/issues/8138) by reverting an internal refactor that caused a regression to the rule `noUnusedPrivateClassMembers`.