-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix(eslint-plugin): [no-unnecessary-condition] ignore optionally accessing index signatures #6762
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
Conversation
Thanks for the PR, @Josh-Cena! 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 settings. |
4ac41b6
to
fd1488a
Compare
function getElem(dict: Record<string, { foo: string }>, key: string) { | ||
if (dict[key]) { | ||
return dict[key].foo; | ||
} else { | ||
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 was arguably a bug fix in the existing test case, because it shouldn't be reported 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.
This actually should be reported because the fixture config doesn't use noUncheckedIndexAccess
!
I think we should probably guard the new code around whether or not the flag is on.
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.
@bradzacher This code is the exact same principle as dictionary[key] ??= defaultVal
. If we want to ignore that, we have to ignore others as well.
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.
They are and they aren't. You're correct in that they're both record index access in a logical position - so they're semantically the same case. However TS reports the types differently for the two cases.
In this instance TS will report the read type, meaning that if noUncheckedIndexAccess
is turned on then TS will report the type as T | undefimed
to us.
However in x ?? y
, TS will report the write type, meaning it reports just T
- no matter whether or not noUncheckedIndexAccess
is on or not.
This is the fundamental difference between the two cases and why we are false positiving on the assignment operators, but not on this case.
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.
However, we already ignore array index access, even without noUncheckedIndexAccess
. See this test case:
declare const arr: { foo: number }[];
const bar = arr[42]?.foo ?? 0;
I find it quite natural to extend the scope to all index signatures, since there isn't something special about array indices.
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.
The reason we did it for arrays and not objects is because defining arrays as (T | undefined)[]
is similar to Record<string, T | undefined>
, but far, far less ergonomic syntactically and is arguably worse given how common it is to iterate arrays directly using an iterator method or for/of.
For objects it's pretty natural to union in undefined
because most usecases for reading is an index signature - the direct iteration methods (Object.entries
/ Object.values
) are both rarer usecases so the undefined
doesn't get in the way as much.
Now that we have noUncheckedIndexAccess
- we don't really need the logic at all, but I understand that some people think the compiler option goes too far and can get in the way (which is true to a degree).
The motivation behind this change isn't bringing the two sets up to parity (that's a much larger change that we should discuss as a group to decide if we think we should treat objects the same as arrays in this regard) - instead it's just fixing the current false positives we have due to the logical operator handling we added in #6594
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #6762 +/- ##
=======================================
Coverage ? 87.30%
=======================================
Files ? 384
Lines ? 13148
Branches ? 3863
=======================================
Hits ? 11479
Misses ? 1302
Partials ? 367
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.
Thanks for getting straight on this!
Almost there I think!
function getElem(dict: Record<string, { foo: string }>, key: string) { | ||
if (dict[key]) { | ||
return dict[key].foo; | ||
} else { | ||
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 actually should be reported because the fixture config doesn't use noUncheckedIndexAccess
!
I think we should probably guard the new code around whether or not the flag is on.
* Update dependency upgrades - non-major * Require valid disables with explanations * Temporarily disable typescript-eslint rule Ref: typescript-eslint/typescript-eslint#6762 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Karl Horky <karl.horky@gmail.com>
👋 @Josh-Cena just checking in, is this still something you have time for? |
Yes! I'll get to it. Sorry for the wait. I still don't know what the best way to fix this is, because I feel like accounting for index signature optionality, even without the compiler option, should be acceptable. |
Hoping this will be available in the near-term future. I'd love to use this rule without having to use dozens of casts |
It seems like the TypeScript PR microsoft/TypeScript#54777 was merged, which closed the issue in the TypeScript repo: Does that mean that this PR is now obsolete, once the TS PR changes are released in a version of TypeScript (eg. maybe 5.2)? |
@karlhorky That PR changes the quick info of the write type so it excludes |
@bradzacher Sorry it's been a really long time and I think neither of us are up to speed on this now. Let's recap—what (potential) false-positives should we fix and what should we continue erroring on? Currently we ignore all |
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.
originally I was against us bringing arrays and objects into parity.
however given the state noUncheckedIndexAccess
is in esp in regards to ??=
- I shall acquiesce here and we can loosen the rule to just ignore really anything with an index signature.
A few comments but otherwise looking good.
if (checker.isTupleType(objType)) { | ||
// If indexing a tuple with a literal, the type will be sound | ||
return node.property.type !== AST_NODE_TYPES.Literal; | ||
} |
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 check is actually no longer sound with modern TS.
Tuples can be variadic now - (eg [T1, ...T2[]]
, [...T1[], T2]
, [T1, ...T2[], T3]
, [...T1[]]
) - so they're no more safe than an array.
I.e. for const foo: [T1, ...T2[]]
- foo[1]
is possibly undefined
} | ||
if (node.computed) { | ||
const indexType = getNodeType(node.property); | ||
if (isTypeAnyType(indexType)) { |
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.
question: do we want to handle the never
case here?
I think it's okay not to just figured I'd flag it for completeness sake
// No need to check anything about any | ||
return true; | ||
} | ||
if (isTypeFlagSet(indexType, ts.TypeFlags.NumberLike)) { |
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 doens't handle "nominal" number types here.
EG type T = number & { __brand: unknown }
would work in the signature but we would not match it here.
I think it's okay that we ignore it here - it's a super niche usecase.
Just commenting for completeness's sake.
if (isTypeFlagSet(indexType, ts.TypeFlags.NumberLike)) { | ||
return ( | ||
checker.getIndexTypeOfType(objType, ts.IndexKind.Number) !== | ||
undefined | ||
); | ||
} | ||
} |
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 also handle symbol
indexes as well.
They're super rare but it should just be one extra if
declare const obj: {[k: symbol]: string};
declare const key: symbol;
const res = obj[key];
res?.length // should not report
👋 ping @Josh-Cena, do you still have time for this one? |
Closing this PR as it's been stale for a while without activity. If anybody wants to drive it forward, please do post your own PR - and if you use this as a start, consider adding a co-author attribution at the end of your PR description. Thanks! 😊 |
PR Checklist
Overview