-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
fix(eslint-plugin): [unbound-method] report on destructuring in function parameters #8952
Conversation
Thanks for the PR, @auvred! 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 on https://opencollective.com/typescript-eslint. |
✅ Deploy Preview for typescript-eslint ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
☁️ Nx Cloud ReportCI is running/has finished running commands for commit e7e3603. As they complete they will appear below. Click to see the status, the terminal output, and the build insights. 📂 See all runs for this CI Pipeline Execution ✅ Successfully ran 1 targetSent with 💌 from NxCloud. |
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 really like these changes! The algorithm change makes sense to me, the code is easy to follow, and the new code for type inspection is very thorough.
Added a few questions and some test cases to flesh out edge cases.
nativelyBoundMembers.has(getMemberFullName(node)) && | ||
isNotImported(objectSymbol, currentSourceFile) | ||
notImported && | ||
nativelyBoundMembers.has(`${object.name}.${property.name}`) |
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.
const { log } = console;
In our current configuration, the console.log
declarations are under @types/node
. So if we remove this check, the rule will report on this case. Not sure what's the best solution there..
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.
Are you saying, you'd like to get rid of the first half of this function and only have the part after the return
statement, but that that would cause bugs to do so? I'm not 100% following.
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.
you'd like to get rid of the first half of this function and only have the part after the
return
statement, but that that would cause bugs to do so?
Yup, exactly!
I'd prefer to use only type checking here for obvious reasons, as it is more flexible and robust than just checking identifier names.
But this type checking-based approach works by checking whether a method is defined in the default library. This can be a problem for us, especially in the case of console
methods.
Imagine the following situation:
// tsconfig.json
{
"compilerOptions": {
"lib": ["ESNext"],
"types": ["node"]
}
}
// index.ts
const { log } = console
The log
method of the Console
interface is defined in the node_modules/@types/node/console.d.ts
, not in the default TypeScript library. Therefore, the rule will false-positively report on this destructuring because it knows that this method is defined outside the default lib.
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.
Hmm. I'm not sure I have enough knowledge to know what to do here. Maybe someone from @typescript-eslint/triage-team will have a suggestion?
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 to make sure I understand: we're saying that:
console
'slog
is a known exception defined inlib.dom.d.ts
- A
log
override is defined in@types/node
' - We'd like to still allow the exception for
console
'slog
, even though type info thinks it's a@types/node
thing
...is that right?
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.
Yeah, that makes sense. So, I guess the plan would be as follows:
- Make exceptions only for known methods from the default TS library.
- Remove purely syntactic checks so that we only rely on type-level checks
- For now, treat types coming from
@types/*
as invalid and let the rule report them - Report invalid types on the DT side
Am i right?
I wonder if this would be too breaking a change for people, though. We'll make them update @types/node
to get rid of lint errors in Node.js projects.
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 would say that Report invalid types on the DT side should be done ASAP, so folks who keep up-to-date-ish dev dependencies are less likely to be impacted. And maybe delaying merging the change on our end till a couple of months after that? Would that be overkill?
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.
My opinion - keep the first check so we can merge without regression (it may not be "ideal" but it already exists). File DT issue, and put a link to it in code with an explanation that the first check is necessary due that issue.
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.
Oops, I just realized that Console
is an interface, and we can't rely on function signatures declared in interfaces, because classes implementing those interfaces can still access this
even if this: void
is declared in an interface:
interface Aaa {
unbound: (this: void) => void
}
class Foo implements Aaa {
data = 'string'
unbound() {
console.log(this.data) // runtime error!
}
}
// not reported by unbound-method rule
const { unbound } = {} as Aaa
unbound()
This is a bug in a rule. See #3503. The rule should rely on function signatures only when dealing with classes, not with interfaces.
So even if we ask DT to change the types on their side, we have to make exceptions for those types because they do not guarantee runtime safety.
So I think now we have two options from #8952 (comment):
We can use the current method: pure AST node name comparison - the
log
method ofconsole
variable was calledWe can try to do some heuristics based on the
.d.ts
file path: for example, if it includesnode_modules
or something like that
Personally, I'm in favor of the second option, because it covers more cases and does so more reliably. On the other hand, pure AST checks have been around for a long time and no one has complained about them yet.
What do you think of that?
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.
Similarly to my opinion in #8952 (comment), I think that Option 2 here is just out of scope for this issue, granted that existing checks are based on Option 1 -like behavior. So my preference would be to go with Option 1 and discuss Option 2 in a followup.
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.
A couple questions/commenting requests, but no significant concrete changes being requested at this time.
isBuiltinSymbolLike( | ||
services.program, | ||
services.getTypeAtLocation(object), | ||
[ |
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's the difference between this array literal and the SUPPORTED_GLOBALS
array? Do we need both? Perhaps giving it a good name will make it clear?
Thoughts?
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.
Do we need both?
I really don't know 🙁
For more reasoning on this, see #8952 (comment)
Perhaps giving it a good name will make it clear?
+1 👍 Will rename it
} | ||
|
||
const objectSymbol = services.getSymbolAtLocation(node.object); | ||
function isNativelyBound( |
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.
It's not super obvious how this function works. I cloned it down to play around with the tests, and I'm still not 100% sure, but my rough understanding is that the first part checks by identity whether something is a natively bound member, and the second part checks via the type system, in order to catch a more general set of cases where the builtin namespace objects are potentially renamed.
Could you add some comments to help the reader 🙏 ?
nativelyBoundMembers.has(getMemberFullName(node)) && | ||
isNotImported(objectSymbol, currentSourceFile) | ||
notImported && | ||
nativelyBoundMembers.has(`${object.name}.${property.name}`) |
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.
Are you saying, you'd like to get rid of the first half of this function and only have the part after the return
statement, but that that would cause bugs to do so? I'm not 100% following.
Co-authored-by: Kirk Waiblinger <kirk.waiblinger@gmail.com>
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8952 +/- ##
=======================================
Coverage 88.38% 88.39%
=======================================
Files 419 419
Lines 14620 14644 +24
Branches 4279 4288 +9
=======================================
+ Hits 12922 12944 +22
- Misses 1374 1376 +2
Partials 324 324
Flags with carried forward coverage won't be shown. Click here to find out more.
|
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 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.
So thorough!! 🙌
PR Checklist
Overview
Old approach: query for
VariableDeclarator, AssignmentExpression
and check if they haveObjectPattern
New approach: query for
ObjectPattern
, check ifVariableDeclarator, AssignmentExpression, AssignmentPattern
its parentWe're iterating over
ObjectPattern
's properties:ObjectPatterns
's parent has init node, check if it's safe to destructure this init node:ObjectPatterns
's parent isAssignmentPattern
:ObjectPattern
has a type annotation, then check if it's safe to destructure this type