Skip to content

Commit 20720d7

Browse files
committed
support promise with global identifier
1 parent 78529d6 commit 20720d7

File tree

3 files changed

+95
-39
lines changed

3 files changed

+95
-39
lines changed

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

+49-38
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use biome_console::markup;
55
use biome_js_factory::make;
66
use biome_js_semantic::SemanticModel;
77
use biome_js_syntax::{
8-
binding_ext::AnyJsBindingDeclaration, AnyJsExpression, AnyTsType, JsArrowFunctionExpression,
9-
JsCallExpression, JsExpressionStatement, JsFunctionDeclaration, JsIdentifierExpression,
10-
JsMethodClassMember, JsMethodObjectMember, JsStaticMemberExpression, JsSyntaxKind,
11-
JsVariableDeclarator, TsReturnTypeAnnotation,
8+
binding_ext::AnyJsBindingDeclaration, global_identifier, AnyJsExpression, AnyTsType,
9+
JsArrowFunctionExpression, JsCallExpression, JsExpressionStatement, JsFunctionDeclaration,
10+
JsIdentifierExpression, JsMethodClassMember, JsMethodObjectMember, JsStaticMemberExpression,
11+
JsSyntaxKind, JsVariableDeclarator, TsReturnTypeAnnotation,
1212
};
1313
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, SyntaxNodeCast, TriviaPieceKind};
1414

@@ -376,6 +376,8 @@ fn is_handled_promise(js_call_expression: &JsCallExpression) -> Option<bool> {
376376
/// async function returnsPromise(): Promise<void> {}
377377
///
378378
/// returnsPromise().then(() => null).catch(() => {});
379+
///
380+
/// globalThis.Promise.reject('value').finally();
379381
/// ```
380382
///
381383
/// Example TypeScript code that would return `false`:
@@ -389,46 +391,23 @@ fn is_member_expression_callee_a_promise(
389391
model: &SemanticModel,
390392
) -> Option<bool> {
391393
let expr = static_member_expr.object().ok()?;
394+
395+
if is_expression_an_promise(&expr) {
396+
return Some(true);
397+
}
398+
392399
match expr {
393400
AnyJsExpression::JsCallExpression(js_call_expr) => {
394401
let callee = js_call_expr.callee().ok()?;
395402
is_callee_a_promise(&callee, model)
396403
}
397-
AnyJsExpression::JsIdentifierExpression(js_ident_expr) => Some(
398-
is_expression_an_promise(&js_ident_expr).unwrap_or_default()
399-
|| is_binding_a_promise(&js_ident_expr, model).unwrap_or_default(),
400-
),
404+
AnyJsExpression::JsIdentifierExpression(js_ident_expr) => {
405+
is_binding_a_promise(&js_ident_expr, model)
406+
}
401407
_ => Some(false),
402408
}
403409
}
404410

405-
/// Checks if a `JsIdentifierExpression` represents a `Promise`.
406-
///
407-
/// This function inspects a given `JsIdentifierExpression` to determine if it represents a `Promise`.
408-
/// It returns `Some(true)` if the identifier is `Promise`, `Some(false)` if it is not, and `None` if there is an error in the process.
409-
///
410-
/// # Arguments
411-
///
412-
/// * `js_ident_expr` - A reference to a `JsIdentifierExpression` to check.
413-
///
414-
/// # Returns
415-
///
416-
/// * `Some(true)` if the identifier is `Promise`.
417-
/// * `Some(false)` if the identifier is not `Promise`.
418-
/// * `None` if there is an error in the process.
419-
///
420-
/// # Examples
421-
///
422-
/// Example TypeScript code that would return `Some(true)`:
423-
/// ```typescript
424-
/// Promise.resolve('value').then(() => { })
425-
/// Promise.all([p1, p2, p3])
426-
/// ```
427-
fn is_expression_an_promise(js_ident_expr: &JsIdentifierExpression) -> Option<bool> {
428-
let js_reference_identifier = js_ident_expr.name().ok()?;
429-
Some(js_reference_identifier.has_name("Promise"))
430-
}
431-
432411
/// Checks if the given `JsExpressionStatement` is within an async function.
433412
///
434413
/// This function traverses up the syntax tree from the given expression node
@@ -495,6 +474,8 @@ fn is_in_async_function(node: &JsExpressionStatement) -> bool {
495474
/// }
496475
///
497476
/// const promise = new Promise((resolve) => resolve('value'));
477+
///
478+
/// const promiseWithGlobalIdentifier = new window.Promise((resolve, reject) => resolve('value'));
498479
/// ```
499480
fn is_variable_initializer_a_promise(
500481
js_variable_declarator: &JsVariableDeclarator,
@@ -513,9 +494,7 @@ fn is_variable_initializer_a_promise(
513494
),
514495
AnyJsExpression::JsNewExpression(js_new_epr) => {
515496
let any_js_expr = js_new_epr.callee().ok()?;
516-
let js_ident_expr = any_js_expr.as_js_identifier_expression()?;
517-
let reference = js_ident_expr.name().ok()?;
518-
Some(reference.has_name("Promise"))
497+
Some(is_expression_an_promise(&any_js_expr))
519498
}
520499
_ => Some(false),
521500
}
@@ -569,3 +548,35 @@ fn is_variable_annotation_a_promise(js_variable_declarator: &JsVariableDeclarato
569548
_ => Some(false),
570549
}
571550
}
551+
552+
/// Checks if an expression is a `Promise`.
553+
///
554+
/// This function inspects a given `AnyJsExpression` to determine if it represents a `Promise`,
555+
/// either as a global identifier (e.g., `window.Promise`) or directly (e.g., `Promise.resolve`).
556+
/// It returns `true` if the expression is a `Promise`, otherwise `false`.
557+
///
558+
/// # Arguments
559+
///
560+
/// * `expr` - A reference to an `AnyJsExpression` to check.
561+
///
562+
/// # Returns
563+
///
564+
/// * `true` if the expression is a `Promise`.
565+
/// * `false` otherwise.
566+
///
567+
/// # Examples
568+
///
569+
/// Example TypeScript code that would return `true`:
570+
/// ```typescript
571+
/// window.Promise.resolve();
572+
/// globalThis.Promise.resolve();
573+
/// Promise.resolve('value').then(() => { });
574+
/// Promise.all([p1, p2, p3]);
575+
/// ```
576+
///
577+
fn is_expression_an_promise(expr: &AnyJsExpression) -> bool {
578+
if let Some((_, value)) = global_identifier(expr) {
579+
return value.text() == "Promise";
580+
}
581+
false
582+
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,9 @@ Promise.all([p1, p2, p3])
5757

5858
const promiseWithParentheses = (new Promise((resolve, reject) => resolve('value')));
5959
promiseWithParentheses;
60-
(returnsPromise());
60+
(returnsPromise());
61+
62+
63+
const promiseWithGlobalIdentifier = new window.Promise((resolve, reject) => resolve('value'));
64+
promiseWithGlobalIdentifier.then(() => { }).finally(() => { });
65+
globalThis.Promise.reject('value').finally();

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

+40
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ Promise.all([p1, p2, p3])
6464
const promiseWithParentheses = (new Promise((resolve, reject) => resolve('value')));
6565
promiseWithParentheses;
6666
(returnsPromise());
67+
68+
69+
const promiseWithGlobalIdentifier = new window.Promise((resolve, reject) => resolve('value'));
70+
promiseWithGlobalIdentifier.then(() => { }).finally(() => { });
71+
globalThis.Promise.reject('value').finally();
72+
6773
```
6874
6975
# Diagnostics
@@ -286,6 +292,7 @@ invalid.ts:59:1 lint/nursery/noFloatingPromises ━━━━━━━━━━
286292
> 59 │ promiseWithParentheses;
287293
│ ^^^^^^^^^^^^^^^^^^^^^^^
288294
60 │ (returnsPromise());
295+
61 │
289296
290297
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.
291298
@@ -301,6 +308,39 @@ invalid.ts:60:1 lint/nursery/noFloatingPromises ━━━━━━━━━━
301308
59 │ promiseWithParentheses;
302309
> 60 │ (returnsPromise());
303310
│ ^^^^^^^^^^^^^^^^^^^
311+
61 │
312+
313+
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.
314+
315+
316+
```
317+
318+
```
319+
invalid.ts:64:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
320+
321+
! A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
322+
323+
63 │ const promiseWithGlobalIdentifier = new window.Promise((resolve, reject) => resolve('value'));
324+
> 64 │ promiseWithGlobalIdentifier.then(() => { }).finally(() => { });
325+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
326+
65 │ globalThis.Promise.reject('value').finally();
327+
66 │
328+
329+
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.
330+
331+
332+
```
333+
334+
```
335+
invalid.ts:65:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
336+
337+
! A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
338+
339+
63 │ const promiseWithGlobalIdentifier = new window.Promise((resolve, reject) => resolve('value'));
340+
64 │ promiseWithGlobalIdentifier.then(() => { }).finally(() => { });
341+
> 65 │ globalThis.Promise.reject('value').finally();
342+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
343+
66 │
304344
305345
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.
306346

0 commit comments

Comments
 (0)