-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
fix(eslint-plugin): [prefer-optional-chain] ignore check
option for most RHS of a chain
#11272
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
base: main
Are you sure you want to change the base?
fix(eslint-plugin): [prefer-optional-chain] ignore check
option for most RHS of a chain
#11272
Conversation
Thanks for the PR, @nayounsang! 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 project configuration. |
View your CI Pipeline Execution ↗ for commit baf82db
☁️ Nx Cloud last updated this comment at |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #11272 +/- ##
=======================================
Coverage 90.86% 90.86%
=======================================
Files 503 503
Lines 51036 51038 +2
Branches 8424 8422 -2
=======================================
+ Hits 46373 46375 +2
Misses 4648 4648
Partials 15 15
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
?.()
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 generally looks great! Just one big question on the implementation approach we want to take. Looking forward to hearing your thoughts!
if (options.checkBigInt === true) { | ||
allowedFlags |= ts.TypeFlags.BigIntLike; | ||
} | ||
|
||
if (options.checkVoid === true) { | ||
allowedFlags |= ts.TypeFlags.Void; | ||
} | ||
|
||
return types.every(t => isTypeFlagSet(t, allowedFlags)); | ||
} |
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'm wondering if this section should be flipped to only specify the flags that aren't checked instead of the ones that are?
For now, no tests fail if I switch the logic to the following, where we don't bother with explicit options for or handling of void
:
let flagsToExcludeFromCheck = 0;
if (options.checkAny !== true) {
flagsToExcludeFromCheck |= ts.TypeFlags.Any;
}
if (options.checkUnknown !== true) {
flagsToExcludeFromCheck |= ts.TypeFlags.Unknown;
}
if (options.checkString !== true) {
flagsToExcludeFromCheck |= ts.TypeFlags.StringLike;
}
if (options.checkNumber !== true) {
flagsToExcludeFromCheck |= ts.TypeFlags.NumberLike;
}
if (options.checkBoolean !== true) {
flagsToExcludeFromCheck |= ts.TypeFlags.BooleanLike;
}
if (options.checkBigInt !== true) {
flagsToExcludeFromCheck |= ts.TypeFlags.BigIntLike;
}
return !types.some(t => isTypeFlagSet(t, flagsToExcludeFromCheck));
Can we come up with some test cases that would
- demonstrate which approach is preferable
- solidify that approach rather than passing with either choice of logic
I haven't thought deeply about which logic makes more sense, so it would be lovely if you have time to give this some thought and justify with examples which way is preferable. Thanks!
For reference, I ask this because:
- prefer-nullish-coalescing does have the "only ignore specific types" approach, rather than "only check some hardcoded types plus allowed primitives" as in the existing implementation of this rule.
- the answer will determine whether creation of a new option is warranted
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 will think about it more as I work, but I will leave a quick comment first. The more immediate the communication, the better. Even if it's not accurate!
- I would recommend adding
void
options. My philosophy is that the more open source users can customize with options, the more use cases users can cover. (Adding options would likely require some additional documentation changes beyond the current commit.) - Since the defaults are given as true, it seems more natural to change to the suggested code. Even better, it prevents optional chaining from being skipped on unexpected types.
- I guess I need to consider side effects when
flagsToExcludeFromCheck = 0
.
let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object;
All tests will pass if flagsToExcludeFromCheck = 0
for now and I don't think there's a problem at all.(Due to 2), but something feels off because allowedFlags
has default value...
Perhaps the test case would be to determine what error/suggestion occurs depending on whether the option is turned off.
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 looked into it further and there is no problem logically even if the default value is 0.
And, I found a tc that causes an lint error even though I set all check options to false.
{
code: `
declare const foo: { x: () => { y: string } } | null;
foo && foo.x();
`,
options: [
{
checkAny: false,
checkBigInt: false,
checkBoolean: false,
checkNumber: false,
checkString: false,
checkUnknown: false,
checkVoid: false,
},
],
},
// --------------------------------
{
code: `
declare const foo: { x: { y: string } } | null;
foo && foo.x;
`,
options: [
{
checkAny: false,
checkBigInt: false,
checkBoolean: false,
checkNumber: false,
checkString: false,
checkUnknown: false,
checkVoid: false,
},
],
},
AssertionError: Should have no errors but had 1: [
{
ruleId: '@rule-tester/prefer-optional-chain',
severity: 2,
message: "Prefer using an optional chain expression instead, as it's more concise and easier to read.",
line: 3,
column: 1,
nodeType: null,
messageId: 'preferOptionalChain',
endLine: 3,
endColumn: 15,
suggestions: [ [Object] ]
}
]
In summary, this is a case for nested Objs. So the commit only adds one of the two tc
related commit: 4b92ad4
There are probably a few more, but I'll give them credit for the discovery.
Is there anything else I should know & work? @kirkwaiblinger
…ypescript-eslint into void-optional-chain
This PR has the effect that the following code is flagged (with declare const maybeVoid: void | { x: () => { some: 'object' } };
maybeVoid && maybeVoid.x(); I'm not sure that that's what we want? I'd say instead the goal is that a In other words, as far as implementation - we're not looking to include possibly- WDYT? |
I've realized — I think this is a much more general bug. The declare const x: { a: string };
// checkString: true
const y = x && x.a; // correctly reports
// checkString: false
const y = x && x.a; // (BUG) doesn't report So, if we generally fix the logic so that the type checking only applies to the LHS of a potential chain expression |
Then, I guess we can check it except for right most. I will finish the work on void and reply. |
After thinking about it further with comments and usage examples, I think the checkVoid option is not necessary. checkXXX is useful when there is an accessible method. When considering real-world usage, I couldn't think of a case where checkXXX would be used for void. |
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 moving in the right direction, but the current state of the PR seems to be mostly extraneous changes. Let's clean it up to be focused on the changes related to the bug. Thanks!
}, | ||
// Exclude for everything else, an error occurs | ||
{ | ||
code: noFormat`declare const foo: { x: { y: string } } | null; foo && foo.x;`, |
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 test anything related to formatting, so there's no reason to use noFormat
here. Let's remove it.
], | ||
options: [{ checkBoolean: false }], | ||
}, | ||
// Exclude for everything else, an error occurs |
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.
?
{ | ||
code: ` | ||
declare const foo: void; | ||
foo && foo(); | ||
`, | ||
errors: [{ messageId: 'preferOptionalChain' }], | ||
output: ` | ||
declare const foo: void; | ||
foo?.(); | ||
`, | ||
}, |
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 should not report (see #11272 (comment)). Please revert.
let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object; | ||
if (options.checkAny === true) { | ||
allowedFlags |= ts.TypeFlags.Any; | ||
let flagsToExcludeFromCheck = 0; | ||
if (options.checkAny !== true) { | ||
flagsToExcludeFromCheck |= ts.TypeFlags.Any; | ||
} | ||
if (options.checkUnknown === true) { | ||
allowedFlags |= ts.TypeFlags.Unknown; | ||
if (options.checkUnknown !== true) { | ||
flagsToExcludeFromCheck |= ts.TypeFlags.Unknown; | ||
} | ||
if (options.checkString === true) { | ||
allowedFlags |= ts.TypeFlags.StringLike; | ||
if (options.checkString !== true) { | ||
flagsToExcludeFromCheck |= ts.TypeFlags.StringLike; | ||
} | ||
if (options.checkNumber === true) { | ||
allowedFlags |= ts.TypeFlags.NumberLike; | ||
if (options.checkNumber !== true) { | ||
flagsToExcludeFromCheck |= ts.TypeFlags.NumberLike; | ||
} | ||
if (options.checkBoolean === true) { | ||
allowedFlags |= ts.TypeFlags.BooleanLike; | ||
if (options.checkBoolean !== true) { | ||
flagsToExcludeFromCheck |= ts.TypeFlags.BooleanLike; | ||
} | ||
if (options.checkBigInt === true) { | ||
allowedFlags |= ts.TypeFlags.BigIntLike; | ||
if (options.checkBigInt !== true) { | ||
flagsToExcludeFromCheck |= ts.TypeFlags.BigIntLike; | ||
} | ||
return types.every(t => isTypeFlagSet(t, allowedFlags)); | ||
|
||
return types.every(t => !isTypeFlagSet(t, flagsToExcludeFromCheck)); | ||
} |
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.
Please revert these changes, since they turn out not to be related to the necessary changes.
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.
(see review)
Oh, sorry. I misunderstood your review. Now I know what to do |
?.()
check
option for most RHS of a chain
PR Checklist
?.()
#11270Overview