[eslint] add rule to forbid async forEach bodies#111637
[eslint] add rule to forbid async forEach bodies#111637spalger merged 15 commits intoelastic:masterfrom
Conversation
cb2684a to
9fb9c19
Compare
...ns/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx
Outdated
Show resolved
Hide resolved
9fb9c19 to
ee60f73
Compare
49e8f46 to
74adce4
Compare
74adce4 to
20495b0
Compare
| ], | ||
|
|
||
| '@kbn/eslint/no_async_promise_body': 'error', | ||
| '@kbn/eslint/no_async_foreach': 'error', |
There was a problem hiding this comment.
I believe we should expand the rule to cover other expressions, like this one #98463 (comment)
I'm not sure whether no-floating-promises implements the same check, but we can give it a try.
There was a problem hiding this comment.
Yeah, I'll look at a similar solution for that. I don't want to overload this rule though and have a special error message for that scenario.
There was a problem hiding this comment.
I don't want to overload this rule though and have a special error message for that scenario.
Would you suggest implementing a custom rule for every use-case when (...args: any[]) => Promise<any> is used instead of (...args: any[]) => void?
There was a problem hiding this comment.
I do think that having custom error messages for each situation would be nice, though I'm not convinced there are that many scenarios right now. It's possibly a bigger more wide issue than I'm thinking though.
| return ( | ||
| node.callee.type === esTypes.MemberExpression && | ||
| node.callee.property.type === esTypes.Identifier && | ||
| node.callee.property.name === 'forEach' && |
There was a problem hiding this comment.
unfortunately, it doesn't cover cases when a promise is returned without an async keyword 😞
There was a problem hiding this comment.
Yeah, in that case we're creating a promise which can be handled, so I feel like it's less of an issue.
There was a problem hiding this comment.
Yeah, in that case we're creating a promise which can be handled
Could you elaborate on it? Sorry, I'm not follow who handles a promise in this case 😞
async function doAsync () {}
[].forEach(doAsync)There was a problem hiding this comment.
Ah, I figured you were describing:
array.forEach(() => new Promise((resolve) => ...))There's a chance we could improve this to use types for locally defined functions, but the more research I do on other things the more I feel like we need to start providing ESLint with full types... Which has a lot of complications.
YulNaumenko
left a comment
There was a problem hiding this comment.
Alerting changes LGTM!
|
Pinging @elastic/fleet (Team:Fleet) |
flash1293
left a comment
There was a problem hiding this comment.
The timelion one scares me a bit, but I think this comment here
is also applicable for reduce and there's not actually something to resolve.Tested and didn't notice any breakages, LGTM.
shahzad31
left a comment
There was a problem hiding this comment.
Uptime Changes Looks Good !!
cjcenizal
left a comment
There was a problem hiding this comment.
Changes to Rollup telemetry collector LGTM, but this code is really the domain of @elastic/kibana-vis-editors since it's about visualizations that use rollups.
x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts
Show resolved
Hide resolved
FrankHassanabad
left a comment
There was a problem hiding this comment.
LGTM,
- Looked over 1 section of code, left one comment
- Talked about it with the internal team.
💚 Build Succeeded
Metrics [docs]Async chunks
Page load bundle
History
To update your PR or re-run it, just comment with: |
thomasneirynck
left a comment
There was a problem hiding this comment.
maps/* changes lgtm
🙇 asyncMap, so much cleaner.
mshustov
left a comment
There was a problem hiding this comment.
Ok for the core changes. I don't want to block this PR. The core team can refactor the tests in @kbn/std later.
vadimkibana
left a comment
There was a problem hiding this comment.
bfetch code changes LGTM.
💔 Backport failed
To backport manually run: |
Co-authored-by: spalger <spalger@users.noreply.github.com> # Conflicts: # x-pack/plugins/event_log/server/es/init.ts
…2178) * [eslint] add rule to forbid async forEach bodies (#111637) Co-authored-by: spalger <spalger@users.noreply.github.com> # Conflicts: # x-pack/plugins/event_log/server/es/init.ts * remove unnecessary async forEach Co-authored-by: spalger <spalger@users.noreply.github.com>
I noticed an unresolved promise rejection on CI and tracked it back to a usage of
array.forEach()which was passed an async function. Like #110349 this creates hidden promises which can't be tracked, preventing the rejections from being handled. To prevent this mistake I've implemented an ESLint rule to prevent devs from accidentally using this pattern.Unlike in the other PR I can't think of a lowest-common-denominator fix to implement across the board, so I have instead fixed all the violations manually. The fixes use one of three new helpers added to
@kbn/std:asyncForEach(iter, fn): The most common replacement was to just replacearray.forEach()with a helper that does the same thing. This callsfnsynchronously for each item inarrayand waits for the returned promise/observables to resolve/reject/complete/error.asyncMap(iter, fn): A common usage of asyncforEach()was to add items to an array within scope. I have replaced those withasyncMap()which callsfnsynchronously for each item inarrayand waits for the returned promise/observables to resolve/reject/complete/error. The result array has each item in the order of the source array.map$(iter, fn): A simple wrapper around RxJS for creating a stream from an iterable value where each item in the result is the return value of calling the map function with each item from the iterable. This is the basis of the other functions.No need for
async: There were a handful of places where the function passed to.forEach()wasasyncunnecessarily.Use a library: In a few places we could just replace the existing logic with a dependency which is already available and does the intended logic without any compromises.
Each helper implemented in this PR includes a variant which accepts a limit parameter between the iterable and the map function. I'm interested in having a discussion about if we should be using these instead of starting each concurrent iteration at the same time, potentially creating a lot more load from a single request on the server, or doing a lot more work on each frame in the UI. It's also possible that several places where I updated code take input from users which are variable in length and could be very long. In those cases I think it would make sense to limit the amount of work we are doing concurrently... idk, I originally wrote the "unlimited" helpers to have a default concurrency of 4 but got sheepish. For now the
*WithLimit()versions are unused outside of the unlimited helpers, so if reviewers request I will remove them.I could also imagine us requiring usage of such helpers instead of doing
Promise.all(array.map())which we do in many, many places. It could still be possible with something likeasyncMapWithLimit(iter, Infinity, fn)but would require opting into this when the dev knows it won't lead to too much concurrency.