Skip to content

feat(biome-js-analyzer): adds class member analyzer to extract mutated properties in a class#7015

Merged
vladimir-ivanov merged 9 commits intobiomejs:mainfrom
vladimir-ivanov:feat/classMemberAnalyzer
Aug 24, 2025
Merged

feat(biome-js-analyzer): adds class member analyzer to extract mutated properties in a class#7015
vladimir-ivanov merged 9 commits intobiomejs:mainfrom
vladimir-ivanov:feat/classMemberAnalyzer

Conversation

@vladimir-ivanov
Copy link
Contributor

@vladimir-ivanov vladimir-ivanov commented Jul 26, 2025

Summary

Adds class member analyser that returns all properties mutations in a class member.
Exposes a single fn:

pub fn class_member_references(list: &JsClassMemberList) -> ClassMemberReferences;

pub struct ClassMemberReferences {
    pub reads: HashSet<ClassMemberReference>,
    pub writes: HashSet<ClassMemberReference>,
}

which returns all reads and writes found in any class member (constructors, property and method members).
Different rules apply for extracting writes and reads from a constructor (consistent with eslint)
The code internally collects the this keyword and its aliases if any (e.g. const thisAlias = this;) with the scope/ range next to it.
It is useful to avoid matching false positives of this.something in nested constructs.

@changeset-bot
Copy link

changeset-bot bot commented Jul 26, 2025

⚠️ No Changeset found

Latest commit: 9608e47

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions bot added A-Linter Area: linter A-Parser Area: parser L-JavaScript Language: JavaScript and super languages labels Jul 26, 2025
@vladimir-ivanov vladimir-ivanov force-pushed the feat/classMemberAnalyzer branch from 0c7fb7d to 821bd3c Compare July 26, 2025 21:08
@codspeed-hq
Copy link

codspeed-hq bot commented Jul 26, 2025

CodSpeed Performance Report

Merging #7015 will not alter performance

Comparing vladimir-ivanov:feat/classMemberAnalyzer (09c73a8) with main (2af2380)

Summary

✅ 133 untouched benchmarks

@dyc3
Copy link
Contributor

dyc3 commented Jul 27, 2025

I don't really understand what you're trying to get at. I don't have very much context into the problem you're trying to solve.

My comment was asking whether the whole file should go into js_analyze, because at a glance, this does analysis things.

@github-actions github-actions bot removed the A-Parser Area: parser label Jul 27, 2025
@vladimir-ivanov vladimir-ivanov requested a review from dyc3 July 27, 2025 09:55
@vladimir-ivanov vladimir-ivanov changed the title feat(biome-js-syntax): adds class member analyzer to extract mutated properties in a class feat(biome-js-analyzer): adds class member analyzer to extract mutated properties in a class Jul 27, 2025
Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't have time to look at all the code. This PR needs a bit more love, and here a few suggestions to make it better:

  • The PR description is very poor, compared to what the code does. A PR description is meant to present the code and all its business logic, so the reviewers know what they are about to review. As for myself, I really don't know what I'm reviewing and how the code is supposed to work
  • The code is over-engineered. We used traits, callbacks with trait bounds, and the visitor pattern isn't implemented correctly. I think all of this isn't needed, and we can use plain functions
  • Inconsistent documentation format. We should stick to one, and put a bit more care into what we document. Some comments lack context, some comments show examples differently.

@vladimir-ivanov
Copy link
Contributor Author

@ematipico thanks for the feedback.
this pr was more of a draft, probably should have marked it as one. Left it deliberately incomplete/ broken tests etc.

Does the idea make sense to proceed or not at all please?

It is the main question here.
Logic is same as found currently in useReadonlyClassProperties:

  1. extract all aliases of this and their scopes in a collection
  2. find all writes (or mutations) of class property members and constructor properties
  3. (new in this pr) find all reads of class property and function members

Step 1 is largely needed to resolve a this.scope to aliases e.g. const self = this; and later self.scope is checked against its valid scope.

when 3. is ready we can reuse in no_unused_private_class_members.

Else I can clean up the pr if there is any value of creating this class analyser?

@ematipico
Copy link
Member

You might want to consider expanding the semantic model at this point, or creating a new one just for classes. I know I said that the current semantic model is more about scopes and such, but after reviewing the code, I think that's the right approach.

@vladimir-ivanov
Copy link
Contributor Author

You might want to consider expanding the semantic model at this point, or creating a new one just for classes. I know I said that the current semantic model is more about scopes and such, but after reviewing the code, I think that's the right approach.

thanks, my feeling is do it incremental - one for classes at first. And then potentially merge with the existing.
Saying so because I am still quite far from being comfortable with rust as you can see - doing wild experiments to see what works and what not.

Can do a few things on this pr here so it keeps similar shape as currently (will be much easier to turn it into a semantic model alike.

Traits - I tried (and failed) to use them instead of namespaces - as grouping smaller pieces of code together, and only expose the public fns out. So it becomes much simpler to refactor later when I learn more.
Can get rid of them and maybe use empty structs instead? if that is accpetable?

e.g. struct ThisAliases {}

and attach methods to it.

Can also clean up the write part of the visitor, now is quite hard to maintain. Instead will use the same walk interface as in read? (downside is it will be slightly slower as it will traverse a bit more nodes -> node.syntax() but will be much simpler to maintain.

And of course I do not mind sctrapping the whole thing if result is a disaster.

Or redo it to a better one..

As it stands I just wanted to make things work. And fix all bugs with no_unused_private_properties.

Step 2 is to make things more pretty :-)
Let's see how it goes.

Appreciate your feedback so far.

And please do feel free always to say if it wont work. My goal is not to merge this if the outcome is not easily readable and maintainable code. Or if the implementation is fundamentally wrong.

@ematipico
Copy link
Member

Can get rid of them and maybe use empty structs instead? if that is accpetable?

e.g. struct ThisAliases {}

Yeah that's a valid use-case, however empty struts are usually created because they eventually implement traits.

However, if you design to create a new semantic model for classes, I don't see a compelling argument for using the previous approach. The model will need to save data because consumers will need to query them. For example if you identify a "write", you might want to save the node where it happens (or the text range). Just food for thought

@vladimir-ivanov vladimir-ivanov marked this pull request as draft August 2, 2025 10:46
@vladimir-ivanov
Copy link
Contributor Author

@ematipico I have addressed most of the issues, added comments etc.

now is ok to be looked at.
Personally think that it is much cleaner now. Fixes all the open bugs around no_unused_class_members and reuses logic/ aligns more closely to other biome code?

I would say maybe we make this mergeable.
Then if we still want to do standalone service alike Semantic or integrate it within, I do not mind, just need a very clear guideance on what exactly to do.

@vladimir-ivanov vladimir-ivanov marked this pull request as ready for review August 2, 2025 14:49
@vladimir-ivanov vladimir-ivanov force-pushed the feat/classMemberAnalyzer branch from 4813afa to b0843a2 Compare August 2, 2025 14:54
@github-actions github-actions bot added A-Core Area: core A-Project Area: project A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-CSS Language: CSS L-JSON Language: JSON and super languages A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages labels Aug 2, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

Note

Generated docstrings for this pull request at #7308

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs (1)

11-15: Imports look clean now — the E0432 saga is over.

The stray identifiers reported earlier are gone; the import list matches actual exports.

crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (2)

236-262: Return TextRange with a robust fallback.

Switch from Option<TextRange> to TextRange, preferring the name’s span and falling back to the node’s range. Mirrors the earlier bot suggestion.

Apply:

-    fn property_range(&self) -> Option<TextRange> {
+    fn property_range(&self) -> TextRange {
         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(),
-                ),
-            },
+            Self::AnyJsClassMember(member) => {
+                let name_range = match member {
+                    AnyJsClassMember::JsGetterClassMember(m) => m.name().ok().map(|n| n.range()),
+                    AnyJsClassMember::JsMethodClassMember(m) => m.name().ok().map(|n| n.range()),
+                    AnyJsClassMember::JsPropertyClassMember(m) => m.name().ok().map(|n| n.range()),
+                    AnyJsClassMember::JsSetterClassMember(m) => m.name().ok().map(|n| n.range()),
+                    _ => None,
+                };
+                name_range.unwrap_or_else(|| member.syntax().text_range())
+            }
+            Self::TsPropertyParameter(ts_property) => {
+                let name_range = ts_property
+                    .formal_parameter()
+                    .ok()
+                    .and_then(|param| match param {
+                        AnyJsFormalParameter::JsFormalParameter(p) => p
+                            .binding()
+                            .ok()?
+                            .as_any_js_binding()?
+                            .as_js_identifier_binding()?
+                            .name_token()
+                            .ok()
+                            .map(|t| t.text_range()),
+                        _ => None,
+                    });
+                name_range.unwrap_or_else(|| ts_property.syntax().text_range())
+            }
         }
     }

123-131: Compile blocker: RuleDiagnostic::new expects TextRange, not Option<TextRange>.

state.property_range() returns Option<TextRange>, so this won’t compile. Return a TextRange with a sensible fallback (member node span) and keep the call site unchanged.

Apply:

-    fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
-        Some(RuleDiagnostic::new(
-            rule_category!(),
-            state.property_range(),
+    fn diagnostic(_: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        Some(RuleDiagnostic::new(
+            rule_category!(),
+            state.property_range(),
             markup! {
                 "This private class member is defined but never used."
             },
         ))
     }

and update property_range() below (see next comment).

🧹 Nitpick comments (7)
crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs (4)

149-172: Cut allocations and scans in the hot path.

We clone the writes set and scan it per member; we also clone items we already own. Use iterators and precompute a lookup set.

Apply:

-            .filter_map(|prop_or_param| {
-                if writes
-                    .clone()
-                    .into_iter()
-                    .any(|ClassMemberReference { name, .. }| {
-                        if let Some(TextAndRange { text, .. }) =
-                            extract_property_or_param_range_and_text(&prop_or_param.clone())
-                        {
-                            return name.eq(&text);
-                        }
-
-                        false
-                    })
-                {
-                    None
-                } else {
-                    Some(prop_or_param.clone())
-                }
-            })
+            .filter_map(|prop_or_param| {
+                let is_written = writes.iter().any(|ClassMemberReference { name, .. }| {
+                    extract_property_or_param_range_and_text(&prop_or_param)
+                        .is_some_and(|TextAndRange { text, .. }| name.eq(&text))
+                });
+                if is_written { None } else { Some(prop_or_param) }
+            })

Optionally, dedupe against constructor params without repeated clones:

-        constructor_params
-            .clone()
-            .into_iter()
-            .chain(
-                non_readonly_class_property_members.filter(|class_property_member| {
-                    !constructor_params.clone().into_iter().any(|node| {
-                        node.to_trimmed_text() == class_property_member.to_trimmed_text()
-                    })
-                }),
-            )
+        let ctor_names: std::collections::HashSet<_> = constructor_params
+            .iter()
+            .filter_map(|p| extract_property_or_param_range_and_text(p).map(|t| t.text))
+            .collect();
+
+        constructor_params
+            .into_iter()
+            .chain(
+                non_readonly_class_property_members.filter(|m| {
+                    extract_property_or_param_range_and_text(m)
+                        .is_some_and(|t| !ctor_names.contains(&t.text))
+                }),
+            )

196-208: Avoid re‑casting from syntax; pattern‑match the union directly.

node is already AnyPropertyMember. Casting the syntax back to AnyPropertyMember is redundant and brittle.

Apply:

-        if let Some(AnyPropertyMember::JsPropertyClassMember(member)) =
-            AnyPropertyMember::cast(original_node.clone())
+        if let AnyPropertyMember::JsPropertyClassMember(member) = node
         {
             …
-        } else if let Some(AnyPropertyMember::TsPropertyParameter(parameter)) =
-            AnyPropertyMember::cast(original_node.clone())
+        } else if let AnyPropertyMember::TsPropertyParameter(parameter) = node
         {
             …
         }

This also removes an extra clone of original_node.

Also applies to: 241-263


268-269: Wording: “modifier”, not “decorator”.

TS readonly is a modifier, not a decorator. Tiny thing, big peace of mind for readers.

Apply:

-            markup! { "Add "<Emphasis>"readonly"</Emphasis>" decorator." }.to_owned(),
+            markup! { "Add "<Emphasis>"readonly"</Emphasis>" modifier." }.to_owned(),

392-424: Simplify: match on the union instead of re‑casting.

Inside extract_property_or_param_range_and_text, you already have &AnyPropertyMember. The extra cast via into() is unnecessary.

Apply:

-fn extract_property_or_param_range_and_text(
-    property_or_param: &AnyPropertyMember,
-) -> Option<TextAndRange> {
-    if let Some(AnyPropertyMember::JsPropertyClassMember(member)) =
-        AnyPropertyMember::cast(property_or_param.clone().into())
-    {
+fn extract_property_or_param_range_and_text(
+    property_or_param: &AnyPropertyMember,
+) -> Option<TextAndRange> {
+    if let AnyPropertyMember::JsPropertyClassMember(member) = property_or_param {
         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())
-    {
+    if let AnyPropertyMember::TsPropertyParameter(parameter) = property_or_param {
         let name = parameter
             .formal_parameter()
             .ok()?
             .as_js_formal_parameter()?
             .binding()
             .ok()?;
 
         return Some(TextAndRange {
             text: name.to_trimmed_text(),
             range: name.range(),
         });
     }
 
     None
 }
crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts (1)

29-36: Two exported classes with the same name may confuse tooling.

Having two export class ToastService declarations in one module can trip bundlers or TS language services, even if our test harness doesn’t care.

Apply:

-export class ToastService {
+export class ToastService2 {
 	private _toastId = 0;

 	show(message: string, type: string, autoClose: boolean): void {
 		const id = this._toastId++;
 		this.activeToasts.push({ id, message, type, autoClose });
 	}
 }
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (2)

90-121: Overall flow looks good; one style nit.

get_constructor_params returns FxHashSet while get_all_declared_private_members collects into HashSet. It’s harmless, but we can use HashSet consistently unless perf dictates otherwise.

If you prefer FxHashSet, mirror it in get_all_declared_private_members; otherwise, switch get_constructor_params to HashSet.


264-307: Name matching is clean; consider normalising once.

You strip # per comparison. If hotspots show up, pre‑normalise the reads/writes names (or the private member names) to avoid repeated allocation/trim logic. Non‑blocking.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d2b4c06 and a895c75.

⛔ Files ignored due to path filters (4)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (6)
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (5 hunks)
  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs (13 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js (1 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts (1 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js (2 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/*.rs : For new JavaScript lint rules generated by `just new-js-lintrule`, implement the rule in the generated file under `biome_js_analyze/lib/src/lint/nursery/`
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/src/**/*.rs : Represent links between types using TypeReference (not Arc) to avoid cross-module retention and recursive structures; store type data in linear vectors
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/biome_module_graph/src/js_module_info/collector.rs : Thin (module-level) inference may resolve qualifiers to local declarations or globals (TypeReference::Resolved), mark imported bindings as TypeReference::Import, and set unresolved to TypeReference::Unknown; it must not look beyond the current module
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/*.rs : For new JavaScript lint rules generated by `just new-js-lintrule`, implement the rule in the generated file under `biome_js_analyze/lib/src/lint/nursery/`

Applied to files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
📚 Learning: 2025-08-11T11:50:12.090Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/biome_module_graph/src/js_module_info/collector.rs : Thin (module-level) inference may resolve qualifiers to local declarations or globals (TypeReference::Resolved), mark imported bindings as TypeReference::Import, and set unresolved to TypeReference::Unknown; it must not look beyond the current module

Applied to files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/tests/quick_test.rs : Use `biome_js_analyze/tests/quick_test.rs` for quick, ad-hoc testing; un-ignore the test and adjust the rule filter as needed

Applied to files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
📚 Learning: 2025-08-11T11:50:12.090Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/src/**/*.rs : Use TypeData::Unknown for unimplemented inference and TypeData::UnknownKeyword for the explicit TypeScript unknown keyword; treat them semantically the same but keep them distinct for measurement

Applied to files:

  • crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/**/lib/src/**/nursery/**/*.rs : If a rule provides a code action, add `fix_kind` in `declare_lint_rule!` and use `ctx.action_category(ctx.category(), ctx.group())` and `ctx.metadata().applicability()` when constructing actions

Applied to files:

  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_rule_options/lib/**/*.rs : Define per-rule options in the `biome_rule_options` crate under `lib/`, with serde- and schemars-compatible derives and `#[serde(rename_all = "camelCase", deny_unknown_fields, default)]`

Applied to files:

  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
🧬 Code graph analysis (2)
crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs (1)
crates/biome_js_analyze/src/class_member_references.rs (1)
  • class_member_references (39-95)
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (2)
crates/biome_js_analyze/src/class_member_references.rs (2)
  • class_member_references (39-95)
  • new (194-199)
crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs (3)
  • run (132-179)
  • ctx (133-133)
  • member (211-217)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: Documentation
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: Bench (biome_package)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: Bench (biome_html_formatter)
  • GitHub Check: Check Dependencies
  • GitHub Check: Bench (biome_json_parser)
  • GitHub Check: Bench (biome_json_analyze)
  • GitHub Check: Bench (biome_graphql_formatter)
  • GitHub Check: Bench (biome_html_parser)
  • GitHub Check: Bench (biome_json_formatter)
  • GitHub Check: Bench (biome_css_analyze)
  • GitHub Check: Bench (biome_graphql_parser)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_css_formatter)
  • GitHub Check: Bench (biome_css_parser)
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: Test Node.js API
  • GitHub Check: autofix
🔇 Additional comments (5)
crates/biome_js_analyze/src/lint/style/use_readonly_class_properties.rs (1)

141-178: Double‑check: are constructor writes excluded from “writes”?

The rule promises “readonly if never modified outside the constructor”, but the filter currently drops any member that appears in writes, regardless of origin. If class_member_references includes constructor writes, properties set only in the constructor will be incorrectly excluded from diagnostics. Please verify the semantics, or filter out constructor-origin writes here.

Would you like me to wire a small flag on ClassMemberReference (e.g., origin: MemberOrigin::{Constructor, NonConstructor}) and adjust the filter?

Example patch at call‑site (assuming writes_outside_ctor is available):

-        let ClassMemberReferences { writes, .. } = class_member_references(&members);
+        let ClassMemberReferences { writes_outside_ctor: writes, .. } =
+            class_member_references(&members);
crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.ts (1)

6-9: Nice coverage of this destructuring.

This exercises alias‑free reads and closes a common hole in member‑usage analysis.

crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js (2)

26-34: Good: inner class reads from a non‑alias parameter.

This guards against false positives where a is mistaken for this.


44-50: Write‑only assignment expression is correctly treated as “unused”.

return this.#val = 1 still doesn’t “use” the field’s value; it’s a pure write — perfect for catching regressions like #6994.

crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js (1)

187-193: Nice addition: post‑increment use case for ES private fields.

Covers the update‑expr path; complements the existing pre/post cases elsewhere.

@vladimir-ivanov vladimir-ivanov enabled auto-merge (squash) August 23, 2025 18:47
@vladimir-ivanov
Copy link
Contributor Author

vladimir-ivanov commented Aug 23, 2025

@ematipico could you please review one last time?
also feeling lost on how to address the failure of this test -> pull_diagnostics_of_syntax_rules, think messages are coming in non deterministic order. Changed the test, but not sure if it is the right thing to do. Please advise.

@github-actions github-actions bot added the A-LSP Area: language server protocol label Aug 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
crates/biome_lsp/src/server.tests.rs (3)

1006-1022: Accept numeric diagnostic codes and avoid relying on borrowed strs.

Some servers may emit numeric codes; converting to owned Strings makes the check robust and avoids lifetime gotchas. Also switch contains to an iterator predicate to match the new type.

Apply this diff:

-    // collect the diagnostic codes for easier comparison
-    let codes: Vec<_> = params.diagnostics.iter().filter_map(|diagnostic| {
-        if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code {
-            Some(code.as_str())
-        } else {
-            None
-        }
-    }).collect();
-
-    assert!(
-        codes.contains(&"syntax/correctness/noDuplicatePrivateClassMembers"),
-        "Expected duplicate private member diagnostic, got {codes:?}"
-    );
-    assert!(
-        codes.contains(&"lint/correctness/noUnusedVariables"),
-        "Expected unused variable diagnostic, got {codes:?}"
-    );
+    // Collect diagnostic codes as owned strings (support both string and numeric codes)
+    let codes: Vec<String> = params
+        .diagnostics
+        .iter()
+        .filter_map(|d| d.code.as_ref().map(|c| match c {
+            lsp::NumberOrString::String(s) => s.clone(),
+            lsp::NumberOrString::Number(n) => n.to_string(),
+        }))
+        .collect();
+
+    assert!(
+        codes.iter().any(|c| c == "syntax/correctness/noDuplicatePrivateClassMembers"),
+        "Expected duplicate private member diagnostic, got {codes:?}"
+    );
+    assert!(
+        codes.iter().any(|c| c == "lint/correctness/noUnusedVariables"),
+        "Expected unused variable diagnostic, got {codes:?}"
+    );

1024-1033: Make message checks case-insensitive and include context on failure.

Minor tweak to avoid casing brittleness and improve debug output. Also the comment says “Optionally”, but these are hard asserts.

Apply this diff:

-    // Optionally also check messages (ignoring positions/ranges to avoid brittleness)
-    let messages: Vec<_> = params.diagnostics.iter().map(|diagnostic| diagnostic.message.as_str()).collect();
-    assert!(
-        messages.iter().any(|message| message.contains("Duplicate private class member")),
-        "Expected duplicate member message"
-    );
-    assert!(
-        messages.iter().any(|message| message.contains("unused")),
-        "Expected unused variable message"
-    );
+    // Also check messages (ignoring positions/ranges to avoid brittleness)
+    let messages_lower: Vec<String> = params
+        .diagnostics
+        .iter()
+        .map(|d| d.message.to_lowercase())
+        .collect();
+    assert!(
+        messages_lower
+            .iter()
+            .any(|m| m.contains("duplicate private class member")),
+        "Expected duplicate member message. Got: {messages_lower:?}"
+    );
+    assert!(
+        messages_lower.iter().any(|m| m.contains("unused")),
+        "Expected unused variable message. Got: {messages_lower:?}"
+    );

1002-1014: Potential flake: assumes both diagnostics arrive in a single PublishDiagnostics.

If the server ever emits syntax and lint diagnostics in separate notifications, this will occasionally fail. Consider aggregating diagnostics for a short window before asserting.

Example (replace the single-use params.diagnostics below with all_diags):

// After receiving the first PublishDiagnostics as `params`:
let mut all_diags = params.diagnostics.clone();

// Drain additional PublishDiagnostics for a short window (e.g., 200ms idle).
loop {
    let next = tokio::select! {
        msg = receiver.next() => msg,
        _ = sleep(Duration::from_millis(200)) => None,
    };
    match next {
        Some(ServerNotification::PublishDiagnostics(next_params)) => {
            all_diags.extend(next_params.diagnostics);
        }
        _ => break,
    }
}

// Then build `codes`/`messages` from `all_diags.iter()` rather than `params.diagnostics.iter()`.

Would you like me to wire this into the test with minimal churn?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a895c75 and 81b7c60.

📒 Files selected for processing (1)
  • crates/biome_lsp/src/server.tests.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_lsp/src/server.tests.rs
crates/biome_service/../biome_lsp/src/server.tests.rs

📄 CodeRabbit inference engine (crates/biome_service/CONTRIBUTING.md)

Keep end-to-end LSP tests in biome_lsp’s server.tests.rs

Files:

  • crates/biome_lsp/src/server.tests.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_lsp/src/server.tests.rs
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/lint/nursery/*.rs : For new JavaScript lint rules generated by `just new-js-lintrule`, implement the rule in the generated file under `biome_js_analyze/lib/src/lint/nursery/`
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/src/**/*.rs : Represent links between types using TypeReference (not Arc) to avoid cross-module retention and recursive structures; store type data in linear vectors
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/biome_module_graph/src/js_module_info/collector.rs : Thin (module-level) inference may resolve qualifiers to local declarations or globals (TypeReference::Resolved), mark imported bindings as TypeReference::Import, and set unresolved to TypeReference::Unknown; it must not look beyond the current module
📚 Learning: 2025-08-11T11:53:15.299Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_service/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:53:15.299Z
Learning: Applies to crates/biome_service/../biome_lsp/src/server.tests.rs : Keep end-to-end LSP tests in biome_lsp’s server.tests.rs

Applied to files:

  • crates/biome_lsp/src/server.tests.rs
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/**/lib/src/**/nursery/**/*.rs : Use the local `rule_category!()` macro in diagnostics for the rule’s category, not string-parsed categories

Applied to files:

  • crates/biome_lsp/src/server.tests.rs
📚 Learning: 2025-08-11T11:46:05.836Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_diagnostics/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:46:05.836Z
Learning: Applies to crates/biome_diagnostics/**/*.rs : Types implementing Diagnostic must also implement Debug (e.g., use #[derive(Debug, Diagnostic)])

Applied to files:

  • crates/biome_lsp/src/server.tests.rs
📚 Learning: 2025-08-11T11:46:05.836Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_diagnostics/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:46:05.836Z
Learning: Applies to crates/biome_diagnostics/**/*.rs : Specify category and severity using #[diagnostic(...)] on the type or derive them from fields

Applied to files:

  • crates/biome_lsp/src/server.tests.rs
🧬 Code graph analysis (1)
crates/biome_lsp/src/server.tests.rs (2)
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (1)
  • diagnostic (123-131)
crates/biome_js_analyze/src/lint/correctness/no_unused_variables.rs (1)
  • diagnostic (306-348)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (25)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Documentation
  • GitHub Check: End-to-end tests
  • GitHub Check: Check Dependencies
  • GitHub Check: Test Node.js API
  • GitHub Check: Bench (biome_package)
  • GitHub Check: Bench (biome_html_formatter)
  • GitHub Check: Bench (biome_css_parser)
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: Bench (biome_json_formatter)
  • GitHub Check: Bench (biome_graphql_formatter)
  • GitHub Check: Bench (biome_json_parser)
  • GitHub Check: Bench (biome_css_analyze)
  • GitHub Check: Bench (biome_graphql_parser)
  • GitHub Check: Bench (biome_html_parser)
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_json_analyze)
  • GitHub Check: Bench (biome_css_formatter)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: autofix
🔇 Additional comments (2)
crates/biome_lsp/src/server.tests.rs (2)

1002-1004: Solid pattern-match; less brittle than full-struct equality.

Graceful destructuring of the notification improves resilience and keeps the failure mode explicit.


1035-1035: Nice touch: explicit close before shutdown.

Keeps the lifecycle tidy and mirrors other tests.

@ematipico
Copy link
Member

@vladimir-ivanov

also feeling lost on how to address the failure of this test -> pull_diagnostics_of_syntax_rules, think messages are coming in non deterministic order. Changed the test, but not sure if it is the right thing to do. Please advise.

To be honest, I'm feeling a bit lost about all the changes. The PR description says that it should address only one lint rule, however it seems the scope of the PR has changed. I also thought you were trying to address a couple of bugs of a nursery rule.

My advice is to take a step back, and keep the changes scoped to that single rule and do not span into others. I really don't understand why the scope changed, and maybe it shouldn't.

@vladimir-ivanov vladimir-ivanov force-pushed the feat/classMemberAnalyzer branch from 66143a9 to 9608e47 Compare August 24, 2025 04:24
@github-actions github-actions bot removed the A-LSP Area: language server protocol label Aug 24, 2025
@vladimir-ivanov
Copy link
Contributor Author

vladimir-ivanov commented Aug 24, 2025

@vladimir-ivanov

also feeling lost on how to address the failure of this test -> pull_diagnostics_of_syntax_rules, think messages are coming in non deterministic order. Changed the test, but not sure if it is the right thing to do. Please advise.

To be honest, I'm feeling a bit lost about all the changes. The PR description says that it should address only one lint rule, however it seems the scope of the PR has changed. I also thought you were trying to address a couple of bugs of a nursery rule.

My advice is to take a step back, and keep the changes scoped to that single rule and do not span into others. I really don't understand why the scope changed, and maybe it shouldn't.

@ematipico thanks for the comment.

Reduced the scope of the pr is now ONLY class member analyser.
Undid the no_unused_private_properties entirely from it.

it will fix many other bugs in useReadonlyClassProperties such as the recently opened one here:
#7310

let me know if we want to keep or scrap entirely the pr please.

Also do we want to move this into service bag at all as discussed above?

if so, do we add it to semantic model or we have separate semantic class model service?

thanks

@ematipico
Copy link
Member

let me know if we want to keep or scrap entirely the pr please.

There's no need, as long as the scope of the pr doesn't change ☺️ and stays the one you agreed. In fact, you might have noticed that using the new utility in another rule caused a regression, which means the new utility has some bug.

Also do we want to move this into service bag at all as discussed above?

Yes I think it should. Do you think it shouldn't?

if so, do we add it to semantic model or we have separate semantic class model service?

I think it will be another service, because this one works only for classes

@vladimir-ivanov vladimir-ivanov merged commit 28cc996 into biomejs:main Aug 24, 2025
35 of 36 checks passed
@vladimir-ivanov
Copy link
Contributor Author

vladimir-ivanov commented Aug 24, 2025

let me know if we want to keep or scrap entirely the pr please.

There's no need, as long as the scope of the pr doesn't change ☺️ and stays the one you agreed. In fact, you might have noticed that using the new utility in another rule caused a regression, which means the new utility has some bug.

Also do we want to move this into service bag at all as discussed above?

Yes I think it should. Do you think it shouldn't?

if so, do we add it to semantic model or we have separate semantic class model service?

I think it will be another service, because this one works only for classes

will give it a shot. thank you

RE: Yes I think it should. Do you think it shouldn't? - I do not assume anything anymore, too many folks have very different opinions, some can surprise me sometimes :-). So there is no harm to ask even obvious questions :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Linter Area: linter L-JavaScript Language: JavaScript and super languages L-JSON Language: JSON and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants