Skip to content

Commit

Permalink
feat(lint): support arrow functions and assigned functions in noFloat…
Browse files Browse the repository at this point in the history
…ingPromises
  • Loading branch information
kaykdm committed Jan 24, 2025
1 parent d400d69 commit a4ee1e3
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 39 deletions.
142 changes: 104 additions & 38 deletions crates/biome_js_analyze/src/lint/nursery/no_floating_promises.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ use biome_js_factory::make;
use biome_js_semantic::SemanticModel;
use biome_js_syntax::{
binding_ext::AnyJsBindingDeclaration, AnyJsExpression, AnyJsName, AnyTsName, AnyTsReturnType,
AnyTsType, JsArrowFunctionExpression, JsCallExpression, JsExpressionStatement,
JsFunctionDeclaration, JsMethodClassMember, JsMethodObjectMember, JsStaticMemberExpression,
JsSyntaxKind, TsReturnTypeAnnotation,
AnyTsType, AnyTsVariableAnnotation, JsArrowFunctionExpression, JsCallExpression,
JsExpressionStatement, JsFunctionDeclaration, JsMethodClassMember, JsMethodObjectMember,
JsStaticMemberExpression, JsSyntaxKind, JsVariableDeclarator, TsReturnTypeAnnotation,
};
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, SyntaxNodeCast, TriviaPieceKind};

Expand Down Expand Up @@ -45,6 +45,15 @@ declare_lint_rule! {
/// returnsPromise().then(() => {});
/// ```
///
/// ```ts,expect_diagnostic
/// const returnsPromise = async (): Promise<string> => {
/// return 'value';
/// }
/// async function returnsPromiseInAsyncFunction() {
/// returnsPromise().then(() => {});
/// }
/// ```
///
/// ### Valid
///
/// ```ts
Expand Down Expand Up @@ -195,13 +204,16 @@ fn is_callee_a_promise(callee: &AnyJsExpression, model: &SemanticModel) -> bool
let Some(any_js_binding_decl) = binding.tree().declaration() else {
return false;
};

let AnyJsBindingDeclaration::JsFunctionDeclaration(func_decl) = any_js_binding_decl
else {
return false;
};

return is_function_a_promise(&func_decl);
match any_js_binding_decl {
AnyJsBindingDeclaration::JsFunctionDeclaration(func_decl) => {
return is_function_a_promise(&func_decl);
}
AnyJsBindingDeclaration::JsVariableDeclarator(js_var_decl) => {
return is_variable_initializer_a_promise(&js_var_decl)
|| is_variable_annotation_a_promise(&js_var_decl);
}
_ => {}
}
}
AnyJsExpression::JsStaticMemberExpression(static_member_expr) => {
return is_member_expression_callee_a_promise(static_member_expr, model);
Expand All @@ -212,7 +224,8 @@ fn is_callee_a_promise(callee: &AnyJsExpression, model: &SemanticModel) -> bool
}

fn is_function_a_promise(func_decl: &JsFunctionDeclaration) -> bool {
func_decl.async_token().is_some() || is_return_type_promise(func_decl.return_type_annotation())
func_decl.async_token().is_some()
|| is_return_type_a_promise(func_decl.return_type_annotation())
}

/// Checks if a TypeScript return type annotation is a `Promise`.
Expand Down Expand Up @@ -240,7 +253,7 @@ fn is_function_a_promise(func_decl: &JsFunctionDeclaration) -> bool {
/// ```typescript
/// function doesNotReturnPromise(): void {}
/// ```
fn is_return_type_promise(return_type: Option<TsReturnTypeAnnotation>) -> bool {
fn is_return_type_a_promise(return_type: Option<TsReturnTypeAnnotation>) -> bool {
return_type
.and_then(|ts_return_type_anno| ts_return_type_anno.ty().ok())
.and_then(|any_ts_return_type| match any_ts_return_type {
Expand Down Expand Up @@ -360,32 +373,7 @@ fn is_member_expression_callee_a_promise(
return false;
};

match callee {
AnyJsExpression::JsStaticMemberExpression(static_member_expr) => {
return is_member_expression_callee_a_promise(&static_member_expr, model);
}
AnyJsExpression::JsIdentifierExpression(ident_expr) => {
let Some(reference) = ident_expr.name().ok() else {
return false;
};
let Some(binding) = model.binding(&reference) else {
return false;
};

let Some(any_js_binding_decl) = binding.tree().declaration() else {
return false;
};

let AnyJsBindingDeclaration::JsFunctionDeclaration(func_decl) = any_js_binding_decl
else {
return false;
};
return is_function_a_promise(&func_decl);
}
_ => {}
}

false
is_callee_a_promise(&callee, model)
}

/// Checks if the given `JsExpressionStatement` is within an async function.
Expand Down Expand Up @@ -423,3 +411,81 @@ fn is_in_async_function(node: &JsExpressionStatement) -> bool {
})
.is_some()
}

/// Checks if the initializer of a `JsVariableDeclarator` is an async function.
///
/// Example TypeScript code that would return `true`:
///
/// ```typescript
/// const returnsPromise = async (): Promise<string> => {
/// return 'value';
/// }
///
/// const returnsPromise = async function (): Promise<string> {
/// return 'value'
/// }
/// ```
fn is_variable_initializer_a_promise(js_variable_declarator: &JsVariableDeclarator) -> bool {
let Some(initializer_clause) = &js_variable_declarator.initializer() else {
return false;
};
let Ok(expr) = initializer_clause.expression() else {
return false;
};
match expr {
AnyJsExpression::JsArrowFunctionExpression(arrow_func) => {
if arrow_func.async_token().is_some() {
return true;
}

return is_return_type_a_promise(arrow_func.return_type_annotation());
}
AnyJsExpression::JsFunctionExpression(func_expr) => {
if func_expr.async_token().is_some() {
return true;
}
return is_return_type_a_promise(func_expr.return_type_annotation());
}
_ => {}
}
false
}

/// Checks if a `JsVariableDeclarator` has a TypeScript type annotation of `Promise`.
///
///
/// Example TypeScript code that would return `true`:
/// ```typescript
/// const returnsPromise: () => Promise<string> = () => {
/// return Promise.resolve("value")
/// }
/// ```
fn is_variable_annotation_a_promise(js_variable_declarator: &JsVariableDeclarator) -> bool {
js_variable_declarator
.variable_annotation()
.and_then(|anno| match anno {
AnyTsVariableAnnotation::TsTypeAnnotation(type_anno) => Some(type_anno),
_ => None,
})
.and_then(|ts_type_anno| ts_type_anno.ty().ok())
.and_then(|any_ts_type| match any_ts_type {
AnyTsType::TsFunctionType(func_type) => {
func_type
.return_type()
.ok()
.and_then(|return_type| match return_type {
AnyTsReturnType::AnyTsType(AnyTsType::TsReferenceType(ref_type)) => {
ref_type.name().ok().map(|name| match name {
AnyTsName::JsReferenceIdentifier(identifier) => {
identifier.has_name("Promise")
}
_ => false,
})
}
_ => None,
})
}
_ => None,
})
.unwrap_or(false)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,25 @@ function returnsPromiseWithoutAsync(): Promise<string> {
}


returnsPromiseWithoutAsync()
returnsPromiseWithoutAsync()


const returnsPromiseAssignedArrowFunction = async (): Promise<string> => {
return 'value';
};

returnsPromiseAssignedArrowFunction();

const returnsPromiseAssignedFunction = async function (): Promise<string> {
return 'value'
}

async function returnsPromiseAssignedFunctionInAsyncFunction(): Promise<void> {
returnsPromiseAssignedFunction().then(() => { })
}

const returnsPromiseAssignedArrowFunctionAnnotatedType: () => Promise<string> = () => {
return Promise.resolve('value');
};

returnsPromiseAssignedArrowFunctionAnnotatedType();
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ function returnsPromiseWithoutAsync(): Promise<string> {


returnsPromiseWithoutAsync()


const returnsPromiseAssignedArrowFunction = async (): Promise<string> => {
return 'value';
};

returnsPromiseAssignedArrowFunction();

const returnsPromiseAssignedFunction = async function (): Promise<string> {
return 'value'
}

async function returnsPromiseAssignedFunctionInAsyncFunction(): Promise<void> {
returnsPromiseAssignedFunction().then(() => { })
}

const returnsPromiseAssignedArrowFunctionAnnotatedType: () => Promise<string> = () => {
return Promise.resolve('value');
};

returnsPromiseAssignedArrowFunctionAnnotatedType();
```

# Diagnostics
Expand Down Expand Up @@ -136,6 +157,59 @@ invalid.ts:27:1 lint/nursery/noFloatingPromises ━━━━━━━━━━
> 27 │ returnsPromiseWithoutAsync()
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 │
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.
```

```
invalid.ts:34:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
32 │ };
33 │
> 34 │ returnsPromiseAssignedArrowFunction();
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 │
36 │ const returnsPromiseAssignedFunction = async function (): Promise<string> {
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.
```

```
invalid.ts:41:3 lint/nursery/noFloatingPromises FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
40 │ async function returnsPromiseAssignedFunctionInAsyncFunction(): Promise<void> {
> 41 │ returnsPromiseAssignedFunction().then(() => { })
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42 │ }
43 │
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.
i Unsafe fix: Add await operator.
41 │ ··await·returnsPromiseAssignedFunction().then(()·=>·{·})
│ ++++++
```

```
invalid.ts:48:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
46 │ };
47 │
> 48 │ returnsPromiseAssignedArrowFunctionAnnotatedType();
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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.
Expand Down

0 comments on commit a4ee1e3

Please sign in to comment.