Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/oxc_linter/src/rules/promise/prefer_await_to_then.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
AstNode,
context::LintContext,
rule::{DefaultRuleConfig, Rule},
utils::is_promise,
utils::is_promise_with_context,
};

#[derive(Debug, Default, Clone, Deserialize)]
Expand Down Expand Up @@ -82,7 +82,7 @@ impl Rule for PreferAwaitToThen {
return;
};

if is_promise(call_expr).is_none_or(|v| v == "withResolvers") {
if is_promise_with_context(call_expr, ctx).is_none_or(|v| v == "withResolvers") {
return;
}

Expand Down Expand Up @@ -137,6 +137,13 @@ fn test() {
}",
None,
),
(
"function foo() {
const globalExceptionFilter = new GlobalExceptionFilter();
globalExceptionFilter.catch(error, host);
}",
None,
),
(
"async function hi() { await thing().then() }",
Some(serde_json::json!([{ "strict": false }])),
Expand Down
5 changes: 3 additions & 2 deletions crates/oxc_linter/src/rules/promise/valid_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{AstNode, context::LintContext, rule::Rule, utils::is_promise};
use crate::{AstNode, context::LintContext, rule::Rule, utils::is_promise_with_context};

fn zero_or_one_argument_required_diagnostic(
span: Span,
Expand Down Expand Up @@ -71,7 +71,7 @@ impl Rule for ValidParams {
return;
};

let Some(prop_name) = is_promise(call_expr) else {
let Some(prop_name) = is_promise_with_context(call_expr, ctx) else {
return;
};

Expand Down Expand Up @@ -148,6 +148,7 @@ fn test() {
"somePromise().finally(() => {})",
"promiseReference.finally(callback)",
"promiseReference.finally(() => {})",
"const globalExceptionFilter = new GlobalExceptionFilter(); globalExceptionFilter.catch(exception, host)",
"Promise.all([
Promise.resolve(1),
Promise.resolve(2),
Expand Down
99 changes: 98 additions & 1 deletion crates/oxc_linter/src/utils/promise.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
use oxc_ast::ast::{CallExpression, Expression, NewExpression};
use rustc_hash::FxHashSet;

use oxc_ast::{
AstKind,
ast::{CallExpression, Expression, IdentifierReference, NewExpression},
};
use oxc_semantic::SymbolId;

use crate::context::LintContext;

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
pub const PROMISE_STATIC_METHODS: [&str; 7] =
Expand All @@ -21,6 +29,95 @@ pub fn is_promise(call_expr: &CallExpression) -> Option<String> {
None
}

/// Like [`is_promise`], but avoids obvious false positives for non-Promise receivers.
///
/// This is intentionally conservative: if we cannot prove the receiver is non-Promise,
/// we keep the original behavior.
pub fn is_promise_with_context<'a>(
call_expr: &CallExpression<'a>,
ctx: &LintContext<'a>,
) -> Option<String> {
let prop_name = is_promise(call_expr)?;

// Promise static methods are already explicit (`Promise.resolve`, etc.).
if !matches!(prop_name.as_str(), "then" | "catch" | "finally") {
return Some(prop_name);
}

let member_expr = call_expr.callee.get_member_expr()?;
let mut visited = FxHashSet::<SymbolId>::default();
if matches!(
classify_receiver(member_expr.object().get_inner_expression(), ctx, &mut visited),
ReceiverKind::NotPromise
) {
return None;
}

Some(prop_name)
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ReceiverKind {
PromiseLike,
NotPromise,
Unknown,
}

fn classify_receiver<'a>(
expr: &Expression<'a>,
ctx: &LintContext<'a>,
visited: &mut FxHashSet<SymbolId>,
) -> ReceiverKind {
match expr.get_inner_expression() {
Expression::CallExpression(call_expr) => {
if is_promise(call_expr).is_some() {
ReceiverKind::PromiseLike
} else {
// Arbitrary call return types are unknown.
ReceiverKind::Unknown
}
}
Expression::NewExpression(new_expr) => {
if is_promise_constructor(new_expr) {
ReceiverKind::PromiseLike
} else {
ReceiverKind::NotPromise
}
}
Expression::Identifier(ident) => classify_identifier_receiver(ident, ctx, visited),
// These expression kinds are never Promise instances.
Expression::ObjectExpression(_)
| Expression::ArrayExpression(_)
| Expression::FunctionExpression(_)
| Expression::ArrowFunctionExpression(_)
| Expression::ClassExpression(_) => ReceiverKind::NotPromise,
_ => ReceiverKind::Unknown,
}
}

fn classify_identifier_receiver<'a>(
ident: &IdentifierReference<'a>,
ctx: &LintContext<'a>,
visited: &mut FxHashSet<SymbolId>,
) -> ReceiverKind {
let Some(symbol_id) = ctx.scoping().get_reference(ident.reference_id()).symbol_id() else {
return ReceiverKind::Unknown;
};

if !visited.insert(symbol_id) {
return ReceiverKind::Unknown;
}

let declaration = ctx.nodes().get_node(ctx.scoping().symbol_declaration(symbol_id));
match declaration.kind() {
AstKind::VariableDeclarator(var_decl) => var_decl
.init
.as_ref()
.map_or(ReceiverKind::Unknown, |init| classify_receiver(init, ctx, visited)),
_ => ReceiverKind::Unknown,
}
}

pub fn is_promise_constructor(new_expr: &NewExpression) -> bool {
new_expr.callee.is_specific_id("Promise")
}
Expand Down
Loading