-
-
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
feat(eslint-plugin): [prefer-optional-chain] handle cases where the first operands are unrelated to the rest of the chain and add type info #6397
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 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. |
This is now "check complete" - all existing tests are passing as are the new tests. This is going to be a big chunk of the logic to figure out the correct places to put an optional chain 🤔 |
@typescript-eslint/triage-team - this is functionally complete in terms of raw logic.
In some future state I can definitely see us including:
But I don't know if it's worth "holding up" this change to re-implement. Happy to do it if we think it's necessary, if not then we can review and land this as is. |
😞 I do dislike removing functionality. Maybe we could put this out as an experimental rule without deleting the existing one? |
That would create a worse state, IMO - as we would have two nearly identical rules that are mostly differentiated by having a fixer or not. What would be the value prop for a user to switch to test the rule? We'd need a breaking change to remove the experimental rule name. Also "experimental" implies instability or that it may not be adopted - but the rule is perfectly stable (passes all the existing tests, and more) and will be adopted - it just doesn't have the previously available (unsafe) fixer. |
Yeah that's a good point. Heh, I'm torn here. Expecting previously (perhaps slightly incorrect) satisfied users to be annoyed by the lack of autofixer. Given that the autofixer today is unsafe, I think it's reasonable for us to say "we've rewritten and removed it for safety - welcoming community PRs to add it back". |
It's really sad to see the fixer being removed, my team absolutely loved it when I turned the rule on since we worked on a code-base full of partial objects (and different patterns to deal with them). |
...maybe we could expose the old one under a name like "deprecated"? |
Also applies to The bigger issue is the conversion of return types - Honestly I'd be happy with having a flag like |
Fun edge case I just discovered: declare const foo: { bar: number } | null;
foo === null || foo.bar;
foo !== null && !foo.bar; In the first case Which means that my code will ignore it and try not to include it in the chain to report and fix. Will need to add special case logic to handle this as it's obviously intended to be chained together. This also opens up another question about how this rule functions (that we can probably punt on for now, but worth mentioning). foo && foo.bar && foo.bar.baz === 1
// rule will currently fix to
foo?.bar && foo.bar.baz === 1 Should the rule inspect through these "invalid" checks? Or should it ignore them entirely?
|
...hmm. My instinct is to ignore "invalid" checks as an odd edge case. If users complain we can add it as a bugfix later on - but if the code is roughly not valid, I'd hesitate to feel confident in making changes. |
Note: I hate the tests with a passion because it's impossible for anyone to understand them as written without checking out and running the test 😢 Snapshot output would make it much simpler to review as you can see the expected reports and fixes properly rather than doing it opaquely. The test coverage should show how much is covered overall - which should be all of the code! |
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 really impressive code. Seriously awesome PR @bradzacher!
I've gone over it a few times and think I generally understand it. Particular ups on the approach of separating out the collection and analysis bits.
I say let's get this in for v6!
packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts
Outdated
Show resolved
Hide resolved
packages/eslint-plugin/src/rules/prefer-optional-chain-utils/analyzeChain.ts
Outdated
Show resolved
Hide resolved
// type parameters are considered | ||
'foo.bar<a>() && foo.bar<a, b>().baz;', |
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.
type parameters
were renamed to type arguments in this context, 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.
}, | ||
], | ||
}, | ||
// type parameters are considered |
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.
Same
PR Checklist
prefer-optional-chain
] handle cases where the first logical(s) are unrelated to the proceeding logicals #6332, fixes [prefer-optional-chain] Only checks the first occurrence in conditional #1461, fixes [eslint-plugin] Breaking: enable type information for prefer-optional-chain #4820, fixes Enhancement: [prefer-optional-chain] Suggest optional chain when the start of the expression is already optionally chained #5697Overview
This is a big refactor of the lint rule so that it can be MUCH more robust and handle more cases.
The new algorithm is based off of flattening the logical expression to an array and then iterating over it. This new algorithm allows us to handle cases like this:
The new algorithm also includes a beefy recursive node comparison function which clearly compares two nodes semantically, instead of comparing them loosely based on source code text. This should lead to fewer weird cases, and also allows us to handle cases like this:
The TL;DR of the algorithm:
foo && foo.bar && foo.bar.baz --> [foo, foo.bar, foo.bar.baz]
foo != null && foo.bar !== undefined && foo.bar.baz --> [foo<NotEqualNullOrUndefined>, foo.bar<NotStrictEqualUndefined>, foo.bar.baz<Boolean>]
foo !== null && foo !== undefined
and group those as one operandfoo.bar
is a subset offoo.bar.baz.bam
, but not offoo.baz
)TODO:
foo?.bar && foo.bar
getComparedName
)&&
and||
conditionals separately (analyzeAndChain
andanalyzeOrChain
respectively)