Skip to content

Conversation

vladimir-ivanov
Copy link
Contributor

Summary

noUnusedPrivateClassMembers now treats private members in compound assignments (+=, -=, ??=, etc.) as used,
while plain assignments (=) do not count as usage.

Example that is now correctly flagged:

class App {
  #persistenceRequest: Promise<boolean> | undefined;
  saveData() {
    this.#persistenceRequest +=  2;
  }
}

closes #7192

Copy link

changeset-bot bot commented Sep 14, 2025

🦋 Changeset detected

Latest commit: 4a50b81

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions github-actions bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Sep 14, 2025
@vladimir-ivanov vladimir-ivanov changed the title feat(biome-js-analyze): adjusts members update assignments to be marked as valid feat(biome-js-analyze): adjusts members update assignments to be marked as valid in noUnusedPrivateClassMembers Sep 14, 2025
Copy link

codspeed-hq bot commented Sep 14, 2025

CodSpeed Performance Report

Merging #7500 will not alter performance

Comparing vladimir-ivanov:feat/no_unused_private_class_members_js_assignments_updates_mark_as_valid (4a50b81) with main (002cded)

Summary

✅ 133 untouched

@Conaclos
Copy link
Member

I would like to take some time to understand why we want this. This looks strange to me. I see update assignments as atomic operations. To me reporting them as unused is fine. Let wait some more discussions in #7192

@vladimir-ivanov vladimir-ivanov marked this pull request as ready for review September 15, 2025 06:25
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Walkthrough

The PR updates the noUnusedPrivateClassMembers lint to treat compound assignments as valid usage of private members. It imports JsAssignmentOperator and expands is_in_update_expression to detect non-plain assignments (e.g., +=, -=, **=, &&=, ||=, ??=) via JsAssignmentExpression. The traversal logic splits handling: update expressions drop the member from the unused set; write-only plain assignments keep it. Tests are adjusted by removing previously invalid self-update cases and adding comprehensive valid cases covering all compound operators. A patch-level changeset entry documents the fix linked to issue #7192.

Possibly related PRs

Suggested reviewers

  • ematipico

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately describes the primary change: treating update/compound assignments to private members as valid in the noUnusedPrivateClassMembers rule, and it maps directly to the changeset and tests; it is slightly wordy but clear and on-topic.
Linked Issues Check ✅ Passed The code changes implement detection of non-plain assignment operators (the operator check in is_in_update_expression) and adjust traversal logic so compound/update assignments (including ??=) mark private members as used; tests were added for many compound operators and the changelog entry and test adjustments align with closing issue #7192. Reviewer discussion about the policy choice remains, but the PR meets the linked issue's technical objective of eliminating the false positive for ??=.
Out of Scope Changes Check ✅ Passed All changed files are the lint rule, related tests, and a changelog entry, which directly relate to the linked issue; removing the old invalid self-update tests and adding many valid operator tests is consistent with the new behaviour and no unrelated code appears modified.
Description Check ✅ Passed The PR description is on-topic and correctly states the behavioural change (compound assignments count as usage while plain = does not), includes a clear example, and references the related issue (#7192), so it satisfies this lenient check.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Comment @coderabbitai help to get the list of available commands and usage tips.

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: 1

🧹 Nitpick comments (2)
.changeset/no_unused_private_class_members_bug.md (1)

5-18: Tighten wording, punctuation, and fix the example to match the reported bug (use ??= with a Promise).

  • End sentences with a period.
  • Replace “correctly flagged” (ambiguous) with “no diagnostic”.
  • Use the original false‑positive shape from #7192 with ??= and a Promise.

Apply this diff:

-Fixed [#7192](https://github.com/biomejs/biome/issues/7192):
-`noUnusedPrivateClassMembers` now treats private members in compound assignments (+=, -=, ??=, etc.) as used,
-while plain assignments (=) do not count as usage.
+Fixed [#7192](https://github.com/biomejs/biome/issues/7192): `noUnusedPrivateClassMembers` now treats private members in compound assignments (+=, -=, **=, %=, <<=, >>=, >>>=, &=, |=, ^=, &&=, ||=, ??=) as used. Plain assignments (=) do not count as usage.
 
-Example that is now correctly flagged:
+Example (no diagnostic):
 
 ```typescript
 class App {
-  #persistenceRequest: Promise<boolean> | undefined;
+  #persistenceRequest?: Promise<boolean>;
   saveData() {
-    this.#persistenceRequest +=  2;
+    this.#persistenceRequest ??= navigator.storage.persist();
   }
 }

</blockquote></details>
<details>
<summary>crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (1)</summary><blockquote>

`430-443`: **Broaden detection and tidy comments.**

- Minor: The comment “grand_parent can also be js expression statement” is confusing; the parent of the assignment/update is the expression statement.
- Consider also counting compound assignments as “used” even when not a direct expression statement (e.g. inside `if (this.#x &&= y) {}`); the general path will mark them used, but an explicit test would guard against future regressions.


Proposed small tidy:

```diff
-    // grand_parent can also be js expression statement
+    // Treat standalone update/compound assignments as usage (statement form only here).

And please add tests that cover:

  • return (this.#x += 1);
  • if (this.#x ??= getVal()) {}

This ensures both statement and expression contexts remain green.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 002cded and 4a50b81.

⛔ Files ignored due to path filters (3)
  • 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 **
📒 Files selected for processing (5)
  • .changeset/no_unused_private_class_members_bug.md (1 hunks)
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (3 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js (0 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts (0 hunks)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js (1 hunks)
💤 Files with no reviewable changes (2)
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.js
  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/invalid.ts
🧰 Additional context used
📓 Path-based instructions (5)
.changeset/*.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

.changeset/*.md: In changeset files, only use #### or ##### headers; avoid other header levels
Changeset descriptions should use past tense for what you did (e.g., "Added...")
Describe current Biome behavior in present tense within changesets (e.g., "Biome now supports...")
For bug fixes in changesets, start with a link to the issue (e.g., "Fixed #1234: ...")
When referencing rules or assists in changesets, include links to their documentation pages
Include a minimal code block in the changeset when applicable to demonstrate the change
End every sentence in the changeset description with a period

Files:

  • .changeset/no_unused_private_class_members_bug.md
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/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/correctness/noUnusedPrivateClassMembers/valid.js
  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
**/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.js
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format all Rust source files before committing (just f)

Files:

  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
🧠 Learnings (1)
📚 Learning: 2025-09-10T08:05:22.867Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-09-10T08:05:22.867Z
Learning: Applies to crates/biome_analyze/crates/biome_js_analyze/lib/src/{lint,assist}/**/*.rs : When banning globals (e.g., `noConsoleLog`), check the semantic model to avoid false positives from locally shadowed bindings

Applied to files:

  • crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (2)
crates/biome_rowan/src/syntax/node.rs (1)
  • grand_parent (254-256)
crates/biome_rowan/src/ast/mod.rs (6)
  • cast (135-137)
  • cast (381-381)
  • cast (385-387)
  • cast (824-829)
  • cast (873-878)
  • cast (1319-1324)
🔇 Additional comments (3)
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (2)

14-16: Importing JsAssignmentOperator is spot on.

Required for distinguishing plain vs compound assignments.


271-276: Early‑return on update expressions correctly treats them as usage.

This cleanly separates update vs write‑only cases and resolves the false positive for compound ops.

Mind adding one spec that exercises compound assignment used as an expression (e.g. return (this.#x ||= 1);) to lock this in?

.changeset/no_unused_private_class_members_bug.md (1)

5-7: Add Biome docs link for noUnusedPrivateClassMembers

Confirmed docs URL: https://v1.biomejs.dev/linter/rules/no-unused-private-class-members/ — add it next to the rule name in .changeset/no_unused_private_class_members_bug.md (lines 5–7).

Comment on lines +207 to +366
class AppSelfAdd {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest += this.#persistenceRequest;
}
}

class AppSelfSubtract {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest -= this.#persistenceRequest;
}
}

class AppSelfMultiply {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest *= this.#persistenceRequest;
}
}

class AppSelfDivide {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest /= this.#persistenceRequest;
}
}

class AppSelfExponent {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest **= this.#persistenceRequest;
}
}

class AppSelfAnd {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &&= this.#persistenceRequest;
}
}

class AppSelfOr {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ||= this.#persistenceRequest;
}
}

class AppSelfNullish {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ??= this.#persistenceRequest;
}
}

class AppAddAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest += 1;
}
}

class AppSubtractAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest -= 1;
}
}

class AppMultiplyAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest *= 2;
}
}

class AppDivideAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest /= 2;
}
}

class AppExponentAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest **= 2;
}
}

class AppModuloAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest %= 2;
}
}

class AppAndAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &= 1;
}
}

class AppOrAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest |= 1;
}
}

class AppXorAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ^= 1;
}
}

class AppLeftShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest <<= 1;
}
}

class AppRightShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest >>= 1;
}
}

class AppUnsignedRightShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest >>>= 1;
}
}

class AppAndLogicalAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &&= 1;
}
}

class AppOrLogicalAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ||= 1;
}
}

class AppNullishAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ??= 1;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add expression‑context cases for compound assignments.

Great coverage of statement forms. Let’s add a couple of expression forms to prevent regressions.

Apply this diff near the end of the file:

 class AppNullishAssignment {
 	#persistenceRequest = 0;
 	saveData() {
 		this.#persistenceRequest ??= 1;
 	}
 }
 
+class AppOrLogicalAssignmentInIf {
+	#persistenceRequest = 0;
+	saveData() {
+		if (this.#persistenceRequest ||= 1) {
+			return true;
+		}
+	}
+}
+
+class AppNullishAssignmentInReturn {
+	#persistenceRequest = 0;
+	saveData() {
+		return (this.#persistenceRequest ??= 1);
+	}
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class AppSelfAdd {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest += this.#persistenceRequest;
}
}
class AppSelfSubtract {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest -= this.#persistenceRequest;
}
}
class AppSelfMultiply {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest *= this.#persistenceRequest;
}
}
class AppSelfDivide {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest /= this.#persistenceRequest;
}
}
class AppSelfExponent {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest **= this.#persistenceRequest;
}
}
class AppSelfAnd {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &&= this.#persistenceRequest;
}
}
class AppSelfOr {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ||= this.#persistenceRequest;
}
}
class AppSelfNullish {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ??= this.#persistenceRequest;
}
}
class AppAddAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest += 1;
}
}
class AppSubtractAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest -= 1;
}
}
class AppMultiplyAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest *= 2;
}
}
class AppDivideAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest /= 2;
}
}
class AppExponentAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest **= 2;
}
}
class AppModuloAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest %= 2;
}
}
class AppAndAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &= 1;
}
}
class AppOrAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest |= 1;
}
}
class AppXorAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ^= 1;
}
}
class AppLeftShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest <<= 1;
}
}
class AppRightShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest >>= 1;
}
}
class AppUnsignedRightShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest >>>= 1;
}
}
class AppAndLogicalAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &&= 1;
}
}
class AppOrLogicalAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ||= 1;
}
}
class AppNullishAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ??= 1;
}
}
class AppSelfAdd {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest += this.#persistenceRequest;
}
}
class AppSelfSubtract {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest -= this.#persistenceRequest;
}
}
class AppSelfMultiply {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest *= this.#persistenceRequest;
}
}
class AppSelfDivide {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest /= this.#persistenceRequest;
}
}
class AppSelfExponent {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest **= this.#persistenceRequest;
}
}
class AppSelfAnd {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &&= this.#persistenceRequest;
}
}
class AppSelfOr {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ||= this.#persistenceRequest;
}
}
class AppSelfNullish {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ??= this.#persistenceRequest;
}
}
class AppAddAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest += 1;
}
}
class AppSubtractAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest -= 1;
}
}
class AppMultiplyAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest *= 2;
}
}
class AppDivideAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest /= 2;
}
}
class AppExponentAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest **= 2;
}
}
class AppModuloAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest %= 2;
}
}
class AppAndAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &= 1;
}
}
class AppOrAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest |= 1;
}
}
class AppXorAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ^= 1;
}
}
class AppLeftShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest <<= 1;
}
}
class AppRightShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest >>= 1;
}
}
class AppUnsignedRightShiftAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest >>>= 1;
}
}
class AppAndLogicalAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest &&= 1;
}
}
class AppOrLogicalAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ||= 1;
}
}
class AppNullishAssignment {
#persistenceRequest = 0;
saveData() {
this.#persistenceRequest ??= 1;
}
}
class AppOrLogicalAssignmentInIf {
#persistenceRequest = 0;
saveData() {
if (this.#persistenceRequest ||= 1) {
return true;
}
}
}
class AppNullishAssignmentInReturn {
#persistenceRequest = 0;
saveData() {
return (this.#persistenceRequest ??= 1);
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Linter Area: linter L-JavaScript Language: JavaScript and super languages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

🐛 False positive for lint/correctness/noUnusedPrivateClassMembers when using ??=
2 participants