Skip to content

Commit 0c0bf82

Browse files
authored
feat(linter): handle arrays of Promises in noFloatingPromises (#6512)
1 parent c5ee385 commit 0c0bf82

File tree

13 files changed

+720
-171
lines changed

13 files changed

+720
-171
lines changed

.changeset/angry-asses-attack.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@biomejs/biome": minor
3+
---
4+
5+
The rule [`noFloatingPromises`](https://biomejs.dev/linter/rules/no-misused-promises/)
6+
can now detect floating arrays of `Promise`s.
7+
8+
## Invalid examples
9+
10+
```ts
11+
// This gets flagged because the Promises are not handled.
12+
[1, 2, 3].map(async (x) => x + 1);
13+
```
14+
15+
## Valid examples
16+
17+
```ts
18+
await Promise.all([1, 2, 3].map(async (x) => x + 1));
19+
```

crates/biome_js_analyze/src/lint/nursery/no_floating_promises.rs

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use biome_analyze::{
33
};
44
use biome_console::markup;
55
use biome_js_factory::make;
6-
use biome_js_syntax::{AnyJsExpression, JsExpressionStatement, JsSyntaxKind};
6+
use biome_js_syntax::{
7+
AnyJsCallArgument, AnyJsExpression, AnyJsName, JsExpressionStatement, JsSyntaxKind, T,
8+
};
79
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, TriviaPieceKind};
810

911
use crate::{JsRuleAction, ast_utils::is_in_async_function, services::typed::Typed};
@@ -164,9 +166,14 @@ declare_lint_rule! {
164166
}
165167
}
166168

169+
pub enum NoFloatingPromisesState {
170+
ArrayOfPromises,
171+
UnhandledPromise,
172+
}
173+
167174
impl Rule for NoFloatingPromises {
168175
type Query = Typed<JsExpressionStatement>;
169-
type State = ();
176+
type State = NoFloatingPromisesState;
170177
type Signals = Option<Self::State>;
171178
type Options = ();
172179

@@ -177,6 +184,10 @@ impl Rule for NoFloatingPromises {
177184

178185
// Uncomment the following line for debugging convenience:
179186
//let printed = format!("type of {expression:?} = {ty:?}");
187+
if ty.is_array_of(|ty| ty.is_promise_instance()) {
188+
return Some(NoFloatingPromisesState::ArrayOfPromises);
189+
}
190+
180191
let is_maybe_promise =
181192
ty.is_promise_instance() || ty.has_variant(|ty| ty.is_promise_instance());
182193
if !is_maybe_promise {
@@ -187,26 +198,48 @@ impl Rule for NoFloatingPromises {
187198
return None;
188199
}
189200

190-
Some(())
201+
Some(NoFloatingPromisesState::UnhandledPromise)
191202
}
192203

193-
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
204+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
194205
let node = ctx.query();
195-
Some(
196-
RuleDiagnostic::new(
197-
rule_category!(),
198-
node.range(),
199-
markup! {
200-
"A \"floating\" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior."
201-
},
202-
)
203-
.note(markup! {
204-
"This happens when a Promise is not awaited, lacks a `.catch` or `.then` rejection handler, or is not explicitly ignored using the `void` operator."
205-
})
206-
)
206+
match state {
207+
NoFloatingPromisesState::ArrayOfPromises => Some(
208+
RuleDiagnostic::new(
209+
rule_category!(),
210+
node.range(),
211+
markup! {
212+
"An array of Promises was found, meaning they are not "
213+
"properly handled and could lead to ignored errors or "
214+
"unexpected behavior."
215+
},
216+
)
217+
.note(markup! {
218+
"This happens when an array of Promises is not wrapped "
219+
"with Promise.all() or a similar method, and is not "
220+
"explicitly ignored using the `void` operator."
221+
}),
222+
),
223+
NoFloatingPromisesState::UnhandledPromise => Some(
224+
RuleDiagnostic::new(
225+
rule_category!(),
226+
node.range(),
227+
markup! {
228+
"A \"floating\" Promise was found, meaning it is not "
229+
"properly handled and could lead to ignored errors or "
230+
"unexpected behavior."
231+
},
232+
)
233+
.note(markup! {
234+
"This happens when a Promise is not awaited, lacks a "
235+
"`.catch` or `.then` rejection handler, or is not "
236+
"explicitly ignored using the `void` operator."
237+
}),
238+
),
239+
}
207240
}
208241

209-
fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
242+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
210243
let node = ctx.query();
211244

212245
if !is_in_async_function(node.syntax()) {
@@ -215,19 +248,66 @@ impl Rule for NoFloatingPromises {
215248

216249
let expression = node.expression().ok()?;
217250
let mut mutation = ctx.root().begin();
218-
let await_expression = AnyJsExpression::JsAwaitExpression(make::js_await_expression(
219-
make::token(JsSyntaxKind::AWAIT_KW)
220-
.with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
221-
expression.clone().trim_leading_trivia()?,
222-
));
251+
match state {
252+
NoFloatingPromisesState::ArrayOfPromises => {
253+
let callee_expression =
254+
AnyJsExpression::JsStaticMemberExpression(make::js_static_member_expression(
255+
AnyJsExpression::JsIdentifierExpression(make::js_identifier_expression(
256+
make::js_reference_identifier(make::ident("Promise")),
257+
)),
258+
make::token(T![.]),
259+
AnyJsName::JsName(make::js_name(make::ident("all"))),
260+
));
261+
262+
let call_expression = AnyJsExpression::JsCallExpression(
263+
make::js_call_expression(
264+
callee_expression,
265+
make::js_call_arguments(
266+
make::token(T!['(']),
267+
make::js_call_argument_list(
268+
[AnyJsCallArgument::AnyJsExpression(
269+
expression.clone().trim_trivia()?,
270+
)],
271+
[],
272+
),
273+
make::token(T![')']),
274+
),
275+
)
276+
.build(),
277+
);
278+
279+
let await_expression =
280+
AnyJsExpression::JsAwaitExpression(make::js_await_expression(
281+
make::token(JsSyntaxKind::AWAIT_KW)
282+
.with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
283+
call_expression,
284+
));
223285

224-
mutation.replace_node(expression, await_expression);
225-
Some(JsRuleAction::new(
226-
ctx.metadata().action_category(ctx.category(), ctx.group()),
227-
ctx.metadata().applicability(),
228-
markup! { "Add await operator." }.to_owned(),
229-
mutation,
230-
))
286+
mutation.replace_node(expression, await_expression);
287+
Some(JsRuleAction::new(
288+
ctx.metadata().action_category(ctx.category(), ctx.group()),
289+
ctx.metadata().applicability(),
290+
markup! { "Wrap in Promise.all() and add await operator." }.to_owned(),
291+
mutation,
292+
))
293+
}
294+
NoFloatingPromisesState::UnhandledPromise => {
295+
let await_expression =
296+
AnyJsExpression::JsAwaitExpression(make::js_await_expression(
297+
make::token(JsSyntaxKind::AWAIT_KW)
298+
.with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
299+
expression.clone().trim_leading_trivia()?,
300+
));
301+
302+
mutation.replace_node(expression, await_expression);
303+
Some(JsRuleAction::new(
304+
ctx.metadata().action_category(ctx.category(), ctx.group()),
305+
ctx.metadata().applicability(),
306+
markup! { "Add await operator." }.to_owned(),
307+
mutation,
308+
))
309+
}
310+
}
231311
}
232312
}
233313

crates/biome_js_analyze/src/lint/nursery/no_misused_promises.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ declare_lint_rule! {
9494

9595
pub enum NoMisusedPromisesState {
9696
Conditional,
97-
ExpectedConditionalReturn,
98-
ExpectedVoidReturn,
97+
ConditionalReturn,
9998
Spread,
99+
VoidReturn,
100100
}
101101

102102
impl Rule for NoMisusedPromises {
@@ -133,7 +133,7 @@ impl Rule for NoMisusedPromises {
133133
"You may have intended to `await` the Promise instead."
134134
}),
135135
),
136-
NoMisusedPromisesState::ExpectedConditionalReturn => Some(
136+
NoMisusedPromisesState::ConditionalReturn => Some(
137137
RuleDiagnostic::new(
138138
rule_category!(),
139139
node.range(),
@@ -150,7 +150,7 @@ impl Rule for NoMisusedPromises {
150150
"does not work inside a synchronous callback."
151151
}),
152152
),
153-
NoMisusedPromisesState::ExpectedVoidReturn => Some(
153+
NoMisusedPromisesState::VoidReturn => Some(
154154
RuleDiagnostic::new(
155155
rule_category!(),
156156
node.range(),
@@ -182,7 +182,7 @@ impl Rule for NoMisusedPromises {
182182

183183
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
184184
use NoMisusedPromisesState::*;
185-
if matches!(state, ExpectedConditionalReturn | ExpectedVoidReturn) {
185+
if matches!(state, ConditionalReturn | VoidReturn) {
186186
return None; // These cannot be automatically fixed.
187187
}
188188

@@ -296,9 +296,9 @@ fn find_misused_promise_returning_callback(
296296
};
297297

298298
if argument_ty.is_function_with_return_type(|ty| ty.is_conditional()) {
299-
Some(NoMisusedPromisesState::ExpectedConditionalReturn)
299+
Some(NoMisusedPromisesState::ConditionalReturn)
300300
} else if argument_ty.is_function_with_return_type(|ty| ty.is_void()) {
301-
Some(NoMisusedPromisesState::ExpectedVoidReturn)
301+
Some(NoMisusedPromisesState::VoidReturn)
302302
} else {
303303
None
304304
}

crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalid.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,9 @@ function returnMaybePromise(): Promise<void> | undefined {
345345
}
346346

347347
returnMaybePromise();
348+
349+
[1, 2, 3].map(async (x) => x + 1);
350+
351+
async function floatingArray() {
352+
[1, 2, 3].map((x) => Promise.resolve(x + 1));
353+
}

crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalid.ts.snap

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,12 @@ function returnMaybePromise(): Promise<void> | undefined {
352352
353353
returnMaybePromise();
354354
355+
[1, 2, 3].map(async (x) => x + 1);
356+
357+
async function floatingArray() {
358+
[1, 2, 3].map((x) => Promise.resolve(x + 1));
359+
}
360+
355361
```
356362
357363
# Diagnostics
@@ -1631,8 +1637,46 @@ invalid.ts:347:1 lint/nursery/noFloatingPromises ━━━━━━━━━━
16311637
> 347 │ returnMaybePromise();
16321638
│ ^^^^^^^^^^^^^^^^^^^^^
16331639
348 │
1640+
349 │ [1, 2, 3].map(async (x) => x + 1);
16341641
16351642
i This happens when a Promise is not awaited, lacks a `.catch` or `.then` rejection handler, or is not explicitly ignored using the `void` operator.
16361643
16371644
16381645
```
1646+
1647+
```
1648+
invalid.ts:349:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1649+
1650+
i An array of Promises was found, meaning they are not properly handled and could lead to ignored errors or unexpected behavior.
1651+
1652+
347 │ returnMaybePromise();
1653+
348 │
1654+
> 349 │ [1, 2, 3].map(async (x) => x + 1);
1655+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1656+
350 │
1657+
351 │ async function floatingArray() {
1658+
1659+
i This happens when an array of Promises is not wrapped with Promise.all() or a similar method, and is not explicitly ignored using the `void` operator.
1660+
1661+
1662+
```
1663+
1664+
```
1665+
invalid.ts:352:2 lint/nursery/noFloatingPromises FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1666+
1667+
i An array of Promises was found, meaning they are not properly handled and could lead to ignored errors or unexpected behavior.
1668+
1669+
351async function floatingArray() {
1670+
> 352 │ [1, 2, 3].map((x) => Promise.resolve(x + 1));
1671+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1672+
353 │ }
1673+
354
1674+
1675+
i This happens when an array of Promises is not wrapped with Promise.all() or a similar method, and is not explicitly ignored using the `void` operator.
1676+
1677+
i Unsafe fix: Wrap in Promise.all() and add await operator.
1678+
1679+
352 │ → await·Promise.all([123].map((x=>·Promise.resolve(x·+·1)));
1680+
++++++++++++++++++ +
1681+
1682+
```

crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/valid.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,9 @@ async function testDestructuringAndCallingReturnsPromiseFromRest({
248248
}: Props) {
249249
rest.returnsPromise().catch(() => {}).finally(() => {});
250250
}
251+
252+
void [1, 2, 3].map(async (x) => x + 1);
253+
254+
async function floatingArray() {
255+
await Promise.all([1, 2, 3].map((x) => Promise.resolve(x + 1)));
256+
}

crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/valid.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,10 @@ async function testDestructuringAndCallingReturnsPromiseFromRest({
255255
rest.returnsPromise().catch(() => {}).finally(() => {});
256256
}
257257
258+
void [1, 2, 3].map(async (x) => x + 1);
259+
260+
async function floatingArray() {
261+
await Promise.all([1, 2, 3].map((x) => Promise.resolve(x + 1)));
262+
}
263+
258264
```

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy