mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): implement no-callback-in-promise (#6157)
This commit is contained in:
parent
8c78f9719b
commit
1e7fab32fd
3 changed files with 274 additions and 0 deletions
|
|
@ -471,6 +471,7 @@ mod tree_shaking {
|
||||||
mod promise {
|
mod promise {
|
||||||
pub mod avoid_new;
|
pub mod avoid_new;
|
||||||
pub mod catch_or_return;
|
pub mod catch_or_return;
|
||||||
|
pub mod no_callback_in_promise;
|
||||||
pub mod no_new_statics;
|
pub mod no_new_statics;
|
||||||
pub mod no_return_in_finally;
|
pub mod no_return_in_finally;
|
||||||
pub mod param_names;
|
pub mod param_names;
|
||||||
|
|
@ -765,6 +766,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
oxc::uninvoked_array_callback,
|
oxc::uninvoked_array_callback,
|
||||||
promise::avoid_new,
|
promise::avoid_new,
|
||||||
promise::catch_or_return,
|
promise::catch_or_return,
|
||||||
|
promise::no_callback_in_promise,
|
||||||
promise::no_new_statics,
|
promise::no_new_statics,
|
||||||
promise::no_return_in_finally,
|
promise::no_return_in_finally,
|
||||||
promise::param_names,
|
promise::param_names,
|
||||||
|
|
|
||||||
186
crates/oxc_linter/src/rules/promise/no_callback_in_promise.rs
Normal file
186
crates/oxc_linter/src/rules/promise/no_callback_in_promise.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
use oxc_ast::{
|
||||||
|
ast::{CallExpression, Expression, MemberExpression},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
|
use oxc_diagnostics::OxcDiagnostic;
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_span::{CompactStr, GetSpan, Span};
|
||||||
|
|
||||||
|
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||||
|
|
||||||
|
fn no_callback_in_promise_diagnostic(span: Span) -> OxcDiagnostic {
|
||||||
|
OxcDiagnostic::warn("Avoid calling back inside of a promise")
|
||||||
|
.with_help("Use `then` and `catch` directly")
|
||||||
|
.with_label(span)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct NoCallbackInPromise(Box<NoCallbackInPromiseConfig>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NoCallbackInPromiseConfig {
|
||||||
|
callbacks: Vec<CompactStr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NoCallbackInPromiseConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { callbacks: vec!["callback".into(), "cb".into(), "done".into(), "next".into()] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for NoCallbackInPromise {
|
||||||
|
type Target = NoCallbackInPromiseConfig;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
///
|
||||||
|
/// Disallows calling a callback function (`cb()`) inside a `Promise.prototype.then()`
|
||||||
|
/// or `Promise.prototype.catch()`.
|
||||||
|
///
|
||||||
|
/// ### Why is this bad?
|
||||||
|
///
|
||||||
|
/// Directly invoking a callback inside a `then()` or `catch()` method can lead to
|
||||||
|
/// unexpected behavior, such as the callback being called multiple times. Additionally,
|
||||||
|
/// mixing the callback and promise paradigms in this way can make the code confusing
|
||||||
|
/// and harder to maintain.
|
||||||
|
///
|
||||||
|
/// ### Examples
|
||||||
|
///
|
||||||
|
/// Examples of **incorrect** code for this rule:
|
||||||
|
/// ```js
|
||||||
|
/// function callback(err, data) {
|
||||||
|
/// console.log('Callback got called with:', err, data)
|
||||||
|
/// throw new Error('My error')
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Promise.resolve()
|
||||||
|
/// .then(() => callback(null, 'data'))
|
||||||
|
/// .catch((err) => callback(err.message, null))
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Examples of **correct** code for this rule:
|
||||||
|
/// ```js
|
||||||
|
/// Promise.resolve()
|
||||||
|
/// .then((data) => { console.log(data) })
|
||||||
|
/// .catch((err) => { console.error(err) })
|
||||||
|
/// ```
|
||||||
|
NoCallbackInPromise,
|
||||||
|
correctness,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for NoCallbackInPromise {
|
||||||
|
fn from_configuration(value: serde_json::Value) -> Self {
|
||||||
|
let mut default_config = NoCallbackInPromiseConfig::default();
|
||||||
|
|
||||||
|
let exceptions: Vec<String> = value
|
||||||
|
.get(0)
|
||||||
|
.and_then(|v| v.get("exceptions"))
|
||||||
|
.and_then(serde_json::Value::as_array)
|
||||||
|
.map(|v| {
|
||||||
|
v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
default_config.callbacks.retain(|item| !exceptions.contains(&item.to_string()));
|
||||||
|
|
||||||
|
Self(Box::new(default_config))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
|
let Some(call_expr) = node.kind().as_call_expression() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_not_callback = call_expr
|
||||||
|
.callee
|
||||||
|
.get_identifier_reference()
|
||||||
|
.map_or(true, |id| self.callbacks.binary_search(&id.name.as_str().into()).is_err());
|
||||||
|
|
||||||
|
if is_not_callback {
|
||||||
|
if Self::has_promise_callback(call_expr) {
|
||||||
|
let Some(id) = call_expr.arguments.first().and_then(|arg| {
|
||||||
|
arg.as_expression().and_then(Expression::get_identifier_reference)
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = id.name.as_str();
|
||||||
|
if self.callbacks.binary_search(&name.into()).is_ok() {
|
||||||
|
ctx.diagnostic(no_callback_in_promise_diagnostic(id.span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ctx
|
||||||
|
.nodes()
|
||||||
|
.iter_parents(node.id())
|
||||||
|
.skip(1)
|
||||||
|
.any(|node| Self::is_inside_promise(node, ctx))
|
||||||
|
{
|
||||||
|
ctx.diagnostic(no_callback_in_promise_diagnostic(node.span()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoCallbackInPromise {
|
||||||
|
fn is_inside_promise(node: &AstNode, ctx: &LintContext) -> bool {
|
||||||
|
if !matches!(node.kind(), AstKind::Function(_) | AstKind::ArrowFunctionExpression(_))
|
||||||
|
|| !matches!(ctx.nodes().parent_kind(node.id()), Some(AstKind::Argument(_)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.nodes().iter_parents(node.id()).nth(2).is_some_and(|node| {
|
||||||
|
node.kind().as_call_expression().is_some_and(Self::has_promise_callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_promise_callback(call_expr: &CallExpression) -> bool {
|
||||||
|
matches!(
|
||||||
|
call_expr
|
||||||
|
.callee
|
||||||
|
.as_member_expression()
|
||||||
|
.and_then(MemberExpression::static_property_name),
|
||||||
|
Some("then" | "catch")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
("function thing(cb) { cb() }", None),
|
||||||
|
("doSomething(function(err) { cb(err) })", None),
|
||||||
|
("function thing(callback) { callback() }", None),
|
||||||
|
("doSomething(function(err) { callback(err) })", None),
|
||||||
|
("let thing = (cb) => cb()", None),
|
||||||
|
("doSomething(err => cb(err))", None),
|
||||||
|
("a.then(() => next())", Some(serde_json::json!([{ "exceptions": ["next"] }]))),
|
||||||
|
(
|
||||||
|
"a.then(() => next()).catch((err) => next(err))",
|
||||||
|
Some(serde_json::json!([{ "exceptions": ["next"] }])),
|
||||||
|
),
|
||||||
|
("a.then(next)", Some(serde_json::json!([{ "exceptions": ["next"] }]))),
|
||||||
|
("a.then(next).catch(next)", Some(serde_json::json!([{ "exceptions": ["next"] }]))),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
("a.then(cb)", None),
|
||||||
|
("a.then(() => cb())", None),
|
||||||
|
("a.then(function(err) { cb(err) })", None),
|
||||||
|
("a.then(function(data) { cb(data) }, function(err) { cb(err) })", None),
|
||||||
|
("a.catch(function(err) { cb(err) })", None),
|
||||||
|
("a.then(callback)", None),
|
||||||
|
("a.then(() => callback())", None),
|
||||||
|
("a.then(function(err) { callback(err) })", None),
|
||||||
|
("a.then(function(data) { callback(data) }, function(err) { callback(err) })", None),
|
||||||
|
("a.catch(function(err) { callback(err) })", None),
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(NoCallbackInPromise::NAME, pass, fail).test_and_snapshot();
|
||||||
|
}
|
||||||
86
crates/oxc_linter/src/snapshots/no_callback_in_promise.snap
Normal file
86
crates/oxc_linter/src/snapshots/no_callback_in_promise.snap
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:8]
|
||||||
|
1 │ a.then(cb)
|
||||||
|
· ──
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:14]
|
||||||
|
1 │ a.then(() => cb())
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:24]
|
||||||
|
1 │ a.then(function(err) { cb(err) })
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:25]
|
||||||
|
1 │ a.then(function(data) { cb(data) }, function(err) { cb(err) })
|
||||||
|
· ────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:53]
|
||||||
|
1 │ a.then(function(data) { cb(data) }, function(err) { cb(err) })
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:25]
|
||||||
|
1 │ a.catch(function(err) { cb(err) })
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:8]
|
||||||
|
1 │ a.then(callback)
|
||||||
|
· ────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:14]
|
||||||
|
1 │ a.then(() => callback())
|
||||||
|
· ──────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:24]
|
||||||
|
1 │ a.then(function(err) { callback(err) })
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:25]
|
||||||
|
1 │ a.then(function(data) { callback(data) }, function(err) { callback(err) })
|
||||||
|
· ──────────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:59]
|
||||||
|
1 │ a.then(function(data) { callback(data) }, function(err) { callback(err) })
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
|
|
||||||
|
⚠ eslint-plugin-promise(no-callback-in-promise): Avoid calling back inside of a promise
|
||||||
|
╭─[no_callback_in_promise.tsx:1:25]
|
||||||
|
1 │ a.catch(function(err) { callback(err) })
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Use `then` and `catch` directly
|
||||||
Loading…
Reference in a new issue