Skip to content

Commit 85ab50c

Browse files
committed
support promise with global identifier
1 parent 78529d6 commit 85ab50c

File tree

3 files changed

+92
-5
lines changed

3 files changed

+92
-5
lines changed

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

+46-4
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,6 +391,11 @@ 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_promise_with_global_identifier(&expr) {
396+
return Some(true);
397+
}
398+
392399
match expr {
393400
AnyJsExpression::JsCallExpression(js_call_expr) => {
394401
let callee = js_call_expr.callee().ok()?;
@@ -495,6 +502,8 @@ fn is_in_async_function(node: &JsExpressionStatement) -> bool {
495502
/// }
496503
///
497504
/// const promise = new Promise((resolve) => resolve('value'));
505+
///
506+
/// const promiseWithGlobalIdentifier = new window.Promise((resolve, reject) => resolve('value'));
498507
/// ```
499508
fn is_variable_initializer_a_promise(
500509
js_variable_declarator: &JsVariableDeclarator,
@@ -513,6 +522,9 @@ fn is_variable_initializer_a_promise(
513522
),
514523
AnyJsExpression::JsNewExpression(js_new_epr) => {
515524
let any_js_expr = js_new_epr.callee().ok()?;
525+
if is_promise_with_global_identifier(&any_js_expr) {
526+
return Some(true);
527+
}
516528
let js_ident_expr = any_js_expr.as_js_identifier_expression()?;
517529
let reference = js_ident_expr.name().ok()?;
518530
Some(reference.has_name("Promise"))
@@ -569,3 +581,33 @@ fn is_variable_annotation_a_promise(js_variable_declarator: &JsVariableDeclarato
569581
_ => Some(false),
570582
}
571583
}
584+
585+
/// Checks if an expression is a `Promise` with a global identifier.
586+
///
587+
/// This function inspects a given `AnyJsExpression` to determine if it represents a `Promise`
588+
/// with a global identifier, such as `window.Promise`. It returns `true` if the expression is a
589+
/// `Promise` with a global identifier, otherwise `false`.
590+
///
591+
/// # Arguments
592+
///
593+
/// * `expr` - A reference to an `AnyJsExpression` to check.
594+
///
595+
/// # Returns
596+
///
597+
/// * `true` if the expression is a `Promise` with a global identifier.
598+
/// * `false` otherwise.
599+
///
600+
/// # Examples
601+
///
602+
/// Example TypeScript code that would return `true`:
603+
/// ```typescript
604+
/// window.Promise.resolve();
605+
/// globalThis.Promise.resolve();
606+
/// ```
607+
///
608+
fn is_promise_with_global_identifier(expr: &AnyJsExpression) -> bool {
609+
if let Some((_, value)) = global_identifier(expr) {
610+
return value.text() == "Promise";
611+
}
612+
false
613+
}

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)