Skip to content

fix(linter): handle destructuring from this in noUnusedPrivateClassMembers#9047

Open
solssak wants to merge 1 commit intobiomejs:mainfrom
solssak:fix/no-unused-private-class-members-destructuring
Open

fix(linter): handle destructuring from this in noUnusedPrivateClassMembers#9047
solssak wants to merge 1 commit intobiomejs:mainfrom
solssak:fix/no-unused-private-class-members-destructuring

Conversation

@solssak
Copy link

@solssak solssak commented Feb 13, 2026

Summary

Fixed #9008.

The noUnusedPrivateClassMembers rule reported false positives when TypeScript private class members were accessed via object destructuring from this.

class MyClass {
  private myVar: string;

  constructor() {
    this.myVar = "Hello";
  }

  print() {
    const { myVar } = this; // was incorrectly flagged as unused
    console.log(myVar);
  }
}

Root Cause

traverse_members_usage only matched AnyJsName nodes (used in direct member access like this.myVar). In destructuring patterns like const { myVar } = this, the property name appears as a JsIdentifierBinding inside a JsObjectBindingPatternShorthandProperty, which is not an AnyJsName node and was therefore never recognized as usage.

Changes

  • Added get_destructured_names_from_this() to extract property names from object destructuring patterns where the initializer is this. Handles:
    • const { myVar } = this; (shorthand binding pattern)
    • const { myVar: renamed } = this; (renamed binding pattern)
    • ({ myVar } = this) (assignment destructuring pattern)
  • Added AnyMember::member_name() to extract the private member name as a String for comparison with destructured names.
  • Integrated destructuring detection into traverse_members_usage.

Test Plan

Added valid.ts test file covering all destructuring patterns:

  • DestructuringShorthandconst { myVar } = this;
  • DestructuringMultiple — multiple private members, some accessed via destructuring
  • DestructuringRenamedconst { myVar: renamed } = this;
  • DestructuringAssignmentPattern({ myVar } = this);

All 7 existing tests pass. New test passes with no diagnostics.

AI Assistance Disclosure

This PR was written primarily by Claude Code.

@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2026

🦋 Changeset detected

Latest commit: 8172a55

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 Feb 13, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR updates the no_unused_private_class_members lint to recognise private class members accessed via object destructuring from this (e.g. const { myVar } = this;, const { myVar: renamed } = this;, ({ myVar } = this)). It adds a helper to extract property names from destructuring patterns, introduces member_name on AnyMember, and updates traversal and usage checks. A new test file covers shorthand, renamed, multiple and assignment-pattern destructuring.

Possibly related PRs

  • biomejs/biome PR 7419: Modifies the same lint to recognise additional this-based access patterns and updates AnyMember handling.
  • biomejs/biome PR 7684: Improves the no_unused_private_class_members detection for non-standard private-member access patterns and adjusts traversal/usage logic.
  • biomejs/biome PR 7424: Refactors traversal and private-member collection/iteration logic in the same lint implementation.

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (4 files):

⚔️ crates/biome_js_analyze/src/lint/a11y/no_interactive_element_to_noninteractive_role.rs (content)
⚔️ crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (content)
⚔️ crates/biome_js_analyze/tests/specs/a11y/noInteractiveElementToNoninteractiveRole/valid.jsx (content)
⚔️ crates/biome_js_analyze/tests/specs/a11y/noInteractiveElementToNoninteractiveRole/valid.jsx.snap (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing the linter rule to handle destructuring from this in noUnusedPrivateClassMembers.
Description check ✅ Passed The description clearly explains the bug fix, root cause, changes made, and test coverage, directly relating to the changeset.
Linked Issues check ✅ Passed The PR fulfils issue #9008's objective: destructured accesses from this (shorthand, renamed, and assignment patterns) are now correctly recognized as valid usages.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue—the rule logic, helper functions, and test cases are narrowly focused on destructuring detection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/no-unused-private-class-members-destructuring
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (2)

297-315: Per-node allocation in hot loop

get_destructured_names_from_this is invoked for every non-AnyJsName descendant, which is the vast majority of nodes. Each call performs two cast_ref attempts and allocates a Vec. A cheap kind-check guard would avoid most of this work.

Suggested guard
                 // Handle destructuring from `this`, e.g. `const { myVar } = this;`
+                if matches!(
+                    node.kind(),
+                    JsSyntaxKind::JS_VARIABLE_DECLARATOR | JsSyntaxKind::JS_ASSIGNMENT_EXPRESSION
+                ) {
                 let destructured_names = get_destructured_names_from_this(&node);
                 if !destructured_names.is_empty() {
                     private_members.retain(|private_member| {
                         let Some(name) = private_member.member_name() else {
                             return true;
                         };

                         if !destructured_names.iter().any(|n| n.text() == name.text()) {
                             return true;
                         }

                         if !private_member.is_private_sharp() {
                             ts_private_count -= 1;
                         }

                         false
                     });
                 }
+                }

Based on learnings: "Avoid string allocations by using &str or TokenText for comparisons instead of calling to_string()" — the TokenText usage is fine, but the repeated Vec allocation on every node is the concern here.


634-666: Duplication with match_js_name and property_range

All three methods (member_name, match_js_name, property_range) repeat the same four-arm match over class member kinds. Consider extracting a shared helper that returns the AnyJsClassMemberName, letting each caller do its own final step. Not urgent — the existing code already had this pattern — but now there are three copies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

🤖 Fix all issues with AI agents
In @.changeset/fix-hr-presentation-role.md:
- Around line 1-5: The changeset .changeset/fix-hr-presentation-role.md only
documents the fix for `#9024`; you must add a separate changeset for the other fix
(issue `#9008`) that describes the bugfix for destructuring from this in the
noUnusedPrivateClassMembers rule. Create a new changeset file (matching the
repo's changeset format) that starts with "Fixed [`#9008`](issue link): ..." and
explains that destructuring assignments from this in noUnusedPrivateClassMembers
are now handled correctly, referencing the noUnusedPrivateClassMembers rule name
so the change is discoverable.
🧹 Nitpick comments (3)
crates/biome_js_analyze/src/lint/correctness/no_unused_private_class_members.rs (3)

461-465: Simplify nested if let / Ok handling.

The double unwrap via map then if let Ok is a bit convoluted. Consider flattening with and_then.

♻️ Minor cleanup
-                            if let Some(name) = shorthand.identifier().ok().map(|id| id.name_token()) {
-                                if let Ok(token) = name {
-                                    names.push(token.text_trimmed().to_string());
-                                }
-                            }
+                            if let Some(token) = shorthand
+                                .identifier()
+                                .ok()
+                                .and_then(|id| id.name_token().ok())
+                            {
+                                names.push(token.text_trimmed().to_string());
+                            }

Based on learnings: "Use functional methods like map, filter, and and_then on Result and Option types instead of nested if let statements to avoid excessive indentation."


634-667: member_name largely duplicates match_js_name and property_range.

All three methods walk the same match arms to extract member names. Consider having member_name be the single source of truth, then expressing match_js_name as self.member_name().map(|n| n == token). This would deduplicate ~40 lines and reduce the maintenance surface.

Not blocking — happy to defer this to a follow-up.


400-486: get_destructured_names_from_this is called on every non-AnyJsName descendant node.

Each call attempts two cast_ref operations. In practice this is cheap (just a kind check), but you could short-circuit with a kind check before entering the function. Very minor — not blocking.

One edge case worth noting: computed property keys in destructuring (e.g., const { [expr]: val } = this) won't be caught since as_js_literal_member_name() returns None. This is acceptable — it mirrors the existing caveat for computed access already documented in the rule — but consider adding a comment for posterity.

Comment on lines 1 to 5
---
"@biomejs/biome": patch
---

Fixed [#9024](https://github.com/biomejs/biome/issues/9024). The `noInteractiveElementToNoninteractiveRole` rule no longer incorrectly flags `<hr>` elements with `role="presentation"` or `role="none"`. The `separator` role (implicit role of `<hr>`) is now treated as non-interactive, matching the WAI-ARIA spec where a non-focusable separator is a static structural element.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing changeset for issue #9008?

This changeset covers the <hr> separator fix (#9024), but the PR also fixes #9008 (destructuring from this in noUnusedPrivateClassMembers). There doesn't appear to be a changeset for that second fix. Based on learnings, bug fixes should have a changeset starting with Fixed [#NUMBER](issue link): ....

🤖 Prompt for AI Agents
In @.changeset/fix-hr-presentation-role.md around lines 1 - 5, The changeset
.changeset/fix-hr-presentation-role.md only documents the fix for `#9024`; you
must add a separate changeset for the other fix (issue `#9008`) that describes the
bugfix for destructuring from this in the noUnusedPrivateClassMembers rule.
Create a new changeset file (matching the repo's changeset format) that starts
with "Fixed [`#9008`](issue link): ..." and explains that destructuring
assignments from this in noUnusedPrivateClassMembers are now handled correctly,
referencing the noUnusedPrivateClassMembers rule name so the change is
discoverable.

Copy link
Author

@solssak solssak Feb 13, 2026

Choose a reason for hiding this comment

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

The changeset for #9008 already exists at .changeset/fix-unused-private-destructuring.md. The fix-hr-presentation-role.md file is from a separate commit on main, not part of this PR's changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

This diff shouldn't be here at all. The PR for this changeset hasn't even been merged at all, and it doesn't even exist yet in the PR that it should be in.

Copy link
Author

Choose a reason for hiding this comment

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

@dyc3
Thanks for catching this. The branch was created from a wrong base that included an unrelated commit, so it leaked into this PR's diff. I've rebased to remove it — now only the destructuring fix remains.

@solssak solssak force-pushed the fix/no-unused-private-class-members-destructuring branch from 810b290 to d3ade0a Compare February 13, 2026 01:44
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 13, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 95 skipped benchmarks1


Comparing solssak:fix/no-unused-private-class-members-destructuring (f89586e) with main (5ac2ad6)2

Open in CodSpeed

Footnotes

  1. 95 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (85fa0f4) during the generation of this report, so 5ac2ad6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@solssak solssak force-pushed the fix/no-unused-private-class-members-destructuring branch from d3ade0a to a9a58a5 Compare February 13, 2026 04:38
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.

Thank you. The implementation is a bit wasteful, I highlighted the changes that are needed

.ok()
.and_then(|id| id.as_js_identifier_binding()?.name_token().ok())
{
names.push(name.text_trimmed().to_string());
Copy link
Member

Choose a reason for hiding this comment

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

Don't use to_string because it allocates a string. Use token_text. There's a method that returns TokenText

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! Updated to use token_text_trimmed() for token-based names and kept name().ok() as-is for JsLiteralMemberName since it already returns TokenText. No more unnecessary allocations.

}
}

fn member_name(&self) -> Option<String> {
Copy link
Member

Choose a reason for hiding this comment

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

Don't return a String. Return a JsSyntaxToken or a TokenText

Copy link
Author

Choose a reason for hiding this comment

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

Good call. Changed the return type to Option. For the class member branches, I'm using Into from ClassMemberName, and token_text_trimmed() for the TsPropertyParameter branch.

@solssak solssak force-pushed the fix/no-unused-private-class-members-destructuring branch 2 times, most recently from f89586e to 6b2f385 Compare February 13, 2026 08:00
@solssak solssak force-pushed the fix/no-unused-private-class-members-destructuring branch from 6b2f385 to 8172a55 Compare February 13, 2026 08:01
@solssak solssak requested a review from ematipico February 14, 2026 01:52
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 "is defined but never used"

3 participants