Skip to content

Commit

Permalink
refactor(noFloatingPromises): update functions to return Option (#4970)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaykdm authored Jan 25, 2025
1 parent c047886 commit bdc34a1
Showing 1 changed file with 130 additions and 140 deletions.
270 changes: 130 additions & 140 deletions crates/biome_js_analyze/src/lint/nursery/no_floating_promises.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use biome_console::markup;
use biome_js_factory::make;
use biome_js_semantic::SemanticModel;
use biome_js_syntax::{
binding_ext::AnyJsBindingDeclaration, AnyJsExpression, AnyJsName, AnyTsName, AnyTsReturnType,
AnyTsType, AnyTsVariableAnnotation, JsArrowFunctionExpression, JsCallExpression,
JsExpressionStatement, JsFunctionDeclaration, JsMethodClassMember, JsMethodObjectMember,
JsStaticMemberExpression, JsSyntaxKind, JsVariableDeclarator, TsReturnTypeAnnotation,
binding_ext::AnyJsBindingDeclaration, AnyJsExpression, JsArrowFunctionExpression,
JsCallExpression, JsExpressionStatement, JsFunctionDeclaration, JsMethodClassMember,
JsMethodObjectMember, JsStaticMemberExpression, JsSyntaxKind, JsVariableDeclarator,
TsReturnTypeAnnotation,
};
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, SyntaxNodeCast, TriviaPieceKind};

Expand Down Expand Up @@ -100,11 +100,11 @@ impl Rule for NoFloatingPromises {
return None;
};

if !is_callee_a_promise(&any_js_expression, model) {
if !is_callee_a_promise(&any_js_expression, model)? {
return None;
}

if is_handled_promise(&js_call_expression) {
if is_handled_promise(&js_call_expression).unwrap_or(false) {
return None;
}

Expand Down Expand Up @@ -190,60 +190,55 @@ impl Rule for NoFloatingPromises {
///
/// doesNotReturnPromise().then(() => {});
/// ```
fn is_callee_a_promise(callee: &AnyJsExpression, model: &SemanticModel) -> bool {
fn is_callee_a_promise(callee: &AnyJsExpression, model: &SemanticModel) -> Option<bool> {
match callee {
AnyJsExpression::JsIdentifierExpression(ident_expr) => {
let Some(reference) = ident_expr.name().ok() else {
return false;
};

let Some(binding) = model.binding(&reference) else {
return false;
};
let reference = ident_expr.name().ok()?;
let binding = model.binding(&reference)?;
let any_js_binding_decl = binding.tree().declaration()?;

let Some(any_js_binding_decl) = binding.tree().declaration() else {
return false;
};
match any_js_binding_decl {
AnyJsBindingDeclaration::JsFunctionDeclaration(func_decl) => {
is_function_a_promise(&func_decl)
Some(is_function_a_promise(&func_decl))
}
AnyJsBindingDeclaration::JsVariableDeclarator(js_var_decl) => {
is_variable_initializer_a_promise(&js_var_decl)
|| is_variable_annotation_a_promise(&js_var_decl)
}
_ => false,
AnyJsBindingDeclaration::JsVariableDeclarator(js_var_decl) => Some(
is_variable_initializer_a_promise(&js_var_decl).unwrap_or(false)
|| is_variable_annotation_a_promise(&js_var_decl).unwrap_or(false),
),
_ => Some(false),
}
}
AnyJsExpression::JsStaticMemberExpression(static_member_expr) => {
is_member_expression_callee_a_promise(static_member_expr, model)
}
_ => false,
_ => Some(false),
}
}

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

/// Checks if a TypeScript return type annotation is a `Promise`.
///
/// This function inspects the return type annotation of a TypeScript function to determine
/// if it is a `Promise`. It returns `true` if the return type annotation is `Promise`, otherwise `false`.
/// if it is a `Promise`. It returns `Some(true)` if the return type annotation is `Promise`,
/// `Some(false)` if it is not, and `None` if there is an error in the process.
///
/// # Arguments
///
/// * `return_type` - An optional `TsReturnTypeAnnotation` to check.
///
/// # Returns
///
/// * `true` if the return type annotation is `Promise`.
/// * `false` otherwise.
/// * `Some(true)` if the return type annotation is `Promise`.
/// * `Some(false)` if the return type annotation is not `Promise`.
/// * `None` if there is an error in the process.
///
/// # Examples
///
/// Example TypeScript code that would return `true`:
/// Example TypeScript code that would return `Some(true)`:
/// ```typescript
/// async function returnsPromise(): Promise<void> {}
/// ```
Expand All @@ -252,84 +247,74 @@ fn is_function_a_promise(func_decl: &JsFunctionDeclaration) -> bool {
/// ```typescript
/// function doesNotReturnPromise(): void {}
/// ```
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 {
AnyTsReturnType::AnyTsType(any_ts_type) => Some(any_ts_type),
_ => None,
})
.and_then(|any_ts_type| match any_ts_type {
AnyTsType::TsReferenceType(reference_type) => Some(reference_type),
_ => None,
})
.and_then(|reference_type| reference_type.name().ok())
.and_then(|name| match name {
AnyTsName::JsReferenceIdentifier(identifier) => Some(identifier),
_ => None,
})
.map_or(false, |reference| reference.has_name("Promise"))
fn is_return_type_a_promise(return_type: Option<TsReturnTypeAnnotation>) -> Option<bool> {
let ts_return_type_anno = return_type?.ty().ok()?;
let any_ts_type = ts_return_type_anno.as_any_ts_type()?;
let reference_type = any_ts_type.as_ts_reference_type()?;
let any_ts_name = reference_type.name().ok()?;
let name = any_ts_name.as_js_reference_identifier()?;

Some(name.has_name("Promise"))
}

/// Checks if a `JsCallExpression` is a handled Promise-like expression.
/// - Calling its .then() with two arguments
/// - Calling its .catch() with one argument
///
/// Example TypeScript code that would return `true`:
/// This function inspects a `JsCallExpression` to determine if it is a handled Promise-like expression.
/// It returns `Some(true)` if the expression is handled, `Some(false)` if it is not, and `None` if there is an error in the process.
///
/// # Arguments
///
/// * `js_call_expression` - A reference to a `JsCallExpression` to check.
///
/// # Returns
///
/// * `Some(true)` if the expression is a handled Promise-like expression.
/// * `Some(false)` if the expression is not a handled Promise-like expression.
/// * `None` if there is an error in the process.
///
/// # Examples
///
/// Example TypeScript code that would return `Some(true)`:
/// ```typescript
/// const promise: Promise<unknown> = new Promise((resolve, reject) => resolve('value'));
/// promise.then(() => "aaa", () => null).finally(() => null)
///
/// const promise: Promise<unknown> = new Promise((resolve, reject) => resolve('value'));
/// promise.then(() => "aaa").catch(() => null).finally(() => null)
/// ```
fn is_handled_promise(js_call_expression: &JsCallExpression) -> bool {
let Ok(expr) = js_call_expression.callee() else {
return false;
};

let AnyJsExpression::JsStaticMemberExpression(static_member_expr) = expr else {
return false;
};

let Ok(AnyJsName::JsName(name)) = static_member_expr.member() else {
return false;
};

let name = name.to_string();
fn is_handled_promise(js_call_expression: &JsCallExpression) -> Option<bool> {
let expr = js_call_expression.callee().ok()?;
let static_member_expr = expr.as_js_static_member_expression()?;
let member = static_member_expr.member().ok()?;
let js_name = member.as_js_name()?;
let name = js_name.to_string();

if name == "finally" {
if let Ok(expr) = static_member_expr.object() {
if let Some(callee) = expr.as_js_call_expression() {
return is_handled_promise(callee);
}
}
let expr = static_member_expr.object().ok()?;
let callee = expr.as_js_call_expression()?;
return is_handled_promise(callee);
}
if name == "catch" {
if let Ok(call_args) = js_call_expression.arguments() {
// just checking if there are any arguments, not if it's a function for simplicity
if call_args.args().len() > 0 {
return true;
}
}
let call_args = js_call_expression.arguments().ok()?;
// just checking if there are any arguments, not if it's a function for simplicity
return Some(call_args.args().len() > 0);
}
if name == "then" {
if let Ok(call_args) = js_call_expression.arguments() {
// just checking arguments have a reject function from length
if call_args.args().len() >= 2 {
return true;
}
}
let call_args = js_call_expression.arguments().ok()?;
// just checking arguments have a reject function from length
return Some(call_args.args().len() >= 2);
}
false

Some(false)
}

/// Checks if the callee of a `JsStaticMemberExpression` is a promise expression.
///
/// This function inspects the callee of a `JsStaticMemberExpression` to determine
/// if it is a promise expression. It returns `true` if the callee is a promise expression,
/// otherwise `false`.
/// if it is a promise expression. It returns `Some(true)` if the callee is a promise expression,
/// `Some(false)` if it is not, and `None` if there is an error in the process.
///
/// # Arguments
///
Expand All @@ -338,8 +323,9 @@ fn is_handled_promise(js_call_expression: &JsCallExpression) -> bool {
///
/// # Returns
///
/// * `true` if the callee is a promise expression.
/// * `false` otherwise.
/// * `Some(true)` if the callee is a promise expression.
/// * `Some(false)` if the callee is not a promise expression.
/// * `None` if there is an error in the process.
///
/// # Examples
///
Expand All @@ -359,18 +345,10 @@ fn is_handled_promise(js_call_expression: &JsCallExpression) -> bool {
fn is_member_expression_callee_a_promise(
static_member_expr: &JsStaticMemberExpression,
model: &SemanticModel,
) -> bool {
let Ok(expr) = static_member_expr.object() else {
return false;
};

let AnyJsExpression::JsCallExpression(js_call_expr) = expr else {
return false;
};

let Ok(callee) = js_call_expr.callee() else {
return false;
};
) -> Option<bool> {
let expr = static_member_expr.object().ok()?;
let js_call_expr = expr.as_js_call_expression()?;
let callee = js_call_expr.callee().ok()?;

is_callee_a_promise(&callee, model)
}
Expand Down Expand Up @@ -411,9 +389,25 @@ fn is_in_async_function(node: &JsExpressionStatement) -> bool {
.is_some()
}

/// Checks if the initializer of a `JsVariableDeclarator` is an async function.
/// Checks if the initializer of a `JsVariableDeclarator` is an async function or returns a promise.
///
/// Example TypeScript code that would return `true`:
/// This function inspects the initializer of a given `JsVariableDeclarator` to determine
/// if it is an async function or returns a promise. It returns `Some(true)` if the initializer
/// is an async function or returns a promise, `Some(false)` if it is not, and `None` if there is an error in the process.
///
/// # Arguments
///
/// * `js_variable_declarator` - A reference to a `JsVariableDeclarator` to check.
///
/// # Returns
///
/// * `Some(true)` if the initializer is an async function or returns a promise.
/// * `Some(false)` if the initializer is not an async function and does not return a promise.
/// * `None` if there is an error in the process.
///
/// # Examples
///
/// Example TypeScript code that would return `Some(true)`:
///
/// ```typescript
/// const returnsPromise = async (): Promise<string> => {
Expand All @@ -424,61 +418,57 @@ fn is_in_async_function(node: &JsExpressionStatement) -> bool {
/// 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;
};
fn is_variable_initializer_a_promise(
js_variable_declarator: &JsVariableDeclarator,
) -> Option<bool> {
let initializer_clause = &js_variable_declarator.initializer()?;
let expr = initializer_clause.expression().ok()?;
match expr {
AnyJsExpression::JsArrowFunctionExpression(arrow_func) => {
AnyJsExpression::JsArrowFunctionExpression(arrow_func) => Some(
arrow_func.async_token().is_some()
|| is_return_type_a_promise(arrow_func.return_type_annotation())
}
AnyJsExpression::JsFunctionExpression(func_expr) => {
|| is_return_type_a_promise(arrow_func.return_type_annotation()).unwrap_or(false),
),
AnyJsExpression::JsFunctionExpression(func_expr) => Some(
func_expr.async_token().is_some()
|| is_return_type_a_promise(func_expr.return_type_annotation())
}
_ => false,
|| is_return_type_a_promise(func_expr.return_type_annotation()).unwrap_or(false),
),
_ => Some(false),
}
}

/// Checks if a `JsVariableDeclarator` has a TypeScript type annotation of `Promise`.
///
/// This function inspects the type annotation of a given `JsVariableDeclarator` to determine
/// if it is a `Promise`. It returns `Some(true)` if the type annotation is `Promise`,
/// `Some(false)` if it is not, and `None` if there is an error in the process.
///
/// Example TypeScript code that would return `true`:
/// # Arguments
///
/// * `js_variable_declarator` - A reference to a `JsVariableDeclarator` to check.
///
/// # Returns
///
/// * `Some(true)` if the type annotation is `Promise`.
/// * `Some(false)` if the type annotation is not `Promise`.
/// * `None` if there is an error in the process.
///
/// # Examples
///
/// Example TypeScript code that would return `Some(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)
fn is_variable_annotation_a_promise(js_variable_declarator: &JsVariableDeclarator) -> Option<bool> {
let any_ts_var_anno = js_variable_declarator.variable_annotation()?;
let ts_type_anno = any_ts_var_anno.as_ts_type_annotation()?;
let any_ts_type = ts_type_anno.ty().ok()?;
let func_type = any_ts_type.as_ts_function_type()?;
let return_type = func_type.return_type().ok()?;
let ref_type = return_type.as_any_ts_type()?.as_ts_reference_type()?;
let name = ref_type.name().ok()?;
let identifier = name.as_js_reference_identifier()?;

Some(identifier.has_name("Promise"))
}

0 comments on commit bdc34a1

Please sign in to comment.