feat(linter/n): implement callback-return rule (#22470)

this PR implements `callback-return` rule, passes all upstream tests.
issue #493

there is an issue with config doc generation for array with default
values. Ill fix it a little later and then move the PR out of draft.

---------

Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
Mikhail BaevandCameron Clark authored and GitHub committed 2026-05-18 15:54:28 +01:00
1 parent 65a77f1a5f
commit 5fa47746a3
8 files changed
+730 -1

No files matched your search

@@ -836,6 +836,7 @@ export interface DummyRuleMap {
"no-void"?: DummyRule;
"no-warning-comments"?: DummyRule;
"no-with"?: DummyRule;
"node/callback-return"?: DummyRule;
"node/global-require"?: DummyRule;
"node/handle-callback-err"?: DummyRule;
"node/no-exports-assign"?: DummyRule;
+6
View File
@@ -4875,6 +4875,12 @@ impl RuleRunner for crate::rules::vitest::warn_todo::WarnTodo {
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnJestNode;
}
impl RuleRunner for crate::rules::node::callback_return::CallbackReturn {
const NODE_TYPES: Option<&AstTypesBitset> =
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
}
impl RuleRunner for crate::rules::node::global_require::GlobalRequire {
const NODE_TYPES: Option<&AstTypesBitset> =
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
+29 -1
View File
@@ -362,6 +362,7 @@ pub use crate::rules::nextjs::no_sync_scripts::NoSyncScripts as NextjsNoSyncScri
pub use crate::rules::nextjs::no_title_in_document_head::NoTitleInDocumentHead as NextjsNoTitleInDocumentHead;
pub use crate::rules::nextjs::no_typos::NoTypos as NextjsNoTypos;
pub use crate::rules::nextjs::no_unwanted_polyfillio::NoUnwantedPolyfillio as NextjsNoUnwantedPolyfillio;
pub use crate::rules::node::callback_return::CallbackReturn as NodeCallbackReturn;
pub use crate::rules::node::global_require::GlobalRequire as NodeGlobalRequire;
pub use crate::rules::node::handle_callback_err::HandleCallbackErr as NodeHandleCallbackErr;
pub use crate::rules::node::no_exports_assign::NoExportsAssign as NodeNoExportsAssign;
@@ -1599,6 +1600,7 @@ pub enum RuleEnum {
VitestValidExpectInPromise(VitestValidExpectInPromise),
VitestValidTitle(VitestValidTitle),
VitestWarnTodo(VitestWarnTodo),
NodeCallbackReturn(NodeCallbackReturn),
NodeGlobalRequire(NodeGlobalRequire),
NodeHandleCallbackErr(NodeHandleCallbackErr),
NodeNoExportsAssign(NodeNoExportsAssign),
@@ -2499,7 +2501,8 @@ const VITEST_VALID_EXPECT_ID: usize = VITEST_VALID_DESCRIBE_CALLBACK_ID + 1usize
const VITEST_VALID_EXPECT_IN_PROMISE_ID: usize = VITEST_VALID_EXPECT_ID + 1usize;
const VITEST_VALID_TITLE_ID: usize = VITEST_VALID_EXPECT_IN_PROMISE_ID + 1usize;
const VITEST_WARN_TODO_ID: usize = VITEST_VALID_TITLE_ID + 1usize;
const NODE_GLOBAL_REQUIRE_ID: usize = VITEST_WARN_TODO_ID + 1usize;
const NODE_CALLBACK_RETURN_ID: usize = VITEST_WARN_TODO_ID + 1usize;
const NODE_GLOBAL_REQUIRE_ID: usize = NODE_CALLBACK_RETURN_ID + 1usize;
const NODE_HANDLE_CALLBACK_ERR_ID: usize = NODE_GLOBAL_REQUIRE_ID + 1usize;
const NODE_NO_EXPORTS_ASSIGN_ID: usize = NODE_HANDLE_CALLBACK_ERR_ID + 1usize;
const NODE_NO_NEW_REQUIRE_ID: usize = NODE_NO_EXPORTS_ASSIGN_ID + 1usize;
@@ -3430,6 +3433,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VITEST_VALID_EXPECT_IN_PROMISE_ID,
Self::VitestValidTitle(_) => VITEST_VALID_TITLE_ID,
Self::VitestWarnTodo(_) => VITEST_WARN_TODO_ID,
Self::NodeCallbackReturn(_) => NODE_CALLBACK_RETURN_ID,
Self::NodeGlobalRequire(_) => NODE_GLOBAL_REQUIRE_ID,
Self::NodeHandleCallbackErr(_) => NODE_HANDLE_CALLBACK_ERR_ID,
Self::NodeNoExportsAssign(_) => NODE_NO_EXPORTS_ASSIGN_ID,
@@ -4345,6 +4349,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::NAME,
Self::VitestValidTitle(_) => VitestValidTitle::NAME,
Self::VitestWarnTodo(_) => VitestWarnTodo::NAME,
Self::NodeCallbackReturn(_) => NodeCallbackReturn::NAME,
Self::NodeGlobalRequire(_) => NodeGlobalRequire::NAME,
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::NAME,
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::NAME,
@@ -5316,6 +5321,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::CATEGORY,
Self::VitestValidTitle(_) => VitestValidTitle::CATEGORY,
Self::VitestWarnTodo(_) => VitestWarnTodo::CATEGORY,
Self::NodeCallbackReturn(_) => NodeCallbackReturn::CATEGORY,
Self::NodeGlobalRequire(_) => NodeGlobalRequire::CATEGORY,
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::CATEGORY,
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::CATEGORY,
@@ -6234,6 +6240,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::FIX,
Self::VitestValidTitle(_) => VitestValidTitle::FIX,
Self::VitestWarnTodo(_) => VitestWarnTodo::FIX,
Self::NodeCallbackReturn(_) => NodeCallbackReturn::FIX,
Self::NodeGlobalRequire(_) => NodeGlobalRequire::FIX,
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::FIX,
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::FIX,
@@ -7392,6 +7399,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::documentation(),
Self::VitestValidTitle(_) => VitestValidTitle::documentation(),
Self::VitestWarnTodo(_) => VitestWarnTodo::documentation(),
Self::NodeCallbackReturn(_) => NodeCallbackReturn::documentation(),
Self::NodeGlobalRequire(_) => NodeGlobalRequire::documentation(),
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::documentation(),
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::documentation(),
@@ -9655,6 +9663,8 @@ impl RuleEnum {
.or_else(|| VitestValidTitle::schema(generator)),
Self::VitestWarnTodo(_) => VitestWarnTodo::config_schema(generator)
.or_else(|| VitestWarnTodo::schema(generator)),
Self::NodeCallbackReturn(_) => NodeCallbackReturn::config_schema(generator)
.or_else(|| NodeCallbackReturn::schema(generator)),
Self::NodeGlobalRequire(_) => NodeGlobalRequire::config_schema(generator)
.or_else(|| NodeGlobalRequire::schema(generator)),
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::config_schema(generator)
@@ -10514,6 +10524,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => "vitest",
Self::VitestValidTitle(_) => "vitest",
Self::VitestWarnTodo(_) => "vitest",
Self::NodeCallbackReturn(_) => "node",
Self::NodeGlobalRequire(_) => "node",
Self::NodeHandleCallbackErr(_) => "node",
Self::NodeNoExportsAssign(_) => "node",
@@ -13042,6 +13053,9 @@ impl RuleEnum {
Self::VitestWarnTodo(_) => {
Ok(Self::VitestWarnTodo(VitestWarnTodo::from_configuration(value)?))
}
Self::NodeCallbackReturn(_) => {
Ok(Self::NodeCallbackReturn(NodeCallbackReturn::from_configuration(value)?))
}
Self::NodeGlobalRequire(_) => {
Ok(Self::NodeGlobalRequire(NodeGlobalRequire::from_configuration(value)?))
}
@@ -13913,6 +13927,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.to_configuration(),
Self::VitestValidTitle(rule) => rule.to_configuration(),
Self::VitestWarnTodo(rule) => rule.to_configuration(),
Self::NodeCallbackReturn(rule) => rule.to_configuration(),
Self::NodeGlobalRequire(rule) => rule.to_configuration(),
Self::NodeHandleCallbackErr(rule) => rule.to_configuration(),
Self::NodeNoExportsAssign(rule) => rule.to_configuration(),
@@ -14728,6 +14743,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run(node, ctx),
Self::VitestValidTitle(rule) => rule.run(node, ctx),
Self::VitestWarnTodo(rule) => rule.run(node, ctx),
Self::NodeCallbackReturn(rule) => rule.run(node, ctx),
Self::NodeGlobalRequire(rule) => rule.run(node, ctx),
Self::NodeHandleCallbackErr(rule) => rule.run(node, ctx),
Self::NodeNoExportsAssign(rule) => rule.run(node, ctx),
@@ -15536,6 +15552,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run(node, ctx),
Self::VitestValidTitle(rule) => rule.run(node, ctx),
Self::VitestWarnTodo(rule) => rule.run(node, ctx),
Self::NodeCallbackReturn(rule) => rule.run(node, ctx),
Self::NodeGlobalRequire(rule) => rule.run(node, ctx),
Self::NodeHandleCallbackErr(rule) => rule.run(node, ctx),
Self::NodeNoExportsAssign(rule) => rule.run(node, ctx),
@@ -16351,6 +16368,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run_once(ctx),
Self::VitestValidTitle(rule) => rule.run_once(ctx),
Self::VitestWarnTodo(rule) => rule.run_once(ctx),
Self::NodeCallbackReturn(rule) => rule.run_once(ctx),
Self::NodeGlobalRequire(rule) => rule.run_once(ctx),
Self::NodeHandleCallbackErr(rule) => rule.run_once(ctx),
Self::NodeNoExportsAssign(rule) => rule.run_once(ctx),
@@ -17159,6 +17177,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run_once(ctx),
Self::VitestValidTitle(rule) => rule.run_once(ctx),
Self::VitestWarnTodo(rule) => rule.run_once(ctx),
Self::NodeCallbackReturn(rule) => rule.run_once(ctx),
Self::NodeGlobalRequire(rule) => rule.run_once(ctx),
Self::NodeHandleCallbackErr(rule) => rule.run_once(ctx),
Self::NodeNoExportsAssign(rule) => rule.run_once(ctx),
@@ -18221,6 +18240,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::VitestValidTitle(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::VitestWarnTodo(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeCallbackReturn(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeGlobalRequire(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeHandleCallbackErr(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeNoExportsAssign(rule) => rule.run_on_jest_node(jest_node, ctx),
@@ -19281,6 +19301,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::VitestValidTitle(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::VitestWarnTodo(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeCallbackReturn(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeGlobalRequire(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeHandleCallbackErr(rule) => rule.run_on_jest_node(jest_node, ctx),
Self::NodeNoExportsAssign(rule) => rule.run_on_jest_node(jest_node, ctx),
@@ -20093,6 +20114,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.should_run(ctx),
Self::VitestValidTitle(rule) => rule.should_run(ctx),
Self::VitestWarnTodo(rule) => rule.should_run(ctx),
Self::NodeCallbackReturn(rule) => rule.should_run(ctx),
Self::NodeGlobalRequire(rule) => rule.should_run(ctx),
Self::NodeHandleCallbackErr(rule) => rule.should_run(ctx),
Self::NodeNoExportsAssign(rule) => rule.should_run(ctx),
@@ -21248,6 +21270,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::IS_TSGOLINT_RULE,
Self::VitestValidTitle(_) => VitestValidTitle::IS_TSGOLINT_RULE,
Self::VitestWarnTodo(_) => VitestWarnTodo::IS_TSGOLINT_RULE,
Self::NodeCallbackReturn(_) => NodeCallbackReturn::IS_TSGOLINT_RULE,
Self::NodeGlobalRequire(_) => NodeGlobalRequire::IS_TSGOLINT_RULE,
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::IS_TSGOLINT_RULE,
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::IS_TSGOLINT_RULE,
@@ -22227,6 +22250,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::VERSION,
Self::VitestValidTitle(_) => VitestValidTitle::VERSION,
Self::VitestWarnTodo(_) => VitestWarnTodo::VERSION,
Self::NodeCallbackReturn(_) => NodeCallbackReturn::VERSION,
Self::NodeGlobalRequire(_) => NodeGlobalRequire::VERSION,
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::VERSION,
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::VERSION,
@@ -23235,6 +23259,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(_) => VitestValidExpectInPromise::HAS_CONFIG,
Self::VitestValidTitle(_) => VitestValidTitle::HAS_CONFIG,
Self::VitestWarnTodo(_) => VitestWarnTodo::HAS_CONFIG,
Self::NodeCallbackReturn(_) => NodeCallbackReturn::HAS_CONFIG,
Self::NodeGlobalRequire(_) => NodeGlobalRequire::HAS_CONFIG,
Self::NodeHandleCallbackErr(_) => NodeHandleCallbackErr::HAS_CONFIG,
Self::NodeNoExportsAssign(_) => NodeNoExportsAssign::HAS_CONFIG,
@@ -24046,6 +24071,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.types_info(),
Self::VitestValidTitle(rule) => rule.types_info(),
Self::VitestWarnTodo(rule) => rule.types_info(),
Self::NodeCallbackReturn(rule) => rule.types_info(),
Self::NodeGlobalRequire(rule) => rule.types_info(),
Self::NodeHandleCallbackErr(rule) => rule.types_info(),
Self::NodeNoExportsAssign(rule) => rule.types_info(),
@@ -24851,6 +24877,7 @@ impl RuleEnum {
Self::VitestValidExpectInPromise(rule) => rule.run_info(),
Self::VitestValidTitle(rule) => rule.run_info(),
Self::VitestWarnTodo(rule) => rule.run_info(),
Self::NodeCallbackReturn(rule) => rule.run_info(),
Self::NodeGlobalRequire(rule) => rule.run_info(),
Self::NodeHandleCallbackErr(rule) => rule.run_info(),
Self::NodeNoExportsAssign(rule) => rule.run_info(),
@@ -25786,6 +25813,7 @@ pub static RULES: std::sync::LazyLock<Vec<RuleEnum>> = std::sync::LazyLock::new(
RuleEnum::VitestValidExpectInPromise(VitestValidExpectInPromise::default()),
RuleEnum::VitestValidTitle(VitestValidTitle::default()),
RuleEnum::VitestWarnTodo(VitestWarnTodo::default()),
RuleEnum::NodeCallbackReturn(NodeCallbackReturn::default()),
RuleEnum::NodeGlobalRequire(NodeGlobalRequire::default()),
RuleEnum::NodeHandleCallbackErr(NodeHandleCallbackErr::default()),
RuleEnum::NodeNoExportsAssign(NodeNoExportsAssign::default()),
+1
View File
@@ -817,6 +817,7 @@ pub(crate) mod vitest {
}
pub(crate) mod node {
pub mod callback_return;
pub mod global_require;
pub mod handle_callback_err;
pub mod no_exports_assign;
@@ -0,0 +1,461 @@
use schemars::JsonSchema;
use serde::Deserialize;
use oxc_ast::{
AstKind,
ast::{CallExpression, Expression, Statement},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use oxc_str::CompactStr;
use crate::{
AstNode,
context::LintContext,
rule::{DefaultRuleConfig, Rule},
};
fn callback_return_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Expected return with your callback function.")
.with_help("Return the callback call or add an explicit return immediately after it.")
.with_label(span)
}
#[derive(Debug, Clone, JsonSchema, Deserialize)]
/// The rule takes a single option - an array of possible callback names - which may include object methods. The default callback names are `callback`, `cb`, `next`.
pub struct CallbackReturn(Box<CallbackNames>);
#[derive(Debug, Clone, JsonSchema, Deserialize)]
#[serde(transparent)]
struct CallbackNames(Vec<CompactStr>);
impl Default for CallbackReturn {
fn default() -> Self {
Self(Box::new(CallbackNames(vec!["callback".into(), "cb".into(), "next".into()])))
}
}
declare_oxc_lint!(
/// ### What it does
///
/// Require `return` statements after callbacks.
///
/// ### Why is this bad?
///
/// This rule is aimed at ensuring that callbacks used outside of the main function block are always part-of or immediately preceding a `return` statement.
/// This rule decides what is a callback based on the name of the function being called.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// function done(err) {
/// if (err) {
/// callback(err);
/// }
/// callback();
/// }
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// function done(err) {
/// if (err) {
/// return callback(err);
/// }
/// callback();
/// }
/// ```
///
/// ### Known Limitations
///
/// Because it is difficult to understand the meaning of a program through static analysis, this rule has limitations:
///
/// - *false negatives* when this rule reports correct code, but the program calls the callback more than one time (which is incorrect behavior)
/// - *false positives* when this rule reports incorrect code, but the program calls the callback only one time (which is correct behavior)
///
/// #### Passing the callback by reference
///
/// The static analysis of this rule does not detect that the program calls the callback if it is an argument of a function (for example, `setTimeout`).
///
/// Example of a *false negative* when this rule reports correct code:
///
/// ```js
/// function foo(err, callback) {
/// if (err) {
/// setTimeout(callback, 0); // this is bad, but WILL NOT warn
/// }
/// callback();
/// }
/// ```
///
/// #### Triggering the callback within a nested function
///
/// The static analysis of this rule does not detect that the program calls the callback from within a nested function or an immediately-invoked function expression (IIFE).
///
/// Example of a *false negative* when this rule reports correct code:
///
/// ```js
/// function foo(err, callback) {
/// if (err) {
/// process.nextTick(function() {
/// return callback(); // this is bad, but WILL NOT warn
/// });
/// }
/// callback();
/// }
/// ```
///
/// #### If/else statements
///
/// The static analysis of this rule does not detect that the program calls the callback only one time in each branch of an `if` statement.
///
/// Example of a *false positive* when this rule reports incorrect code:
///
/// ```js
/// function foo(err, callback) {
/// if (err) {
/// callback(err); // this is fine, but WILL warn
/// } else {
/// callback(); // this is fine, but WILL warn
/// }
/// }
/// ```
CallbackReturn,
node,
style,
version = "next",
config = CallbackReturn,
);
impl Rule for CallbackReturn {
fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};
if !self.is_callback(call_expr, ctx.source_text()) {
return;
}
let Some(closest_block) = find_closest_block_parent(node, ctx) else {
return;
};
if matches!(
closest_block.kind(),
AstKind::ReturnStatement(_) | AstKind::ArrowFunctionExpression(_)
) {
return;
}
let block_body = match closest_block.kind() {
AstKind::BlockStatement(block_stmt) => block_stmt.body.as_slice(),
AstKind::FunctionBody(function_body) => function_body.statements.as_slice(),
_ => &[],
};
if let Some(last_item) = block_body.last() {
if is_callback_expression(call_expr, last_item)
&& is_function_like(&ctx.nodes().parent_node(closest_block.id()).kind())
{
return;
}
if matches!(last_item, Statement::ReturnStatement(_))
&& block_body
.get(block_body.len().saturating_sub(2))
.is_some_and(|previous_item| is_callback_expression(call_expr, previous_item))
{
return;
}
}
if find_closest_function_parent(node, ctx).is_some() {
ctx.diagnostic(callback_return_diagnostic(call_expr.span));
}
}
}
impl CallbackReturn {
fn is_callback(&self, call_expr: &CallExpression, source_text: &str) -> bool {
contains_only_identifiers(&call_expr.callee)
&& self.0.0.iter().any(|callback| {
// compare by source text instead of `callee_name()` to handle cases like `obj.method(err)` with config `["obj.method"]`
callback.as_str() == call_expr.callee.span().source_text(source_text)
})
}
}
fn contains_only_identifiers(expr: &Expression) -> bool {
if let Expression::Identifier(_) = expr {
return true;
}
if let Some(member_expr) = &expr.as_member_expression() {
if matches!(member_expr.object(), Expression::Identifier(_)) {
return true;
}
if let Some(obj_member_expr) = member_expr.object().as_member_expression() {
return contains_only_identifiers(obj_member_expr.object());
}
}
false
}
fn find_closest_block_parent<'a, 'b>(
ast_node: &AstNode<'a>,
ctx: &'b LintContext<'a>,
) -> Option<&'b AstNode<'a>> {
ctx.nodes().ancestors(ast_node.id()).find(|parent| {
matches!(
parent.kind(),
AstKind::BlockStatement(_)
| AstKind::FunctionBody(_)
| AstKind::ReturnStatement(_)
| AstKind::ArrowFunctionExpression(_)
)
})
}
fn find_closest_function_parent<'a, 'b>(
ast_node: &AstNode<'a>,
ctx: &'b LintContext<'a>,
) -> Option<&'b AstNode<'a>> {
ctx.nodes().ancestors(ast_node.id()).find(|parent| is_function_like(&parent.kind()))
}
fn is_function_like(kind: &AstKind) -> bool {
matches!(kind, AstKind::Function(_) | AstKind::ArrowFunctionExpression(_))
}
fn is_callback_expression(call_expr: &CallExpression, parent_node: &Statement) -> bool {
let Statement::ExpressionStatement(expr_stmt) = parent_node else {
return false;
};
if expr_stmt.expression.without_parentheses().span() == call_expr.span {
return true;
}
match expr_stmt.expression.without_parentheses() {
Expression::BinaryExpression(binary_expr) => {
binary_expr.right.without_parentheses().span() == call_expr.span
}
Expression::LogicalExpression(logical_expr) => {
logical_expr.right.without_parentheses().span() == call_expr.span
}
_ => false,
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
("function a(err) { if (err) return callback (err); }", None),
("function a(err) { if (err) return callback (err); callback(); }", None),
("function a(err) { if (err) { return callback (err); } callback(); }", None),
(
"function a(err) { if (err) { return /* confusing comment */ callback (err); } callback(); }",
None,
),
("function x(err) { if (err) { callback(); return; } }", None),
(
"function x(err) { if (err) {
log();
callback(); return; } }",
None,
),
("function x(err) { if (err) { callback(); return; } return callback(); }", None),
("function x(err) { if (err) { return callback(); } else { return callback(); } }", None),
(
"function x(err) { if (err) { return callback(); } else if (x) { return callback(); } }",
None,
),
("function x(err) { if (err) return callback(); else return callback(); }", None),
("function x(cb) { cb && cb(); }", None),
("function x(next) { typeof next !== 'undefined' && next(); }", None),
("function x(next) { if (typeof next === 'function') { return next() } }", None),
("function x() { switch(x) { case 'a': return next(); } }", None),
("function x() { for(x = 0; x < 10; x++) { return next(); } }", None),
("function x() { while(x) { return next(); } }", None),
("function a(err) { if (err) { obj.method (err); } }", None),
("callback()", None),
("callback(); callback();", None),
("while(x) { move(); }", None),
("for (var i = 0; i < 10; i++) { move(); }", None),
("for (var i = 0; i < 10; i++) move();", None),
("if (x) callback();", None),
("if (x) { callback(); }", None),
("var x = err => { if (err) { callback(); return; } }", None),
("var x = err => callback(err)", None),
("var x = err => { setTimeout( () => { callback(); }); }", None),
("class x { horse() { callback(); } } ", None),
("class x { horse() { if (err) { return callback(); } callback(); } } ", None),
("function a(err) { if (err) { callback(err) } }", Some(serde_json::json!([["cb"]]))),
(
"function a(err) { if (err) { callback(err) } next(); }",
Some(serde_json::json!([["cb", "next"]])),
),
(
"function a(err) { if (err) { return next(err) } else { callback(); } }",
Some(serde_json::json!([["cb", "next"]])),
),
(
"function a(err) { if (err) { return obj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { return obj.prop.method(err); } }",
Some(serde_json::json!([["obj.prop.method"]])),
),
(
"function a(err) { if (err) { return obj.prop.method(err); } otherObj.prop.method() }",
Some(serde_json::json!([["obj.prop.method", "otherObj.prop.method"]])),
),
(
"function a(err) { if (err) { callback(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { otherObj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { //comment
return obj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { /*comment*/return obj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { return obj.method(err); //comment
} }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { return obj.method(err); /*comment*/ } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { obj().method(err); } }",
Some(serde_json::json!([["obj().method"]])),
),
(
"function a(err) { if (err) { obj.prop().method(err); } }",
Some(serde_json::json!([["obj.prop().method"]])),
),
(
"function a(err) { if (err) { obj().prop.method(err); } }",
Some(serde_json::json!([["obj().prop.method"]])),
),
(
"function a(err) { if (err) { obj().method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { obj().method(err); } obj.method(); }",
Some(serde_json::json!([["obj.method"]])),
),
("function x(err) { if (err) { setTimeout(callback, 0); } callback(); }", None),
(
"function x(err) { if (err) { process.nextTick(function(err) { callback(); }); } callback(); }",
None,
),
("function a(err) { (callback(err)); }", None),
("function x(err) { if (err) { (callback()); return; } }", None),
];
let fail = vec![
("function a(err) { if (err) { callback (err); } }", None),
("function a(callback) { if (typeof callback !== 'undefined') { callback(); } }", None),
("function a(callback) { if (typeof callback !== 'undefined') callback(); }", None),
("function a(callback) { if (err) { callback(); horse && horse(); } }", None),
("var x = (err) => { if (err) { callback (err); } }", None),
("var x = { x(err) { if (err) { callback (err); } } }", None),
(
"function x(err) { if (err) {
log();
callback(err); } }",
None,
),
("var x = { x(err) { if (err) { callback && callback (err); } } }", None),
("function a(err) { callback (err); callback(); }", None),
("function a(err) { callback (err); horse(); }", None),
("function a(err) { if (err) { callback (err); horse(); return; } }", None),
("var a = (err) => { callback (err); callback(); }", None),
(
"function a(err) { if (err) { callback (err); } else if (x) { callback(err); return; } }",
None,
),
(
"function x(err) { if (err) { return callback(); }
else if (abc) {
callback(); }
else {
return callback(); } }",
None,
),
("class x { horse() { if (err) { callback(); } callback(); } } ", None),
("function x(err) { if (err) { callback() } else { callback() } }", None),
("function x(err) { if (err) return callback(); else callback(); }", None),
("() => { if (x) { callback(); } }", None),
("function b() { switch(x) { case 'horse': callback(); } }", None),
(
"function a() { switch(x) { case 'horse': move(); } }",
Some(serde_json::json!([["move"]])),
),
("var x = function() { while(x) { move(); } }", Some(serde_json::json!([["move"]]))),
(
"function x() { for (var i = 0; i < 10; i++) { move(); } }",
Some(serde_json::json!([["move"]])),
),
(
"var x = function() { for (var i = 0; i < 10; i++) move(); }",
Some(serde_json::json!([["move"]])),
),
(
"function a(err) { if (err) { obj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { obj.prop.method(err); } }",
Some(serde_json::json!([["obj.prop.method"]])),
),
(
"function a(err) { if (err) { obj.prop.method(err); } otherObj.prop.method() }",
Some(serde_json::json!([["obj.prop.method", "otherObj.prop.method"]])),
),
(
"function a(err) { if (err) { /*comment*/obj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { //comment
obj.method(err); } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { obj.method(err); /*comment*/ } }",
Some(serde_json::json!([["obj.method"]])),
),
(
"function a(err) { if (err) { obj.method(err); //comment
} }",
Some(serde_json::json!([["obj.method"]])),
),
];
Tester::new(CallbackReturn::NAME, CallbackReturn::PLUGIN, pass, fail).test_and_snapshot();
}
@@ -0,0 +1,226 @@
---
source: crates/oxc_linter/src/tester.rs
assertion_line: 490
---
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { callback (err); } }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:63]
1 │ function a(callback) { if (typeof callback !== 'undefined') { callback(); } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:61]
1 │ function a(callback) { if (typeof callback !== 'undefined') callback(); }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:35]
1 │ function a(callback) { if (err) { callback(); horse && horse(); } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:31]
1 │ var x = (err) => { if (err) { callback (err); } }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:31]
1 │ var x = { x(err) { if (err) { callback (err); } } }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:3:14]
2 │ log();
3 │ callback(err); } }
· ─────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:43]
1 │ var x = { x(err) { if (err) { callback && callback (err); } } }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:19]
1 │ function a(err) { callback (err); callback(); }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:19]
1 │ function a(err) { callback (err); horse(); }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { callback (err); horse(); return; } }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:20]
1 │ var a = (err) => { callback (err); callback(); }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { callback (err); } else if (x) { callback(err); return; } }
· ──────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:3:13]
2 │ else if (abc) {
3 │ callback(); }
· ──────────
4 │ else {
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:32]
1 │ class x { horse() { if (err) { callback(); } callback(); } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function x(err) { if (err) { callback() } else { callback() } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:50]
1 │ function x(err) { if (err) { callback() } else { callback() } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:52]
1 │ function x(err) { if (err) return callback(); else callback(); }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:18]
1 │ () => { if (x) { callback(); } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:42]
1 │ function b() { switch(x) { case 'horse': callback(); } }
· ──────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:42]
1 │ function a() { switch(x) { case 'horse': move(); } }
· ──────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:33]
1 │ var x = function() { while(x) { move(); } }
· ──────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:47]
1 │ function x() { for (var i = 0; i < 10; i++) { move(); } }
· ──────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:51]
1 │ var x = function() { for (var i = 0; i < 10; i++) move(); }
· ──────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { obj.method(err); } }
· ───────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { obj.prop.method(err); } }
· ────────────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { obj.prop.method(err); } otherObj.prop.method() }
· ────────────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:41]
1 │ function a(err) { if (err) { /*comment*/obj.method(err); } }
· ───────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:2:13]
1 │ function a(err) { if (err) { //comment
2 │ obj.method(err); } }
· ───────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { obj.method(err); /*comment*/ } }
· ───────────────
╰────
help: Return the callback call or add an explicit return immediately after it.
⚠ node(callback-return): Expected return with your callback function.
╭─[callback_return.tsx:1:30]
1 │ function a(err) { if (err) { obj.method(err); //comment
· ───────────────
2 │ } }
╰────
help: Return the callback call or add an explicit return immediately after it.
+3
View File
@@ -1219,6 +1219,9 @@
"no-with": {
"$ref": "#/definitions/DummyRule"
},
"node/callback-return": {
"$ref": "#/definitions/DummyRule"
},
"node/global-require": {
"$ref": "#/definitions/DummyRule"
},
@@ -1223,6 +1223,9 @@ expression: json
"no-with": {
"$ref": "#/definitions/DummyRule"
},
"node/callback-return": {
"$ref": "#/definitions/DummyRule"
},
"node/global-require": {
"$ref": "#/definitions/DummyRule"
},