| /** |
| * @fileoverview Rule to flag declared but unused private class members |
| * @author Tim van der Lippe |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../shared/types').Rule} */ |
| module.exports = { |
| meta: { |
| type: "problem", |
| |
| docs: { |
| description: "disallow unused private class members", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/no-unused-private-class-members" |
| }, |
| |
| schema: [], |
| |
| messages: { |
| unusedPrivateClassMember: "'{{classMemberName}}' is defined but never used." |
| } |
| }, |
| |
| create(context) { |
| const trackedClasses = []; |
| |
| /** |
| * Check whether the current node is in a write only assignment. |
| * @param {ASTNode} privateIdentifierNode Node referring to a private identifier |
| * @returns {boolean} Whether the node is in a write only assignment |
| * @private |
| */ |
| function isWriteOnlyAssignment(privateIdentifierNode) { |
| const parentStatement = privateIdentifierNode.parent.parent; |
| const isAssignmentExpression = parentStatement.type === "AssignmentExpression"; |
| |
| if (!isAssignmentExpression && |
| parentStatement.type !== "ForInStatement" && |
| parentStatement.type !== "ForOfStatement" && |
| parentStatement.type !== "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 === "ExpressionStatement"; |
| } |
| |
| return true; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Public |
| //-------------------------------------------------------------------------- |
| |
| return { |
| |
| // Collect all declared members up front and assume they are all unused |
| ClassBody(classBodyNode) { |
| const privateMembers = new Map(); |
| |
| trackedClasses.unshift(privateMembers); |
| for (const bodyMember of classBodyNode.body) { |
| if (bodyMember.type === "PropertyDefinition" || bodyMember.type === "MethodDefinition") { |
| if (bodyMember.key.type === "PrivateIdentifier") { |
| privateMembers.set(bodyMember.key.name, { |
| declaredNode: bodyMember, |
| isAccessor: bodyMember.type === "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. |
| */ |
| PrivateIdentifier(privateIdentifierNode) { |
| 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) { |
| return; |
| } |
| |
| // In case any other usage was already detected, we can short circuit the logic here. |
| const memberDefinition = classBody.get(privateIdentifierNode.name); |
| |
| if (memberDefinition.isUsed) { |
| return; |
| } |
| |
| // The definition of the class member itself |
| if (privateIdentifierNode.parent.type === "PropertyDefinition" || |
| privateIdentifierNode.parent.type === "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) { |
| memberDefinition.isUsed = true; |
| 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 === "UpdateExpression" && |
| parentOfWrappingExpressionType === "ExpressionStatement") { |
| return; |
| } |
| |
| /* |
| * ({ x: this.#usedInDestructuring } = bar); |
| * |
| * But should treat the following as a read: |
| * ({ [this.#x]: a } = foo); |
| */ |
| if (wrappingExpressionType === "Property" && |
| parentOfWrappingExpressionType === "ObjectPattern" && |
| privateIdentifierNode.parent.parent.value === privateIdentifierNode.parent) { |
| return; |
| } |
| |
| // [...this.#unusedInRestPattern] = bar; |
| if (wrappingExpressionType === "RestElement") { |
| return; |
| } |
| |
| // [this.#unusedInAssignmentPattern] = bar; |
| if (wrappingExpressionType === "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. |
| */ |
| memberDefinition.isUsed = true; |
| }, |
| |
| /* |
| * 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"() { |
| const unusedPrivateMembers = trackedClasses.shift(); |
| |
| for (const [classMemberName, { declaredNode, isUsed }] of unusedPrivateMembers.entries()) { |
| if (isUsed) { |
| continue; |
| } |
| context.report({ |
| node: declaredNode, |
| loc: declaredNode.key.loc, |
| messageId: "unusedPrivateClassMember", |
| data: { |
| classMemberName: `#${classMemberName}` |
| } |
| }); |
| } |
| } |
| }; |
| } |
| }; |