Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): [no-unused-private-class-members] new extension rule#10913
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Conversation
Thanks for the PR,@bradzacher! typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community. The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately. Thanks again! 🙏Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently onhttps://opencollective.com/typescript-eslint. |
netlifybot commentedMar 3, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
✅ Deploy Preview fortypescript-eslint ready!
To edit notification comments on pull requests, go to yourNetlify project configuration. |
nx-cloudbot commentedMar 3, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
|
Command | Status | Duration | Result |
---|---|---|---|
nx test eslint-plugin --coverage=false | ❌ Failed | 5m 23s | View ↗ |
nx run-many -t lint | ❌ Failed | 3m 25s | View ↗ |
nx test eslint-plugin | ❌ Failed | 4m 59s | View ↗ |
nx run-many -t typecheck | ✅ Succeeded | 2m 16s | View ↗ |
nx test typescript-estree --coverage=false | ✅ Succeeded | 2s | View ↗ |
nx run types:build | ✅ Succeeded | 5s | View ↗ |
nx test eslint-plugin-internal --coverage=false | ✅ Succeeded | 3s | View ↗ |
nx run integration-tests:test | ✅ Succeeded | 3s | View ↗ |
Additional runs (26) | ✅ Succeeded | ... | View ↗ |
☁️Nx Cloud last updated this comment at2025-08-10 05:47:58
UTC
View yourCI Pipeline Execution ↗ for commit34b77b8. ☁️Nx Cloud last updated this comment at |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
packages/eslint-plugin/src/rules/no-unused-private-class-members.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
packages/eslint-plugin/src/rules/no-unused-private-class-members.ts OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
// 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. | ||
trackedClassMembersUsed.add(memberDefinition.declaredNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
[Bug]: I've noticed some missed reports with functions that have a differentthis
than the class instance (deploy preview playground link):
classTest1{// should be reported but doesn't?privatebar:number;foo=function(){returnthis.bar;}}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This is because the base rule doesn't support that case becausethis.#bar
is a syntax error inside a function expression!!
It's worth noting that TS doesn't actually catch this case either and reportsbar
as unused because the type of thethis
in the function expression isany
!
If you turn onnoImplicitThis
then TS will error on thatthis
We could support this if we wanted to. I'm leaning towards not because TS itself doesn't catch it either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yes! I think the rule should reportbar
as unused in this case. I've mentioned this since TypeScript reports it as unused but the rule doesn't report it as unused (regardless of the type error).
PrivateIdentifier(node): void { | ||
processPrivateIdentifier(node); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
[Bug]: I think there's an inconsistency that creates false positives withstatic
private
properties (deploy preview playground link):
classTest1{publicstaticfoo(){returnTest1.#bar;}// correctly not reportedstatic #bar=1;}classTest2{publicstaticfoo(){returnTest2.bar;}// reported but shouldn't?privatestaticbar=1;}
I've noticed a similar false positive with the following (deploy preview playground link):
classTest1{publicfoo(a:Test1){returna.#bar;}// correctly not reported #bar=1;}classTest2{publicfoo(a:Test2){returna.bar;}// reported but shouldn't?privatebar=1;}
bradzacherMar 7, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
For your second example -- this is expected and is the same purposely not covered case as here:
https://github.com/typescript-eslint/typescript-eslint/pull/10913/files#diff-914330b2e3f9d71dfb628800b850412c3c5560900b13cf8a2925ff7af0905e53R50-R66
I made this rule purposely NOT use type info because the cases that type info enable are mostly going to be rarer edge cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This first example of static private member access we could handle though with scope analysis.
ronami left a comment• edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Looks amazing! Excited to enable this on projects I work on 🚀🚀🚀
(I accidentally posted the review earlier while still adding comments 🙈)
</TabItem> | ||
</Tabs> | ||
## Limitations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
What do you think about also mentioning assigningthis
to a variable as a limitaiton?
classTest1{privatebar:number;foo(){constself=this;returnfunction(){// ❌ NOT detected as a usageself.bar;}}}
FYI I'm going to make@Josh-Cena very happy - I'm going to build a "class scope analyser" so we can do this right rather than just hacking it in. This does mean that I'm going to rewrite the rule from the ground up -- but it'll be worht it |
kirkwaiblinger commentedMar 19, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Do you intend for this to proceed as-is, with this work intended to happen in the future, or should we pause/draft it pending this work? |
Let's draft it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Looks amazing and a very beneficial rule to have; just a few small comments from me 🚀🚀🚀
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Disallow unused private class members', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
What do you think about covering the following use-case? (deploy preview link)
classTest1{// should be reported but doesn't?constructor(privatefoo:number){}}
if ( | ||
upper != null && | ||
node.params[0].type === AST_NODE_TYPES.Identifier && | ||
node.params[0].name === 'this' | ||
) { | ||
const thisType = node.params[0].typeAnnotation?.typeAnnotation; | ||
if ( | ||
thisType?.type === AST_NODE_TYPES.TSTypeReference && | ||
thisType.typeName.type === AST_NODE_TYPES.Identifier | ||
) { | ||
const thisContext = upper.findClassScopeWithName( | ||
thisType.typeName.name, | ||
); | ||
if (thisContext != null) { | ||
super(scopeManager, upper, thisContext, false); | ||
return; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This doesn't seem to have a test, and it throws for a function declaration/expression with no arguments (deploy preview link):
functionfoo(){}
// TODO -- use scope analysis to do some basic type resolution to handle cases like: | ||
// ``` | ||
// class Foo { | ||
// private prop: number; | ||
// method(thing: Foo) { | ||
// // this references the private instance member but not via `this` so we can't see it | ||
// thing.prop = 1; | ||
// } | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Ironically, it seems like TypeScript incorrectly reports this as unused (playground link):
classFoo{privateprop:number;method(thing:Foo){thing.prop=1;}}
Seems like a bug though, unless I'm totally missing something 😅
Edit: Interestingly, seems like the assignment was the culprit, and the following isn't reported by TypeScript (playground link):
classFoo{privateprop:number;method(thing:Foo){returnthing.prop;}}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
also looks like my comment is wrong and I did actually add support for this case haha
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Disallow unused private class members', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I think this is a bug and shouldn't be reported (deploy preview playground link):
classTest1{// reported but shouldn't?privatefoo:number|null;publicbar(){this.foo??=1;}}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This is a weird case that isn't currently reported byno-unused-vars
but really should be.
It is reported by the baseno-unused-private-class-members
rule
kirkwaiblingerAug 27, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
There was just a recent eslint bug report about these; I guess it's intentional for no-unused-vars sincex ?? = y
readsx
first (equivalent tox != null ? x : (x = y)
) rather thanunconditionally assigning (x = x != null ? x : y
). Seeeslint/eslint#20029. But presumably the two rules should agree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Oh, but do note that these are reported by no-useless-assignment so 🤷♂️ 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
we should probably file a bug for NUPCM then so it matches NUV?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I have very little to add that Kirk and Ronami didn't already. Very exciting!! ⚡
} | ||
/** | ||
* Extracts the string name for a member. | ||
* @returns `null` if the name cannot be extracted due to it being a computed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
[Docs] Small grammar point:
* @returns`null`ifthenamecannotbeextractedduetoitbeingacomputed. | |
* @returns`null`ifthenamecannotbeextractedduetoitbeingcomputed. |
*/ | ||
export function extractNameForMember(node: MemberNode): [Key, string] | null { | ||
if (node.computed) { | ||
return extractComputedName(node.key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Just noting,#11006 has some other logic around computed keys. It might be useful to unify?
PR Checklist
Overview
This PR implements an extension rule for
no-unused-private-class-members
that introduces support forprivate
members.