forked from eslint/eslint
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New: Report unused private class members (fixes eslint#14859)
For any private class property or method, report those that are unused. Since private class members can only be accessed in the same class body, we can safely assume that all usages are processed when we leave the ClassBody scope.
- Loading branch information
1 parent
537cf6a
commit 8b2d49f
Showing
6 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Disallow Unused Private Class Members (no-unused-private-class-members) | ||
|
||
Private class members that are declared and not used anywhere in the code are most likely an error due to incomplete refactoring. Such class members take up space in the code and can lead to confusion by readers. | ||
|
||
## Rule Details | ||
|
||
A private class member `#foo` is considered to be used if any of the following are true: | ||
|
||
* It is called (`this.#foo()`) | ||
* It is read (`this.#foo`) | ||
* It is passed into a function as an argument (`doSomething(this.#foo)`) | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
/*eslint no-unused-private-class-members: "error"*/ | ||
class Foo { | ||
#unusedMember = 5; | ||
} | ||
|
||
class Foo { | ||
#usedOnlyInWrite = 5; | ||
method() { | ||
this.#usedOnlyInWrite = 42; | ||
} | ||
} | ||
|
||
class Foo { | ||
#unusedMethod() {} | ||
} | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```js | ||
/*eslint no-unused-private-class-members: "error"*/ | ||
|
||
class Foo { | ||
#usedMember = 42; | ||
method() { | ||
return this.#usedMember; | ||
} | ||
} | ||
|
||
class Foo { | ||
#usedMethod() { | ||
return 42; | ||
} | ||
anotherMethod() { | ||
return this.#usedMethod(); | ||
} | ||
} | ||
``` | ||
|
||
## 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/** | ||
* @fileoverview Rule to flag declared but unused private class members | ||
* @author Tim van der Lippe | ||
*/ | ||
|
||
"use strict"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Rule Definition | ||
//------------------------------------------------------------------------------ | ||
|
||
module.exports = { | ||
meta: { | ||
type: "problem", | ||
|
||
docs: { | ||
description: "disallow unused private class members", | ||
category: "Variables", | ||
recommended: true, | ||
url: "https://eslint.org/docs/rules/no-unused-private-class-members" | ||
}, | ||
|
||
schema: [], | ||
|
||
messages: { | ||
unusedPrivateClassMember: "'{{classMemberName}}' is defined but never used." | ||
} | ||
}, | ||
|
||
create(context) { | ||
|
||
const declaredAndUnusedPrivateMembers = new Map(); | ||
|
||
//-------------------------------------------------------------------------- | ||
// Public | ||
//-------------------------------------------------------------------------- | ||
|
||
return { | ||
|
||
// Collect all declared members up front and assume they are all unused | ||
ClassBody(classBodyNode) { | ||
for (const bodyMember of classBodyNode.body) { | ||
if (bodyMember.type === "PropertyDefinition" || bodyMember.type === "MethodDefinition") { | ||
if (bodyMember.key.type === "PrivateIdentifier") { | ||
declaredAndUnusedPrivateMembers.set(bodyMember.key.name, bodyMember); | ||
} | ||
} | ||
} | ||
}, | ||
|
||
/* | ||
* Process all usages of the private identifier and remove a member from | ||
* `declaredAndUnusedPrivateMembers` if we deem it used. | ||
*/ | ||
PrivateIdentifier(privateIdentifierNode) { | ||
if ( | ||
|
||
// The definition of the class member itself | ||
privateIdentifierNode.parent.type !== "PropertyDefinition" && | ||
privateIdentifierNode.parent.type !== "MethodDefinition" && | ||
|
||
// Any usage of this member, except for writes (if it is a property) | ||
privateIdentifierNode.parent.parent.type !== "AssignmentExpression") { | ||
|
||
declaredAndUnusedPrivateMembers.delete(privateIdentifierNode.name); | ||
} | ||
}, | ||
|
||
/* | ||
* 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"() { | ||
for (const [classMemberName, remainingDeclaredMember] of declaredAndUnusedPrivateMembers.entries()) { | ||
context.report({ | ||
node: remainingDeclaredMember, | ||
messageId: "unusedPrivateClassMember", | ||
data: { | ||
classMemberName | ||
} | ||
}); | ||
} | ||
declaredAndUnusedPrivateMembers.clear(); | ||
} | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/** | ||
* @fileoverview Tests for no-unused-private-class-members rule. | ||
* @author Tim van der Lippe | ||
*/ | ||
|
||
"use strict"; | ||
|
||
//------------------------------------------------------------------------------ | ||
// Requirements | ||
//------------------------------------------------------------------------------ | ||
|
||
const rule = require("../../../lib/rules/no-unused-private-class-members"), | ||
{ RuleTester } = require("../../../lib/rule-tester"); | ||
|
||
//------------------------------------------------------------------------------ | ||
// Tests | ||
//------------------------------------------------------------------------------ | ||
|
||
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2022 } }); | ||
|
||
/** | ||
* Returns an expected error for defined-but-not-used private class member. | ||
* @param {string} classMemberName The name of the class member | ||
* @returns {Object} An expected error object | ||
*/ | ||
function definedError(classMemberName) { | ||
return { | ||
messageId: "unusedPrivateClassMember", | ||
data: { | ||
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; | ||
method() { | ||
return someGlobalMethod(this.#usedMember); | ||
} | ||
}`, | ||
|
||
//-------------------------------------------------------------------------- | ||
// Method definitions | ||
//-------------------------------------------------------------------------- | ||
` | ||
class Foo { | ||
#usedMethod() { | ||
return 42; | ||
} | ||
anotherMethod() { | ||
return this.#usedMethod(); | ||
} | ||
}` | ||
], | ||
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")] | ||
}, | ||
|
||
//-------------------------------------------------------------------------- | ||
// Unused method definitions | ||
//-------------------------------------------------------------------------- | ||
{ | ||
code: `class Foo { | ||
#unusedMethod() {} | ||
}`, | ||
errors: [definedError("unusedMethod")] | ||
}, | ||
{ | ||
code: `class Foo { | ||
#unusedMethod() {} | ||
#usedMethod() { | ||
return 42; | ||
} | ||
publicMethod() { | ||
return this.#usedMethod(); | ||
} | ||
}`, | ||
errors: [definedError("unusedMethod")] | ||
} | ||
] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters