diff --git a/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts b/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts index b72ece7f9a25..9f082124d9d5 100644 --- a/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts +++ b/packages/eslint-plugin/src/util/class-scope-analyzer/classScopeAnalyzer.ts @@ -16,7 +16,7 @@ import { extractNameForMember, extractNameForMemberExpression, } from './extractComputedName'; -import { privateKey } from './types'; +import { privateKey, publicKey } from './types'; export class Member { /** @@ -435,6 +435,28 @@ abstract class ThisScope extends Visitor { // Visit selectors // ///////////////////// + protected AssignmentExpression(node: TSESTree.AssignmentExpression): void { + this.visitChildren(node); + + if ( + node.right.type === AST_NODE_TYPES.ThisExpression && + node.left.type === AST_NODE_TYPES.ObjectPattern + ) { + this.handleThisDestructuring(node.left); + } + } + + protected AssignmentPattern(node: TSESTree.AssignmentPattern): void { + this.visitChildren(node); + + if ( + node.right.type === AST_NODE_TYPES.ThisExpression && + node.left.type === AST_NODE_TYPES.ObjectPattern + ) { + this.handleThisDestructuring(node.left); + } + } + protected ClassDeclaration(node: TSESTree.ClassDeclaration): void { this.visitClass(node); } @@ -528,6 +550,49 @@ abstract class ThisScope extends Visitor { protected StaticBlock(node: TSESTree.StaticBlock): void { this.visitIntermediate(node); } + + protected VariableDeclarator(node: TSESTree.VariableDeclarator): void { + this.visitChildren(node); + + if ( + node.init?.type === AST_NODE_TYPES.ThisExpression && + node.id.type === AST_NODE_TYPES.ObjectPattern + ) { + this.handleThisDestructuring(node.id); + } + } + + /** + * Handles destructuring from `this` in ObjectPattern. + * Example: const { property } = this; + */ + private handleThisDestructuring(pattern: TSESTree.ObjectPattern): void { + if (this.thisContext == null) { + return; + } + + for (const prop of pattern.properties) { + if (prop.type !== AST_NODE_TYPES.Property) { + continue; + } + + if (prop.key.type !== AST_NODE_TYPES.Identifier) { + continue; + } + + const memberKey = publicKey(prop.key.name); + const members = this.isStaticThisContext + ? this.thisContext.members.static + : this.thisContext.members.instance; + const member = members.get(memberKey); + + if (member == null) { + continue; + } + + countReference(prop.key, member); + } + } } /** diff --git a/packages/eslint-plugin/tests/rules/no-unused-private-class-members.test.ts b/packages/eslint-plugin/tests/rules/no-unused-private-class-members.test.ts index c6541fc7464c..5c7349d35da6 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-private-class-members.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-private-class-members.test.ts @@ -379,7 +379,70 @@ class C { } } `, + ` +class Foo { + private privateMember; + private privateMember2; + method() { + const { privateMember, privateMember2 } = this; + console.log(privateMember, privateMember2); + } +} + `, + ` +class Foo { + private static staticMember = 1; + static method() { + const { staticMember } = this; + console.log(staticMember); + } +} + `, + ` +class Foo { + private privateMember = 1; + method() { + const { privateMember } = this; + } +} + `, + ` +class Foo { + private privateMember = 1; + method() { + const { privateMember: privateMember2 } = this; + } +} + `, + ` +class Foo { + private privateMember = 1; + + method() { + let privateMember; + ({ privateMember } = this); + } +} + `, + ` +class Foo { + private privateMember = 1; + + method() { + const foo = ({ privateMember } = this) => {}; + } +} + `, + ` +class Foo { + private privateMember; + + method() { + const { privateMember: used } = this; + } +} + `, //-------------------------------------------------------------------------- // Method definitions //-------------------------------------------------------------------------- @@ -1217,6 +1280,25 @@ class Foo { method() { [this.privateMember] = bar; } +} + `, + errors: [ + { + data: { + classMemberName: 'privateMember', + }, + messageId: 'unusedPrivateClassMember', + }, + ], + }, + { + code: ` +class Foo { + private privateMember; + + method() { + const { unused: privateMember } = this; + } } `, errors: [