From 91f34066a05548df81d3b8f6ecda076758518703 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Sat, 14 Sep 2024 09:42:29 -0400 Subject: [PATCH 01/13] feat: implement original rule without any other modifications --- .../rules/no-unused-private-class-members.ts | 244 ++++++++++ .../no-unused-private-class-members.test.ts | 451 ++++++++++++++++++ 2 files changed, 695 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/no-unused-private-class-members.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unused-private-class-members.test.ts diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts new file mode 100644 index 000000000000..8c05bc99076c --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -0,0 +1,244 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule } from '../util'; + +type Options = []; +export type MessageIds = 'unusedPrivateClassMember'; + +interface PrivateMember { + declaredNode: + | TSESTree.MethodDefinitionComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.PropertyDefinitionComputedName + | TSESTree.PropertyDefinitionNonComputedName; + isAccessor: boolean; +} + +export default createRule({ + name: 'no-unused-private-class-members', + meta: { + type: 'problem', + docs: { + description: 'Disallow unused private class members', + extendsBaseRule: true, + requiresTypeChecking: false, + }, + + schema: [], + + messages: { + unusedPrivateClassMember: + "'{{classMemberName}}' is defined but never used.", + }, + }, + defaultOptions: [], + create(context) { + const trackedClasses: Map[] = []; + const trackedMembersBeingUsed = new Set< + | TSESTree.MethodDefinitionComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.PropertyDefinitionComputedName + | TSESTree.PropertyDefinitionNonComputedName + >(); + + /** + * Check whether the current node is in a write only assignment. + * @param privateIdentifierNode Node referring to a private identifier + * @returns Whether the node is in a write only assignment + * @private + */ + function isWriteOnlyAssignment( + privateIdentifierNode: TSESTree.PrivateIdentifier, + ): boolean { + const parentStatement = privateIdentifierNode.parent.parent; + if (parentStatement === undefined) { + return false; + } + + const isAssignmentExpression = + parentStatement.type === AST_NODE_TYPES.AssignmentExpression; + + if ( + !isAssignmentExpression && + parentStatement.type !== AST_NODE_TYPES.ForInStatement && + parentStatement.type !== AST_NODE_TYPES.ForOfStatement && + parentStatement.type !== AST_NODE_TYPES.AssignmentPattern + ) { + return false; + } + + // It is a write-only usage, since we still allow usages on the right for reads + if (parentStatement.left !== privateIdentifierNode.parent) { + return false; + } + + // For any other operator (such as '+=') we still consider it a read operation + if (isAssignmentExpression && parentStatement.operator !== '=') { + /* + * However, if the read operation is "discarded" in an empty statement, then + * we consider it write only. + */ + return ( + parentStatement.parent.type === AST_NODE_TYPES.ExpressionStatement + ); + } + + return true; + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + return { + // Collect all declared members up front and assume they are all unused + ClassBody(classBodyNode): void { + const privateMembers = new Map(); + + trackedClasses.unshift(privateMembers); + for (const bodyMember of classBodyNode.body) { + if ( + (bodyMember.type === AST_NODE_TYPES.PropertyDefinition || + bodyMember.type === AST_NODE_TYPES.MethodDefinition) && + bodyMember.key.type === AST_NODE_TYPES.PrivateIdentifier + ) { + privateMembers.set(bodyMember.key.name, { + declaredNode: bodyMember, + isAccessor: + bodyMember.type === AST_NODE_TYPES.MethodDefinition && + (bodyMember.kind === 'set' || bodyMember.kind === 'get'), + }); + } + } + }, + + /* + * Process all usages of the private identifier and remove a member from + * `declaredAndUnusedPrivateMembers` if we deem it used. + */ + // Bug: We have to manually specify the type of the node or it will be + // inferred to `never` for some reason. + PrivateIdentifier( + privateIdentifierNode: TSESTree.PrivateIdentifier, + ): void { + const classBody = trackedClasses.find(classProperties => + classProperties.has(privateIdentifierNode.name), + ); + + // Can't happen, as it is a parser to have a missing class body, but + // let's code defensively here. + if (classBody === undefined) { + return; + } + + // In case any other usage was already detected, we can short circuit the logic here. + const memberDefinition = classBody.get(privateIdentifierNode.name); + if (memberDefinition === undefined) { + return; + } + if (trackedMembersBeingUsed.has(memberDefinition.declaredNode)) { + return; + } + + // The definition of the class member itself + if ( + privateIdentifierNode.parent.type === + AST_NODE_TYPES.PropertyDefinition || + privateIdentifierNode.parent.type === AST_NODE_TYPES.MethodDefinition + ) { + return; + } + + /* + * Any usage of an accessor is considered a read, as the getter/setter can have + * side-effects in its definition. + */ + if (memberDefinition.isAccessor) { + trackedMembersBeingUsed.add(memberDefinition.declaredNode); + return; + } + + // Any assignments to this member, except for assignments that also read + if (isWriteOnlyAssignment(privateIdentifierNode)) { + return; + } + + const wrappingExpressionType = + privateIdentifierNode.parent.parent?.type; + const parentOfWrappingExpressionType = + privateIdentifierNode.parent.parent?.parent?.type; + + // A statement which only increments (`this.#x++;`) + if ( + wrappingExpressionType === AST_NODE_TYPES.UpdateExpression && + parentOfWrappingExpressionType === AST_NODE_TYPES.ExpressionStatement + ) { + return; + } + + /* + * ({ x: this.#usedInDestructuring } = bar); + * + * But should treat the following as a read: + * ({ [this.#x]: a } = foo); + */ + if ( + wrappingExpressionType === AST_NODE_TYPES.Property && + parentOfWrappingExpressionType === AST_NODE_TYPES.ObjectPattern && + privateIdentifierNode.parent.parent?.value === + privateIdentifierNode.parent + ) { + return; + } + + // [...this.#unusedInRestPattern] = bar; + if (wrappingExpressionType === AST_NODE_TYPES.RestElement) { + return; + } + + // [this.#unusedInAssignmentPattern] = bar; + if (wrappingExpressionType === AST_NODE_TYPES.ArrayPattern) { + return; + } + + /* + * We can't delete the memberDefinition, as we need to keep track of which member we are marking as used. + * In the case of nested classes, we only mark the first member we encounter as used. If you were to delete + * the member, then any subsequent usage could incorrectly mark the member of an encapsulating parent class + * as used, which is incorrect. + */ + trackedMembersBeingUsed.add(memberDefinition.declaredNode); + }, + + /* + * Post-process the class members and report any remaining members. + * Since private members can only be accessed in the current class context, + * we can safely assume that all usages are within the current class body. + */ + 'ClassBody:exit'(): void { + const unusedPrivateMembers = trackedClasses.shift(); + if (unusedPrivateMembers === undefined) { + return; + } + + for (const [ + classMemberName, + { declaredNode }, + ] of unusedPrivateMembers.entries()) { + if (trackedMembersBeingUsed.has(declaredNode)) { + continue; + } + context.report({ + node: declaredNode, + loc: declaredNode.key.loc, + messageId: 'unusedPrivateClassMember', + data: { + classMemberName: `#${classMemberName}`, + }, + }); + } + }, + }; + }, +}); 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 new file mode 100644 index 000000000000..ac16d5ff6d52 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-private-class-members.test.ts @@ -0,0 +1,451 @@ +import type { TestCaseError } from '@typescript-eslint/rule-tester'; +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import type { MessageIds } from '../../src/rules/no-unused-private-class-members'; +import rule from '../../src/rules/no-unused-private-class-members'; + +const ruleTester = new RuleTester(); + +/** Returns an expected error for defined-but-not-used private class member. */ +function definedError(classMemberName: string): TestCaseError { + return { + messageId: 'unusedPrivateClassMember', + data: { + classMemberName: `#${classMemberName}`, + }, + }; +} + +ruleTester.run('no-unused-private-class-members', rule, { + valid: [ + 'class Foo {}', + ` +class Foo { + publicMember = 42; +} + `, + ` +class Foo { + #usedMember = 42; + method() { + return this.#usedMember; + } +} + `, + ` +class Foo { + #usedMember = 42; + anotherMember = this.#usedMember; +} + `, + ` +class Foo { + #usedMember = 42; + foo() { + anotherMember = this.#usedMember; + } +} + `, + ` +class C { + #usedMember; + + foo() { + bar((this.#usedMember += 1)); + } +} + `, + ` +class Foo { + #usedMember = 42; + method() { + return someGlobalMethod(this.#usedMember); + } +} + `, + ` +class C { + #usedInOuterClass; + + foo() { + return class {}; + } + + bar() { + return this.#usedInOuterClass; + } +} + `, + ` +class Foo { + #usedInForInLoop; + method() { + for (const bar in this.#usedInForInLoop) { + } + } +} + `, + ` +class Foo { + #usedInForOfLoop; + method() { + for (const bar of this.#usedInForOfLoop) { + } + } +} + `, + ` +class Foo { + #usedInAssignmentPattern; + method() { + [bar = 1] = this.#usedInAssignmentPattern; + } +} + `, + ` +class Foo { + #usedInArrayPattern; + method() { + [bar] = this.#usedInArrayPattern; + } +} + `, + ` +class Foo { + #usedInAssignmentPattern; + method() { + [bar] = this.#usedInAssignmentPattern; + } +} + `, + ` +class C { + #usedInObjectAssignment; + + method() { + ({ [this.#usedInObjectAssignment]: a } = foo); + } +} + `, + ` +class C { + set #accessorWithSetterFirst(value) { + doSomething(value); + } + get #accessorWithSetterFirst() { + return something(); + } + method() { + this.#accessorWithSetterFirst += 1; + } +} + `, + ` +class Foo { + set #accessorUsedInMemberAccess(value) {} + + method(a) { + [this.#accessorUsedInMemberAccess] = a; + } +} + `, + ` +class C { + get #accessorWithGetterFirst() { + return something(); + } + set #accessorWithGetterFirst(value) { + doSomething(value); + } + method() { + this.#accessorWithGetterFirst += 1; + } +} + `, + ` +class C { + #usedInInnerClass; + + method(a) { + return class { + foo = a.#usedInInnerClass; + }; + } +} + `, + + //-------------------------------------------------------------------------- + // Method definitions + //-------------------------------------------------------------------------- + ` +class Foo { + #usedMethod() { + return 42; + } + anotherMethod() { + return this.#usedMethod(); + } +} + `, + ` +class C { + set #x(value) { + doSomething(value); + } + + foo() { + this.#x = 1; + } +} + `, + ], + invalid: [ + { + code: ` +class Foo { + #unusedMember = 5; +} + `, + errors: [definedError('unusedMember')], + }, + { + code: ` +class First {} +class Second { + #unusedMemberInSecondClass = 5; +} + `, + errors: [definedError('unusedMemberInSecondClass')], + }, + { + code: ` +class First { + #unusedMemberInFirstClass = 5; +} +class Second {} + `, + errors: [definedError('unusedMemberInFirstClass')], + }, + { + code: ` +class First { + #firstUnusedMemberInSameClass = 5; + #secondUnusedMemberInSameClass = 5; +} + `, + errors: [ + definedError('firstUnusedMemberInSameClass'), + definedError('secondUnusedMemberInSameClass'), + ], + }, + { + code: ` +class Foo { + #usedOnlyInWrite = 5; + method() { + this.#usedOnlyInWrite = 42; + } +} + `, + errors: [definedError('usedOnlyInWrite')], + }, + { + code: ` +class Foo { + #usedOnlyInWriteStatement = 5; + method() { + this.#usedOnlyInWriteStatement += 42; + } +} + `, + errors: [definedError('usedOnlyInWriteStatement')], + }, + { + code: ` +class C { + #usedOnlyInIncrement; + + foo() { + this.#usedOnlyInIncrement++; + } +} + `, + errors: [definedError('usedOnlyInIncrement')], + }, + { + code: ` +class C { + #unusedInOuterClass; + + foo() { + return class { + #unusedInOuterClass; + + bar() { + return this.#unusedInOuterClass; + } + }; + } +} + `, + errors: [definedError('unusedInOuterClass')], + }, + { + code: ` +class C { + #unusedOnlyInSecondNestedClass; + + foo() { + return class { + #unusedOnlyInSecondNestedClass; + + bar() { + return this.#unusedOnlyInSecondNestedClass; + } + }; + } + + baz() { + return this.#unusedOnlyInSecondNestedClass; + } + + bar() { + return class { + #unusedOnlyInSecondNestedClass; + }; + } +} + `, + errors: [definedError('unusedOnlyInSecondNestedClass')], + }, + + //-------------------------------------------------------------------------- + // Unused method definitions + //-------------------------------------------------------------------------- + { + code: ` +class Foo { + #unusedMethod() {} +} + `, + errors: [definedError('unusedMethod')], + }, + { + code: ` +class Foo { + #unusedMethod() {} + #usedMethod() { + return 42; + } + publicMethod() { + return this.#usedMethod(); + } +} + `, + errors: [definedError('unusedMethod')], + }, + { + code: ` +class Foo { + set #unusedSetter(value) {} +} + `, + errors: [definedError('unusedSetter')], + }, + { + code: ` +class Foo { + #unusedForInLoop; + method() { + for (this.#unusedForInLoop in bar) { + } + } +} + `, + errors: [definedError('unusedForInLoop')], + }, + { + code: ` +class Foo { + #unusedForOfLoop; + method() { + for (this.#unusedForOfLoop of bar) { + } + } +} + `, + errors: [definedError('unusedForOfLoop')], + }, + { + code: ` +class Foo { + #unusedInDestructuring; + method() { + ({ x: this.#unusedInDestructuring } = bar); + } +} + `, + errors: [definedError('unusedInDestructuring')], + }, + { + code: ` +class Foo { + #unusedInRestPattern; + method() { + [...this.#unusedInRestPattern] = bar; + } +} + `, + errors: [definedError('unusedInRestPattern')], + }, + { + code: ` +class Foo { + #unusedInAssignmentPattern; + method() { + [this.#unusedInAssignmentPattern = 1] = bar; + } +} + `, + errors: [definedError('unusedInAssignmentPattern')], + }, + { + code: ` +class Foo { + #unusedInAssignmentPattern; + method() { + [this.#unusedInAssignmentPattern] = bar; + } +} + `, + errors: [definedError('unusedInAssignmentPattern')], + }, + { + code: ` +class C { + #usedOnlyInTheSecondInnerClass; + + method(a) { + return class { + #usedOnlyInTheSecondInnerClass; + + method2(b) { + foo = b.#usedOnlyInTheSecondInnerClass; + } + + method3(b) { + foo = b.#usedOnlyInTheSecondInnerClass; + } + }; + } +} + `, + errors: [ + { + ...definedError('usedOnlyInTheSecondInnerClass'), + line: 3, + }, + ], + }, + ], +}); From b2f0126c40f397f92cf881a8d192f04a4413a952 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Sat, 14 Sep 2024 09:51:38 -0400 Subject: [PATCH 02/13] chore: document data structure --- .../no-unused-private-class-members.mdx.txt | 21 +++++++++++++++++++ .../rules/no-unused-private-class-members.ts | 7 +++++++ 2 files changed, 28 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt diff --git a/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt b/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt new file mode 100644 index 000000000000..28e0d75a2369 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt @@ -0,0 +1,21 @@ +--- +description: 'Disallow unused private class members.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> ๐Ÿ›‘ This file is source code, not the primary documentation location! ๐Ÿ›‘ +> +> See **https://typescript-eslint.io/rules/no-unused-private-class-members** for documentation. + +This rule extends the base [`eslint/class-methods-use-this`](https://eslint.org/docs/rules/no-unused-private-class-members) rule. +It adds support for members declared with TypeScript's `private` keyword. + +## Options + +This rule has no options. + +## When Not To Use It + +If you donโ€™t want to be notified about unused private class members, you can safely turn this rule off. diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 8c05bc99076c..785f091ee5f2 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -35,6 +35,13 @@ export default createRule({ defaultOptions: [], create(context) { const trackedClasses: Map[] = []; + + /** + * The core ESLint rule tracks methods by adding an extra property of + * "isUsed" to the method, which obviously violates type safety. Instead, in + * our extended rule, we create a separate data structure to track whether a + * method is being used. + */ const trackedMembersBeingUsed = new Set< | TSESTree.MethodDefinitionComputedName | TSESTree.MethodDefinitionNonComputedName From e37fd8bca0b896a551a9a92c25c713bfeb1dce0c Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:27:46 -0400 Subject: [PATCH 03/13] update --- .../rules/no-unused-private-class-members.ts | 195 ++++++++++-------- 1 file changed, 104 insertions(+), 91 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 785f091ee5f2..28b1ff07b6ec 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -75,17 +75,17 @@ export default createRule({ return false; } - // It is a write-only usage, since we still allow usages on the right for reads + // It is a write-only usage, since we still allow usages on the right for + // reads. if (parentStatement.left !== privateIdentifierNode.parent) { return false; } - // For any other operator (such as '+=') we still consider it a read operation + // For any other operator (such as '+=') we still consider it a read + // operation. if (isAssignmentExpression && parentStatement.operator !== '=') { - /* - * However, if the read operation is "discarded" in an empty statement, then - * we consider it write only. - */ + // However, if the read operation is "discarded" in an empty statement, + // then we consider it write only. return ( parentStatement.parent.type === AST_NODE_TYPES.ExpressionStatement ); @@ -98,6 +98,95 @@ export default createRule({ // Public //-------------------------------------------------------------------------- + function processPrivateIdentifier( + privateIdentifierNode: TSESTree.PrivateIdentifier, + ): void { + const classBody = trackedClasses.find(classProperties => + classProperties.has(privateIdentifierNode.name), + ); + + // Can't happen, as it is a parser error to have a missing class body, but + // let's code defensively here. + if (classBody === undefined) { + return; + } + + // In case any other usage was already detected, we can short circuit the + // logic here. + const memberDefinition = classBody.get(privateIdentifierNode.name); + if (memberDefinition === undefined) { + return; + } + if (trackedMembersBeingUsed.has(memberDefinition.declaredNode)) { + return; + } + + // The definition of the class member itself + if ( + privateIdentifierNode.parent.type === + AST_NODE_TYPES.PropertyDefinition || + privateIdentifierNode.parent.type === AST_NODE_TYPES.MethodDefinition + ) { + return; + } + + // Any usage of an accessor is considered a read, as the getter/setter can + // have side-effects in its definition. + if (memberDefinition.isAccessor) { + trackedMembersBeingUsed.add(memberDefinition.declaredNode); + return; + } + + // Any assignments to this member, except for assignments that also read + if (isWriteOnlyAssignment(privateIdentifierNode)) { + return; + } + + const wrappingExpressionType = privateIdentifierNode.parent.parent?.type; + const parentOfWrappingExpressionType = + privateIdentifierNode.parent.parent?.parent?.type; + + // A statement which only increments (`this.#x++;`) + if ( + wrappingExpressionType === AST_NODE_TYPES.UpdateExpression && + parentOfWrappingExpressionType === AST_NODE_TYPES.ExpressionStatement + ) { + return; + } + + /* + * ({ x: this.#usedInDestructuring } = bar); + * + * But should treat the following as a read: + * ({ [this.#x]: a } = foo); + */ + if ( + wrappingExpressionType === AST_NODE_TYPES.Property && + parentOfWrappingExpressionType === AST_NODE_TYPES.ObjectPattern && + privateIdentifierNode.parent.parent?.value === + privateIdentifierNode.parent + ) { + return; + } + + // [...this.#unusedInRestPattern] = bar; + if (wrappingExpressionType === AST_NODE_TYPES.RestElement) { + return; + } + + // [this.#unusedInAssignmentPattern] = bar; + if (wrappingExpressionType === AST_NODE_TYPES.ArrayPattern) { + return; + } + + // We can't delete the memberDefinition, as we need to keep track of which + // member we are marking as used. In the case of nested classes, we only + // mark the first member we encounter as used. If you were to delete the + // member, then any subsequent usage could incorrectly mark the member of + // an encapsulating parent class as used, which is incorrect. + trackedMembersBeingUsed.add(memberDefinition.declaredNode); + } + return { // Collect all declared members up front and assume they are all unused ClassBody(classBodyNode): void { @@ -124,98 +213,22 @@ export default createRule({ * Process all usages of the private identifier and remove a member from * `declaredAndUnusedPrivateMembers` if we deem it used. */ - // Bug: We have to manually specify the type of the node or it will be + // Bug: We have to manually specify the type for the node or it will be // inferred to `never` for some reason. + // https://github.com/typescript-eslint/typescript-eslint/issues/9988 PrivateIdentifier( privateIdentifierNode: TSESTree.PrivateIdentifier, ): void { - const classBody = trackedClasses.find(classProperties => - classProperties.has(privateIdentifierNode.name), - ); - - // Can't happen, as it is a parser to have a missing class body, but - // let's code defensively here. - if (classBody === undefined) { - return; - } - - // In case any other usage was already detected, we can short circuit the logic here. - const memberDefinition = classBody.get(privateIdentifierNode.name); - if (memberDefinition === undefined) { - return; - } - if (trackedMembersBeingUsed.has(memberDefinition.declaredNode)) { - return; - } - - // The definition of the class member itself - if ( - privateIdentifierNode.parent.type === - AST_NODE_TYPES.PropertyDefinition || - privateIdentifierNode.parent.type === AST_NODE_TYPES.MethodDefinition - ) { - return; - } - - /* - * Any usage of an accessor is considered a read, as the getter/setter can have - * side-effects in its definition. - */ - if (memberDefinition.isAccessor) { - trackedMembersBeingUsed.add(memberDefinition.declaredNode); - return; - } - - // Any assignments to this member, except for assignments that also read - if (isWriteOnlyAssignment(privateIdentifierNode)) { - return; - } - - const wrappingExpressionType = - privateIdentifierNode.parent.parent?.type; - const parentOfWrappingExpressionType = - privateIdentifierNode.parent.parent?.parent?.type; - - // A statement which only increments (`this.#x++;`) - if ( - wrappingExpressionType === AST_NODE_TYPES.UpdateExpression && - parentOfWrappingExpressionType === AST_NODE_TYPES.ExpressionStatement - ) { - return; - } - - /* - * ({ x: this.#usedInDestructuring } = bar); - * - * But should treat the following as a read: - * ({ [this.#x]: a } = foo); - */ - if ( - wrappingExpressionType === AST_NODE_TYPES.Property && - parentOfWrappingExpressionType === AST_NODE_TYPES.ObjectPattern && - privateIdentifierNode.parent.parent?.value === - privateIdentifierNode.parent - ) { - return; - } - - // [...this.#unusedInRestPattern] = bar; - if (wrappingExpressionType === AST_NODE_TYPES.RestElement) { - return; - } + processPrivateIdentifier(privateIdentifierNode); + }, - // [this.#unusedInAssignmentPattern] = bar; - if (wrappingExpressionType === AST_NODE_TYPES.ArrayPattern) { + /** + * We need to repeat the logic from the `PrivateIdentitier` block above + */ + PropertyDefinition(node): void { + if (node.accessibility !== 'private') { return; } - - /* - * We can't delete the memberDefinition, as we need to keep track of which member we are marking as used. - * In the case of nested classes, we only mark the first member we encounter as used. If you were to delete - * the member, then any subsequent usage could incorrectly mark the member of an encapsulating parent class - * as used, which is incorrect. - */ - trackedMembersBeingUsed.add(memberDefinition.declaredNode); }, /* From 819b8a5fcf5f82f4eb40a168f31f837cefe79daa Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Sat, 2 Nov 2024 06:36:51 -0400 Subject: [PATCH 04/13] update --- .../src/rules/no-unused-private-class-members.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 28b1ff07b6ec..fc563025dad5 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -233,8 +233,9 @@ export default createRule({ /* * Post-process the class members and report any remaining members. - * Since private members can only be accessed in the current class context, - * we can safely assume that all usages are within the current class body. + * Since private members can only be accessed in the current class + * context, we can safely assume that all usages are within the current + * class body. */ 'ClassBody:exit'(): void { const unusedPrivateMembers = trackedClasses.shift(); From c6216974e9c612826869a648235aa3812f98ae8b Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Sat, 2 Nov 2024 07:30:11 -0400 Subject: [PATCH 05/13] update --- ...xt => no-unused-private-class-members.mdx} | 2 +- .../rules/no-unused-private-class-members.ts | 110 +++++++++--------- .../no-unused-private-class-members.test.ts | 30 +++++ 3 files changed, 87 insertions(+), 55 deletions(-) rename packages/eslint-plugin/docs/rules/{no-unused-private-class-members.mdx.txt => no-unused-private-class-members.mdx} (79%) diff --git a/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt b/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx similarity index 79% rename from packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt rename to packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx index 28e0d75a2369..d3d468c8f525 100644 --- a/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx.txt +++ b/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx @@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/no-unused-private-class-members** for documentation. -This rule extends the base [`eslint/class-methods-use-this`](https://eslint.org/docs/rules/no-unused-private-class-members) rule. +This rule extends the base [`eslint/no-unused-private-class-members`](https://eslint.org/docs/rules/no-unused-private-class-members) rule. It adds support for members declared with TypeScript's `private` keyword. ## Options diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index fc563025dad5..ac36489f9437 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -34,7 +34,7 @@ export default createRule({ }, defaultOptions: [], create(context) { - const trackedClasses: Map[] = []; + const trackedClassMembers: Map[] = []; /** * The core ESLint rule tracks methods by adding an extra property of @@ -42,7 +42,7 @@ export default createRule({ * our extended rule, we create a separate data structure to track whether a * method is being used. */ - const trackedMembersBeingUsed = new Set< + const trackedClassMembersUsed = new Set< | TSESTree.MethodDefinitionComputedName | TSESTree.MethodDefinitionNonComputedName | TSESTree.PropertyDefinitionComputedName @@ -51,14 +51,12 @@ export default createRule({ /** * Check whether the current node is in a write only assignment. - * @param privateIdentifierNode Node referring to a private identifier + * @param node Node referring to a private identifier * @returns Whether the node is in a write only assignment * @private */ - function isWriteOnlyAssignment( - privateIdentifierNode: TSESTree.PrivateIdentifier, - ): boolean { - const parentStatement = privateIdentifierNode.parent.parent; + function isWriteOnlyAssignment(node: TSESTree.Identifier): boolean { + const parentStatement = node.parent.parent; if (parentStatement === undefined) { return false; } @@ -77,7 +75,7 @@ export default createRule({ // It is a write-only usage, since we still allow usages on the right for // reads. - if (parentStatement.left !== privateIdentifierNode.parent) { + if (parentStatement.left !== node.parent) { return false; } @@ -98,11 +96,9 @@ export default createRule({ // Public //-------------------------------------------------------------------------- - function processPrivateIdentifier( - privateIdentifierNode: TSESTree.PrivateIdentifier, - ): void { - const classBody = trackedClasses.find(classProperties => - classProperties.has(privateIdentifierNode.name), + function processPrivateIdentifier(node: TSESTree.Identifier): void { + const classBody = trackedClassMembers.find(classProperties => + classProperties.has(node.name), ); // Can't happen, as it is a parser error to have a missing class body, but @@ -113,19 +109,18 @@ export default createRule({ // In case any other usage was already detected, we can short circuit the // logic here. - const memberDefinition = classBody.get(privateIdentifierNode.name); + const memberDefinition = classBody.get(node.name); if (memberDefinition === undefined) { return; } - if (trackedMembersBeingUsed.has(memberDefinition.declaredNode)) { + if (trackedClassMembersUsed.has(memberDefinition.declaredNode)) { return; } // The definition of the class member itself if ( - privateIdentifierNode.parent.type === - AST_NODE_TYPES.PropertyDefinition || - privateIdentifierNode.parent.type === AST_NODE_TYPES.MethodDefinition + node.parent.type === AST_NODE_TYPES.PropertyDefinition || + node.parent.type === AST_NODE_TYPES.MethodDefinition ) { return; } @@ -133,18 +128,17 @@ export default createRule({ // Any usage of an accessor is considered a read, as the getter/setter can // have side-effects in its definition. if (memberDefinition.isAccessor) { - trackedMembersBeingUsed.add(memberDefinition.declaredNode); + trackedClassMembersUsed.add(memberDefinition.declaredNode); return; } // Any assignments to this member, except for assignments that also read - if (isWriteOnlyAssignment(privateIdentifierNode)) { + if (isWriteOnlyAssignment(node)) { return; } - const wrappingExpressionType = privateIdentifierNode.parent.parent?.type; - const parentOfWrappingExpressionType = - privateIdentifierNode.parent.parent?.parent?.type; + const wrappingExpressionType = node.parent.parent?.type; + const parentOfWrappingExpressionType = node.parent.parent?.parent?.type; // A statement which only increments (`this.#x++;`) if ( @@ -163,8 +157,7 @@ export default createRule({ if ( wrappingExpressionType === AST_NODE_TYPES.Property && parentOfWrappingExpressionType === AST_NODE_TYPES.ObjectPattern && - privateIdentifierNode.parent.parent?.value === - privateIdentifierNode.parent + node.parent.parent?.value === node.parent ) { return; } @@ -184,20 +177,34 @@ export default createRule({ // mark the first member we encounter as used. If you were to delete the // member, then any subsequent usage could incorrectly mark the member of // an encapsulating parent class as used, which is incorrect. - trackedMembersBeingUsed.add(memberDefinition.declaredNode); + trackedClassMembersUsed.add(memberDefinition.declaredNode); + } + + function processPropertyDefinition( + node: TSESTree.PropertyDefinition, + ): void { + if ( + node.accessibility === 'private' && + node.key.type === AST_NODE_TYPES.Identifier + ) { + processPrivateIdentifier(node.key); + } } return { - // Collect all declared members up front and assume they are all unused + // Collect all declared members/methods up front and assume they are all + // unused. ClassBody(classBodyNode): void { const privateMembers = new Map(); - trackedClasses.unshift(privateMembers); + trackedClassMembers.unshift(privateMembers); for (const bodyMember of classBodyNode.body) { if ( (bodyMember.type === AST_NODE_TYPES.PropertyDefinition || bodyMember.type === AST_NODE_TYPES.MethodDefinition) && - bodyMember.key.type === AST_NODE_TYPES.PrivateIdentifier + (bodyMember.key.type === AST_NODE_TYPES.PrivateIdentifier || + (bodyMember.key.type === AST_NODE_TYPES.Identifier && + bodyMember.accessibility === 'private')) ) { privateMembers.set(bodyMember.key.name, { declaredNode: bodyMember, @@ -209,36 +216,31 @@ export default createRule({ } }, - /* - * Process all usages of the private identifier and remove a member from - * `declaredAndUnusedPrivateMembers` if we deem it used. - */ - // Bug: We have to manually specify the type for the node or it will be - // inferred to `never` for some reason. - // https://github.com/typescript-eslint/typescript-eslint/issues/9988 - PrivateIdentifier( - privateIdentifierNode: TSESTree.PrivateIdentifier, - ): void { - processPrivateIdentifier(privateIdentifierNode); + // Process nodes like: + // ```ts + // class A { + // #myPrivateMember = 123; + // } + // ``` + PrivateIdentifier(node): void { + processPrivateIdentifier(node); }, - /** - * We need to repeat the logic from the `PrivateIdentitier` block above - */ + // Process nodes like: + // ```ts + // class A { + // private myPrivateMember = 123; + // } + // ``` PropertyDefinition(node): void { - if (node.accessibility !== 'private') { - return; - } + processPropertyDefinition(node); }, - /* - * Post-process the class members and report any remaining members. - * Since private members can only be accessed in the current class - * context, we can safely assume that all usages are within the current - * class body. - */ + // Post-process the class members and report any remaining members. Since + // private members can only be accessed in the current class context, we + // can safely assume that all usages are within the current class body. 'ClassBody:exit'(): void { - const unusedPrivateMembers = trackedClasses.shift(); + const unusedPrivateMembers = trackedClassMembers.shift(); if (unusedPrivateMembers === undefined) { return; } @@ -247,7 +249,7 @@ export default createRule({ classMemberName, { declaredNode }, ] of unusedPrivateMembers.entries()) { - if (trackedMembersBeingUsed.has(declaredNode)) { + if (trackedClassMembersUsed.has(declaredNode)) { continue; } context.report({ 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 ac16d5ff6d52..76e8f7d93e58 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 @@ -447,5 +447,35 @@ class C { }, ], }, + + //-------------------------------------------------------------------------- + // TypeScript checks + //-------------------------------------------------------------------------- + { + code: ` +class A { + private unusedMember; +} + `, + errors: [ + { + ...definedError('unusedMember'), + line: 3, + }, + ], + }, + { + code: ` +class A { + private unusedMethod() {} +} + `, + errors: [ + { + ...definedError('unusedMethod'), + line: 3, + }, + ], + }, ], }); From 389e1f2c6d42dda2597526c32c50e05b78a4597b Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Sat, 2 Nov 2024 07:32:59 -0400 Subject: [PATCH 06/13] update --- .../rules/no-unused-private-class-members.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index ac36489f9437..299c43dca7a9 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -180,17 +180,6 @@ export default createRule({ trackedClassMembersUsed.add(memberDefinition.declaredNode); } - function processPropertyDefinition( - node: TSESTree.PropertyDefinition, - ): void { - if ( - node.accessibility === 'private' && - node.key.type === AST_NODE_TYPES.Identifier - ) { - processPrivateIdentifier(node.key); - } - } - return { // Collect all declared members/methods up front and assume they are all // unused. @@ -233,7 +222,12 @@ export default createRule({ // } // ``` PropertyDefinition(node): void { - processPropertyDefinition(node); + if ( + node.accessibility === 'private' && + node.key.type === AST_NODE_TYPES.Identifier + ) { + processPrivateIdentifier(node.key); + } }, // Post-process the class members and report any remaining members. Since From e82d5f5132b559f9b06b48dfbf561e2538b8f24a Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:26:03 -0500 Subject: [PATCH 07/13] update --- .../tests/rules/no-unused-private-class-members.test.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 76e8f7d93e58..3acc43f19d22 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 @@ -1,3 +1,7 @@ +// The following tests are adapted from the tests in eslint. +// Original Code: https://github.com/eslint/eslint/blob/eb76282e0a2db8aa10a3d5659f5f9237d9729121/tests/lib/rules/no-unused-vars.js +// License : https://github.com/eslint/eslint/blob/eb76282e0a2db8aa10a3d5659f5f9237d9729121/LICENSE + import type { TestCaseError } from '@typescript-eslint/rule-tester'; import { RuleTester } from '@typescript-eslint/rule-tester'; From 57369abf81bd0ca762642d1b154c5e7e112a30e8 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:27:11 -0500 Subject: [PATCH 08/13] update --- .../tests/rules/no-unused-private-class-members.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3acc43f19d22..ee400ffc5162 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 @@ -1,6 +1,6 @@ // The following tests are adapted from the tests in eslint. -// Original Code: https://github.com/eslint/eslint/blob/eb76282e0a2db8aa10a3d5659f5f9237d9729121/tests/lib/rules/no-unused-vars.js -// License : https://github.com/eslint/eslint/blob/eb76282e0a2db8aa10a3d5659f5f9237d9729121/LICENSE +// Original Code: https://github.com/eslint/eslint/blob/522d8a32f326c52886c531f43cf6a1ff15af8286/tests/lib/rules/no-unused-private-class-members.js +// License : https://github.com/eslint/eslint/blob/522d8a32f326c52886c531f43cf6a1ff15af8286/LICENSE import type { TestCaseError } from '@typescript-eslint/rule-tester'; import { RuleTester } from '@typescript-eslint/rule-tester'; From 12c2c6dc71bc4e2037b8afab901a7107eb548759 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:28:39 -0500 Subject: [PATCH 09/13] update --- .../src/rules/no-unused-private-class-members.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 299c43dca7a9..9f9de7fd8050 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -1,3 +1,7 @@ +// The following code is adapted from the code in eslint/eslint. +// Source: https://github.com/eslint/eslint/blob/522d8a32f326c52886c531f43cf6a1ff15af8286/lib/rules/no-unused-private-class-members.js +// License: https://github.com/eslint/eslint/blob/522d8a32f326c52886c531f43cf6a1ff15af8286/LICENSE + import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; From 76f1693cf7849e0a67b88c6105c336a48bac7501 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:32:03 -0500 Subject: [PATCH 10/13] update --- .../rules/no-unused-private-class-members.mdx | 27 +++++++++++++++++++ .../rules/no-unused-private-class-members.ts | 8 ++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx b/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx index d3d468c8f525..15f7c271f3e7 100644 --- a/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx +++ b/packages/eslint-plugin/docs/rules/no-unused-private-class-members.mdx @@ -16,6 +16,33 @@ It adds support for members declared with TypeScript's `private` keyword. This rule has no options. +## Examples + + + + +```ts +class A { + private foo = 123; +} +``` + + + + +```tsx +class A { + private foo = 123; + + constructor() { + console.log(this.foo); + } +} +``` + + + + ## When Not To Use It If you donโ€™t want to be notified about unused private class members, you can safely turn this rule off. diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 9f9de7fd8050..2e1e2f4c74b1 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -59,7 +59,9 @@ export default createRule({ * @returns Whether the node is in a write only assignment * @private */ - function isWriteOnlyAssignment(node: TSESTree.Identifier): boolean { + function isWriteOnlyAssignment( + node: TSESTree.Identifier | TSESTree.PrivateIdentifier, + ): boolean { const parentStatement = node.parent.parent; if (parentStatement === undefined) { return false; @@ -100,7 +102,9 @@ export default createRule({ // Public //-------------------------------------------------------------------------- - function processPrivateIdentifier(node: TSESTree.Identifier): void { + function processPrivateIdentifier( + node: TSESTree.Identifier | TSESTree.PrivateIdentifier, + ): void { const classBody = trackedClassMembers.find(classProperties => classProperties.has(node.name), ); From b8231a9f35f1d0d4ee4c149ec05730c4ca62eda4 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:33:18 -0500 Subject: [PATCH 11/13] update --- .../src/rules/no-unused-private-class-members.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 2e1e2f4c74b1..4c3ba24151ac 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -42,9 +42,9 @@ export default createRule({ /** * The core ESLint rule tracks methods by adding an extra property of - * "isUsed" to the method, which obviously violates type safety. Instead, in - * our extended rule, we create a separate data structure to track whether a - * method is being used. + * "isUsed" to the method, which is a bug according to Joshua Goldberg. + * Instead, in our extended rule, we create a separate data structure to + * track whether a method is being used. */ const trackedClassMembersUsed = new Set< | TSESTree.MethodDefinitionComputedName From d430b1744dbc8db578b485ee84897c82b6a96699 Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:49:53 -0500 Subject: [PATCH 12/13] update --- .../src/rules/no-unused-private-class-members.ts | 7 ++++--- .../tests/rules/no-unused-private-class-members.test.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts index 4c3ba24151ac..b2fe3bf0b153 100644 --- a/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts +++ b/packages/eslint-plugin/src/rules/no-unused-private-class-members.ts @@ -3,6 +3,7 @@ // License: https://github.com/eslint/eslint/blob/522d8a32f326c52886c531f43cf6a1ff15af8286/LICENSE import type { TSESTree } from '@typescript-eslint/utils'; + import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule } from '../util'; @@ -29,12 +30,12 @@ export default createRule({ requiresTypeChecking: false, }, - schema: [], - messages: { unusedPrivateClassMember: "'{{classMemberName}}' is defined but never used.", }, + + schema: [], }, defaultOptions: [], create(context) { @@ -255,8 +256,8 @@ export default createRule({ continue; } context.report({ - node: declaredNode, loc: declaredNode.key.loc, + node: declaredNode, messageId: 'unusedPrivateClassMember', data: { classMemberName: `#${classMemberName}`, 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 ee400ffc5162..9f3c61b6b32d 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 @@ -3,9 +3,11 @@ // License : https://github.com/eslint/eslint/blob/522d8a32f326c52886c531f43cf6a1ff15af8286/LICENSE import type { TestCaseError } from '@typescript-eslint/rule-tester'; + import { RuleTester } from '@typescript-eslint/rule-tester'; import type { MessageIds } from '../../src/rules/no-unused-private-class-members'; + import rule from '../../src/rules/no-unused-private-class-members'; const ruleTester = new RuleTester(); @@ -13,10 +15,10 @@ const ruleTester = new RuleTester(); /** Returns an expected error for defined-but-not-used private class member. */ function definedError(classMemberName: string): TestCaseError { return { - messageId: 'unusedPrivateClassMember', data: { classMemberName: `#${classMemberName}`, }, + messageId: 'unusedPrivateClassMember', }; } From 45320ab3792484bf604cbdd2e047c5b12249b66e Mon Sep 17 00:00:00 2001 From: Zamiell <5511220+Zamiell@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:46:48 -0500 Subject: [PATCH 13/13] update --- packages/eslint-plugin/src/rules/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 72c62f3e0122..a200c8cc7b9f 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -85,6 +85,7 @@ import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; import noUnsafeUnaryMinus from './no-unsafe-unary-minus'; import noUnusedExpressions from './no-unused-expressions'; +import noUnusedPrivateClassMembers from './no-unused-private-class-members'; import noUnusedVars from './no-unused-vars'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; @@ -215,6 +216,7 @@ const rules = { 'no-unsafe-return': noUnsafeReturn, 'no-unsafe-unary-minus': noUnsafeUnaryMinus, 'no-unused-expressions': noUnusedExpressions, + 'no-unused-private-class-members': noUnusedPrivateClassMembers, 'no-unused-vars': noUnusedVars, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor,