mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter): add no-promise-in-callback (#7307)
related: #4655
This PR implements a rule to detect Promises inside error-first
callbacks, preventing the mixed usage of callbacks and Promises.
Example of problematic code:
```javascript
a(function(err) { doThing().then(a) });
^^^^^^^^^^^^^^
```
[Original
implementation](266ddbb030/rules/no-promise-in-callback.js)
This commit is contained in:
parent
9002e97e12
commit
4ad26b952f
3 changed files with 250 additions and 0 deletions
|
|
@ -485,6 +485,7 @@ mod promise {
|
|||
pub mod catch_or_return;
|
||||
pub mod no_callback_in_promise;
|
||||
pub mod no_new_statics;
|
||||
pub mod no_promise_in_callback;
|
||||
pub mod no_return_in_finally;
|
||||
pub mod param_names;
|
||||
pub mod prefer_await_to_callbacks;
|
||||
|
|
@ -784,6 +785,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
oxc::uninvoked_array_callback,
|
||||
promise::avoid_new,
|
||||
promise::catch_or_return,
|
||||
promise::no_promise_in_callback,
|
||||
promise::no_callback_in_promise,
|
||||
promise::no_new_statics,
|
||||
promise::no_return_in_finally,
|
||||
|
|
|
|||
168
crates/oxc_linter/src/rules/promise/no_promise_in_callback.rs
Normal file
168
crates/oxc_linter/src/rules/promise/no_promise_in_callback.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
use oxc_ast::ast::FormalParameters;
|
||||
use oxc_ast::AstKind;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
|
||||
use crate::utils::is_promise;
|
||||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||
|
||||
fn no_promise_in_callback_diagnostic(span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Avoid using promises inside of callbacks.")
|
||||
.with_help("Use either promises or callbacks exclusively for handling asynchronous code.")
|
||||
.with_label(span)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoPromiseInCallback;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
/// Disallows the use of Promises within error-first callback functions.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
/// Mixing Promises and callbacks can lead to unclear and inconsistent code.
|
||||
/// Promises and callbacks are different patterns for handling asynchronous code.
|
||||
/// Mixing them makes the logic flow harder to follow and complicates error handling,
|
||||
/// as callbacks rely on an error-first pattern, while Promises use `catch`.
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// Examples of **incorrect** code for this rule:
|
||||
/// ```js
|
||||
/// doSomething((err, val) => {
|
||||
/// if (err) console.error(err)
|
||||
/// else doSomethingElse(val).then(console.log)
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// Examples of **correct** code for this rule:
|
||||
/// ```js
|
||||
/// promisify(doSomething)()
|
||||
/// .then(doSomethingElse)
|
||||
/// .then(console.log)
|
||||
/// .catch(console.error)
|
||||
/// ```
|
||||
NoPromiseInCallback,
|
||||
suspicious,
|
||||
);
|
||||
|
||||
impl Rule for NoPromiseInCallback {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if is_promise(call_expr).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// When a Promise is returned in a ReturnStatement, the function is most likely
|
||||
// being used as part of a Promise chain rather than as a callback function.
|
||||
// To avoid false positives, this case is intentionally excluded from the scope of this rule.
|
||||
if let Some(AstKind::ReturnStatement(_)) = ctx.nodes().parent_kind(node.id()) {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut ancestors = ctx.nodes().ancestors(node.id());
|
||||
if ancestors.any(|node| is_callback_function(node, ctx)) {
|
||||
ctx.diagnostic(no_promise_in_callback_diagnostic(call_expr.callee.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_callback_function<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
|
||||
if !node.kind().is_function_like() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_within_promise_handler(node, ctx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match node.kind() {
|
||||
AstKind::Function(function) => is_error_first_callback(&function.params),
|
||||
AstKind::ArrowFunctionExpression(arrow_function) => {
|
||||
is_error_first_callback(&arrow_function.params)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_error_first_callback(params: &FormalParameters) -> bool {
|
||||
let Some(first_parameter) = params.items.first() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(ident) = first_parameter.pattern.get_binding_identifier() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
matches!(ident.name.as_str(), "err" | "error")
|
||||
}
|
||||
|
||||
fn is_within_promise_handler<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
|
||||
if !matches!(node.kind(), AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(parent) = ctx.nodes().parent_node(node.id()) else {
|
||||
return false;
|
||||
};
|
||||
if !matches!(ctx.nodes().kind(parent.id()), AstKind::Argument(_)) {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(AstKind::CallExpression(call_expr)) = ctx.nodes().parent_kind(parent.id()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
matches!(call_expr.callee_name(), Some("then" | "catch"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
// The following test cases are based on the original
|
||||
// implementation from eslint-plugin-promise and are licensed under the ISC License.
|
||||
//
|
||||
// Copyright (c) 2020, Jamund Ferguson
|
||||
// https://github.com/eslint-community/eslint-plugin-promise/blob/266ddbb03076c05c362a6daecb9382b80cdd7108/__tests__/no-promise-in-callback.js
|
||||
let pass = vec![
|
||||
"go(function() { return Promise.resolve(4) })",
|
||||
"go(function() { return a.then(b) })",
|
||||
"go(function() { b.catch(c) })",
|
||||
"go(function() { b.then(c, d) })",
|
||||
"go(() => Promise.resolve(4))",
|
||||
"go((errrr) => a.then(b))",
|
||||
"go((helpers) => { b.catch(c) })",
|
||||
"go((e) => { b.then(c, d) })",
|
||||
"a.catch((err) => { b.then(c, d) })",
|
||||
"var x = function() { return Promise.resolve(4) }",
|
||||
"function y() { return Promise.resolve(4) }",
|
||||
"function then() { return Promise.reject() }",
|
||||
"doThing(function(x) { return Promise.reject(x) })",
|
||||
"doThing().then(function() { return Promise.all([a,b,c]) })",
|
||||
"doThing().then(function() { return Promise.resolve(4) })",
|
||||
"doThing().then(() => Promise.resolve(4))",
|
||||
"doThing().then(() => Promise.all([a]))",
|
||||
"a(function(err) { return doThing().then(a) })",
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
"a(function(err) { doThing().then(a) })",
|
||||
"a(function(error, zup, supa) { doThing().then(a) })",
|
||||
"a(function(error) { doThing().then(a) })",
|
||||
"a((error) => { doThing().then(a) })",
|
||||
"a((error) => doThing().then(a))",
|
||||
"a((err, data) => { doThing().then(a) })",
|
||||
"a((err, data) => doThing().then(a))",
|
||||
"function x(err) { Promise.all() }",
|
||||
"function x(err) { Promise.allSettled() }",
|
||||
"function x(err) { Promise.any() }",
|
||||
"let x = (err) => doThingWith(err).then(a)",
|
||||
];
|
||||
|
||||
Tester::new(NoPromiseInCallback::NAME, pass, fail).test_and_snapshot();
|
||||
}
|
||||
80
crates/oxc_linter/src/snapshots/no_promise_in_callback.snap
Normal file
80
crates/oxc_linter/src/snapshots/no_promise_in_callback.snap
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:19]
|
||||
1 │ a(function(err) { doThing().then(a) })
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:32]
|
||||
1 │ a(function(error, zup, supa) { doThing().then(a) })
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:21]
|
||||
1 │ a(function(error) { doThing().then(a) })
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:16]
|
||||
1 │ a((error) => { doThing().then(a) })
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:14]
|
||||
1 │ a((error) => doThing().then(a))
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:20]
|
||||
1 │ a((err, data) => { doThing().then(a) })
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:18]
|
||||
1 │ a((err, data) => doThing().then(a))
|
||||
· ──────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:19]
|
||||
1 │ function x(err) { Promise.all() }
|
||||
· ───────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:19]
|
||||
1 │ function x(err) { Promise.allSettled() }
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:19]
|
||||
1 │ function x(err) { Promise.any() }
|
||||
· ───────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
|
||||
⚠ eslint-plugin-promise(no-promise-in-callback): Avoid using promises inside of callbacks.
|
||||
╭─[no_promise_in_callback.tsx:1:18]
|
||||
1 │ let x = (err) => doThingWith(err).then(a)
|
||||
· ─────────────────────
|
||||
╰────
|
||||
help: Use either promises or callbacks exclusively for handling asynchronous code.
|
||||
Loading…
Reference in a new issue