|
| 1 | +use oxc_ast::ast::FormalParameters; |
| 2 | +use oxc_ast::AstKind; |
| 3 | +use oxc_diagnostics::OxcDiagnostic; |
| 4 | +use oxc_macros::declare_oxc_lint; |
| 5 | +use oxc_span::{GetSpan, Span}; |
| 6 | + |
| 7 | +use crate::utils::is_promise; |
| 8 | +use crate::{context::LintContext, rule::Rule, AstNode}; |
| 9 | + |
| 10 | +fn no_promise_in_callback_diagnostic(span: Span) -> OxcDiagnostic { |
| 11 | + OxcDiagnostic::warn("Avoid using promises inside of callbacks.") |
| 12 | + .with_help("Use either promises or callbacks exclusively for handling asynchronous code.") |
| 13 | + .with_label(span) |
| 14 | +} |
| 15 | + |
| 16 | +#[derive(Debug, Default, Clone)] |
| 17 | +pub struct NoPromiseInCallback; |
| 18 | + |
| 19 | +declare_oxc_lint!( |
| 20 | + /// ### What it does |
| 21 | + /// Disallows the use of Promises within error-first callback functions. |
| 22 | + /// |
| 23 | + /// ### Why is this bad? |
| 24 | + /// Mixing Promises and callbacks can lead to unclear and inconsistent code. |
| 25 | + /// Promises and callbacks are different patterns for handling asynchronous code. |
| 26 | + /// Mixing them makes the logic flow harder to follow and complicates error handling, |
| 27 | + /// as callbacks rely on an error-first pattern, while Promises use `catch`. |
| 28 | + /// |
| 29 | + /// ### Examples |
| 30 | + /// |
| 31 | + /// Examples of **incorrect** code for this rule: |
| 32 | + /// ```js |
| 33 | + /// doSomething((err, val) => { |
| 34 | + /// if (err) console.error(err) |
| 35 | + /// else doSomethingElse(val).then(console.log) |
| 36 | + /// }) |
| 37 | + /// ``` |
| 38 | + /// |
| 39 | + /// Examples of **correct** code for this rule: |
| 40 | + /// ```js |
| 41 | + /// promisify(doSomething)() |
| 42 | + /// .then(doSomethingElse) |
| 43 | + /// .then(console.log) |
| 44 | + /// .catch(console.error) |
| 45 | + /// ``` |
| 46 | + NoPromiseInCallback, |
| 47 | + suspicious, |
| 48 | +); |
| 49 | + |
| 50 | +impl Rule for NoPromiseInCallback { |
| 51 | + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { |
| 52 | + let AstKind::CallExpression(call_expr) = node.kind() else { |
| 53 | + return; |
| 54 | + }; |
| 55 | + |
| 56 | + if is_promise(call_expr).is_none() { |
| 57 | + return; |
| 58 | + } |
| 59 | + |
| 60 | + // When a Promise is returned in a ReturnStatement, the function is most likely |
| 61 | + // being used as part of a Promise chain rather than as a callback function. |
| 62 | + // To avoid false positives, this case is intentionally excluded from the scope of this rule. |
| 63 | + if let Some(AstKind::ReturnStatement(_)) = ctx.nodes().parent_kind(node.id()) { |
| 64 | + return; |
| 65 | + }; |
| 66 | + |
| 67 | + let mut ancestors = ctx.nodes().ancestors(node.id()); |
| 68 | + if ancestors.any(|node| is_callback_function(node, ctx)) { |
| 69 | + ctx.diagnostic(no_promise_in_callback_diagnostic(call_expr.callee.span())); |
| 70 | + } |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +fn is_callback_function<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool { |
| 75 | + if !node.kind().is_function_like() { |
| 76 | + return false; |
| 77 | + } |
| 78 | + |
| 79 | + if is_within_promise_handler(node, ctx) { |
| 80 | + return false; |
| 81 | + } |
| 82 | + |
| 83 | + match node.kind() { |
| 84 | + AstKind::Function(function) => is_error_first_callback(&function.params), |
| 85 | + AstKind::ArrowFunctionExpression(arrow_function) => { |
| 86 | + is_error_first_callback(&arrow_function.params) |
| 87 | + } |
| 88 | + _ => false, |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +fn is_error_first_callback(params: &FormalParameters) -> bool { |
| 93 | + let Some(first_parameter) = params.items.first() else { |
| 94 | + return false; |
| 95 | + }; |
| 96 | + |
| 97 | + let Some(ident) = first_parameter.pattern.get_binding_identifier() else { |
| 98 | + return false; |
| 99 | + }; |
| 100 | + |
| 101 | + matches!(ident.name.as_str(), "err" | "error") |
| 102 | +} |
| 103 | + |
| 104 | +fn is_within_promise_handler<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool { |
| 105 | + if !matches!(node.kind(), AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)) { |
| 106 | + return false; |
| 107 | + } |
| 108 | + |
| 109 | + let Some(parent) = ctx.nodes().parent_node(node.id()) else { |
| 110 | + return false; |
| 111 | + }; |
| 112 | + if !matches!(ctx.nodes().kind(parent.id()), AstKind::Argument(_)) { |
| 113 | + return false; |
| 114 | + }; |
| 115 | + |
| 116 | + let Some(AstKind::CallExpression(call_expr)) = ctx.nodes().parent_kind(parent.id()) else { |
| 117 | + return false; |
| 118 | + }; |
| 119 | + |
| 120 | + matches!(call_expr.callee_name(), Some("then" | "catch")) |
| 121 | +} |
| 122 | + |
| 123 | +#[test] |
| 124 | +fn test() { |
| 125 | + use crate::tester::Tester; |
| 126 | + |
| 127 | + // The following test cases are based on the original |
| 128 | + // implementation from eslint-plugin-promise and are licensed under the ISC License. |
| 129 | + // |
| 130 | + // Copyright (c) 2020, Jamund Ferguson |
| 131 | + // https://github.com/eslint-community/eslint-plugin-promise/blob/266ddbb03076c05c362a6daecb9382b80cdd7108/__tests__/no-promise-in-callback.js |
| 132 | + let pass = vec![ |
| 133 | + "go(function() { return Promise.resolve(4) })", |
| 134 | + "go(function() { return a.then(b) })", |
| 135 | + "go(function() { b.catch(c) })", |
| 136 | + "go(function() { b.then(c, d) })", |
| 137 | + "go(() => Promise.resolve(4))", |
| 138 | + "go((errrr) => a.then(b))", |
| 139 | + "go((helpers) => { b.catch(c) })", |
| 140 | + "go((e) => { b.then(c, d) })", |
| 141 | + "a.catch((err) => { b.then(c, d) })", |
| 142 | + "var x = function() { return Promise.resolve(4) }", |
| 143 | + "function y() { return Promise.resolve(4) }", |
| 144 | + "function then() { return Promise.reject() }", |
| 145 | + "doThing(function(x) { return Promise.reject(x) })", |
| 146 | + "doThing().then(function() { return Promise.all([a,b,c]) })", |
| 147 | + "doThing().then(function() { return Promise.resolve(4) })", |
| 148 | + "doThing().then(() => Promise.resolve(4))", |
| 149 | + "doThing().then(() => Promise.all([a]))", |
| 150 | + "a(function(err) { return doThing().then(a) })", |
| 151 | + ]; |
| 152 | + |
| 153 | + let fail = vec![ |
| 154 | + "a(function(err) { doThing().then(a) })", |
| 155 | + "a(function(error, zup, supa) { doThing().then(a) })", |
| 156 | + "a(function(error) { doThing().then(a) })", |
| 157 | + "a((error) => { doThing().then(a) })", |
| 158 | + "a((error) => doThing().then(a))", |
| 159 | + "a((err, data) => { doThing().then(a) })", |
| 160 | + "a((err, data) => doThing().then(a))", |
| 161 | + "function x(err) { Promise.all() }", |
| 162 | + "function x(err) { Promise.allSettled() }", |
| 163 | + "function x(err) { Promise.any() }", |
| 164 | + "let x = (err) => doThingWith(err).then(a)", |
| 165 | + ]; |
| 166 | + |
| 167 | + Tester::new(NoPromiseInCallback::NAME, pass, fail).test_and_snapshot(); |
| 168 | +} |
0 commit comments