Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Add no-unused-private-class-members rule (fixes #14859) #14895

Merged
merged 12 commits into from Oct 22, 2021
1 change: 0 additions & 1 deletion conf/eslint-recommended.js
Expand Up @@ -64,7 +64,6 @@ module.exports = {
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
Expand Down
103 changes: 90 additions & 13 deletions lib/rules/no-unused-private-class-members.js
Expand Up @@ -16,7 +16,7 @@ module.exports = {
docs: {
description: "disallow unused private class members",
category: "Variables",
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
recommended: true,
recommended: false,
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
url: "https://eslint.org/docs/rules/no-unused-private-class-members"
},

Expand All @@ -28,8 +28,51 @@ module.exports = {
},

create(context) {
const stackOfCurrentlyProcessingClassNodes = [];
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved

/**
* Check whether the current node is in a write only assignment.
* @param {ASTNode} privateIdentifierNode Node referring to a private identifier
* @param {boolean} isSetter Whether the property is declared as setter
* @returns {boolean} Whether the node is in a write only assignment
* @private
*/
function isWriteOnlyAssignmentStatementWithoutPerformingAnyReads(privateIdentifierNode, isSetter) {
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
const parentStatement = privateIdentifierNode.parent.parent;
const isAssignmentExpression = parentStatement.type === "AssignmentExpression";

if (!isAssignmentExpression &&
parentStatement.type !== "ForInStatement" &&
parentStatement.type !== "ForOfStatement" &&
parentStatement.type !== "AssignmentPattern") {
return false;
}

/*
* Any assignment that writes to a setter is considered a read, as the setter can have
* side-effects in its definition.
*/
if (isSetter) {
return false;
}
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved

// 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";
}

const declaredAndUnusedPrivateMembers = new Map();
return true;
}

//--------------------------------------------------------------------------
// Public
Expand All @@ -39,10 +82,16 @@ module.exports = {

// Collect all declared members up front and assume they are all unused
ClassBody(classBodyNode) {
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
const currentPrivateMembersPerClass = new Map();

stackOfCurrentlyProcessingClassNodes.unshift(currentPrivateMembersPerClass);
for (const bodyMember of classBodyNode.body) {
if (bodyMember.type === "PropertyDefinition" || bodyMember.type === "MethodDefinition") {
if (bodyMember.key.type === "PrivateIdentifier") {
declaredAndUnusedPrivateMembers.set(bodyMember.key.name, bodyMember);
currentPrivateMembersPerClass.set(bodyMember.key.name, {
declaredNode: bodyMember,
isSetter: bodyMember.type === "MethodDefinition" && bodyMember.kind === "set"
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
}
Expand All @@ -53,17 +102,44 @@ module.exports = {
* `declaredAndUnusedPrivateMembers` if we deem it used.
*/
PrivateIdentifier(privateIdentifierNode) {
if (

// The definition of the class member itself
privateIdentifierNode.parent.type !== "PropertyDefinition" &&
privateIdentifierNode.parent.type !== "MethodDefinition" &&
// The definition of the class member itself
if (privateIdentifierNode.parent.type === "PropertyDefinition" ||
privateIdentifierNode.parent.type === "MethodDefinition") {
return;
}

const isSetter = stackOfCurrentlyProcessingClassNodes[0].get(privateIdentifierNode.name)?.isSetter;

// Any assignments to this member, except for assignments that also read
if (isWriteOnlyAssignmentStatementWithoutPerformingAnyReads(privateIdentifierNode, isSetter)) {
return;
}

// Any usage of this member, except for writes (if it is a property)
privateIdentifierNode.parent.parent.type !== "AssignmentExpression") {
// A statement which only increments (`this.#x++;`)
if (privateIdentifierNode.parent.parent.type === "UpdateExpression" &&
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
privateIdentifierNode.parent.parent.parent.type === "ExpressionStatement") {
return;
}

declaredAndUnusedPrivateMembers.delete(privateIdentifierNode.name);
// ({ x: this.#usedInDestructuring } = bar);
if (privateIdentifierNode.parent.parent.type === "Property" &&
privateIdentifierNode.parent.parent.parent.type === "ObjectPattern") {
return;
}

// [...this.#unusedInRestPattern] = bar;
if (privateIdentifierNode.parent.parent.type === "RestElement") {
return;
}

// [this.#unusedInAssignmentPattern] = bar;
if (privateIdentifierNode.parent.parent.type === "ArrayPattern" &&
privateIdentifierNode.parent.parent.parent.type === "AssignmentExpression") {
return;
}

stackOfCurrentlyProcessingClassNodes[0].delete(privateIdentifierNode.name);
},

/*
Expand All @@ -72,16 +148,17 @@ module.exports = {
* we can safely assume that all usages are within the current class body.
*/
"ClassBody:exit"() {
for (const [classMemberName, remainingDeclaredMember] of declaredAndUnusedPrivateMembers.entries()) {
const declaredAndUnusedPrivateMembers = stackOfCurrentlyProcessingClassNodes.shift();
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved

for (const [classMemberName, { declaredNode }] of declaredAndUnusedPrivateMembers.entries()) {
context.report({
node: remainingDeclaredMember,
node: declaredNode,
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
messageId: "unusedPrivateClassMember",
data: {
classMemberName
TimvdLippe marked this conversation as resolved.
Show resolved Hide resolved
}
});
}
declaredAndUnusedPrivateMembers.clear();
}
};
}
Expand Down