Skip to content

Commit 5992b75

Browse files
jellymysteryven
andauthored
feat(linter): implement eslint-plugin-promise/no-return-in-finally, prefer-await-to-then rule (#4318)
Part of #4252 --------- Co-authored-by: wenzhe <[email protected]>
1 parent c519295 commit 5992b75

File tree

5 files changed

+305
-0
lines changed

5 files changed

+305
-0
lines changed

crates/oxc_linter/src/rules.rs

+4
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,9 @@ mod tree_shaking {
444444
mod promise {
445445
pub mod avoid_new;
446446
pub mod no_new_statics;
447+
pub mod no_return_in_finally;
447448
pub mod param_names;
449+
pub mod prefer_await_to_then;
448450
pub mod valid_params;
449451
}
450452

@@ -857,6 +859,8 @@ oxc_macros::declare_all_lint_rules! {
857859
promise::no_new_statics,
858860
promise::param_names,
859861
promise::valid_params,
862+
promise::no_return_in_finally,
863+
promise::prefer_await_to_then,
860864
vitest::no_import_node_test,
861865
vitest::prefer_to_be_falsy,
862866
vitest::prefer_to_be_truthy,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use oxc_allocator::Box as OBox;
2+
use oxc_ast::{
3+
ast::{Expression, FunctionBody, Statement},
4+
AstKind,
5+
};
6+
use oxc_diagnostics::OxcDiagnostic;
7+
use oxc_macros::declare_oxc_lint;
8+
use oxc_span::Span;
9+
10+
use crate::{context::LintContext, rule::Rule, utils::is_promise, AstNode};
11+
12+
fn no_return_in_finally_diagnostic(span0: Span) -> OxcDiagnostic {
13+
OxcDiagnostic::warn("Don't return in a finally callback")
14+
.with_help("Remove the return statement as nothing can consume the return value")
15+
.with_label(span0)
16+
}
17+
18+
#[derive(Debug, Default, Clone)]
19+
pub struct NoReturnInFinally;
20+
21+
declare_oxc_lint!(
22+
/// ### What it does
23+
///
24+
/// Disallow return statements in a finally() callback of a promise.
25+
///
26+
/// ### Why is this bad?
27+
///
28+
/// Disallow return statements inside a callback passed to finally(), since nothing would
29+
/// consume what's returned.
30+
///
31+
/// ### Example
32+
/// ```javascript
33+
/// myPromise.finally(function (val) {
34+
/// return val
35+
/// })
36+
/// ```
37+
NoReturnInFinally,
38+
nursery,
39+
);
40+
41+
impl Rule for NoReturnInFinally {
42+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
43+
let AstKind::CallExpression(call_expr) = node.kind() else {
44+
return;
45+
};
46+
47+
let Some(prop_name) = is_promise(call_expr) else {
48+
return;
49+
};
50+
51+
if prop_name != "finally" {
52+
return;
53+
}
54+
55+
for argument in &call_expr.arguments {
56+
let Some(arg_expr) = argument.as_expression() else {
57+
continue;
58+
};
59+
match arg_expr {
60+
Expression::ArrowFunctionExpression(arrow_expr) => {
61+
find_return_statement(&arrow_expr.body, ctx);
62+
}
63+
Expression::FunctionExpression(func_expr) => {
64+
let Some(func_body) = &func_expr.body else {
65+
continue;
66+
};
67+
find_return_statement(func_body, ctx);
68+
}
69+
_ => continue,
70+
}
71+
}
72+
}
73+
}
74+
75+
fn find_return_statement<'a>(func_body: &OBox<'_, FunctionBody<'a>>, ctx: &LintContext<'a>) {
76+
let Some(return_stmt) =
77+
func_body.statements.iter().find(|stmt| matches!(stmt, Statement::ReturnStatement(_)))
78+
else {
79+
return;
80+
};
81+
82+
let Statement::ReturnStatement(stmt) = return_stmt else {
83+
return;
84+
};
85+
86+
ctx.diagnostic(no_return_in_finally_diagnostic(stmt.span));
87+
}
88+
89+
#[test]
90+
fn test() {
91+
use crate::tester::Tester;
92+
93+
let pass = vec![
94+
"Promise.resolve(1).finally(() => { console.log(2) })",
95+
"Promise.reject(4).finally(() => { console.log(2) })",
96+
"Promise.reject(4).finally(() => {})",
97+
"myPromise.finally(() => {});",
98+
"Promise.resolve(1).finally(function () { })",
99+
];
100+
101+
let fail = vec![
102+
"Promise.resolve(1).finally(() => { return 2 })",
103+
"Promise.reject(0).finally(() => { return 2 })",
104+
"myPromise.finally(() => { return 2 });",
105+
"Promise.resolve(1).finally(function () { return 2 })",
106+
];
107+
108+
Tester::new(NoReturnInFinally::NAME, pass, fail).test_and_snapshot();
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use oxc_ast::AstKind;
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::Span;
5+
6+
fn prefer_wait_to_then_diagnostic(span0: Span) -> OxcDiagnostic {
7+
OxcDiagnostic::warn("Prefer await to then()/catch()/finally()").with_label(span0)
8+
}
9+
10+
use crate::{context::LintContext, rule::Rule, utils::is_promise, AstNode};
11+
12+
#[derive(Debug, Default, Clone)]
13+
pub struct PreferAwaitToThen;
14+
15+
declare_oxc_lint!(
16+
/// ### What it does
17+
///
18+
/// Prefer `await` to `then()`/`catch()`/`finally()` for reading Promise values
19+
///
20+
/// ### Why is this bad?
21+
///
22+
/// Async/await syntax can be seen as more readable.
23+
///
24+
/// ### Example
25+
/// ```javascript
26+
/// myPromise.then(doSomething)
27+
/// ```
28+
PreferAwaitToThen,
29+
style,
30+
);
31+
32+
fn is_inside_yield_or_await(node: &AstNode) -> bool {
33+
matches!(node.kind(), AstKind::YieldExpression(_) | AstKind::AwaitExpression(_))
34+
}
35+
36+
impl Rule for PreferAwaitToThen {
37+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
38+
let AstKind::CallExpression(call_expr) = node.kind() else {
39+
return;
40+
};
41+
42+
if is_promise(call_expr).is_none() {
43+
return;
44+
}
45+
46+
// Already inside a yield or await
47+
if ctx
48+
.nodes()
49+
.ancestors(node.id())
50+
.any(|node_id| is_inside_yield_or_await(ctx.nodes().get_node(node_id)))
51+
{
52+
return;
53+
}
54+
55+
ctx.diagnostic(prefer_wait_to_then_diagnostic(call_expr.span));
56+
}
57+
}
58+
59+
#[test]
60+
fn test() {
61+
use crate::tester::Tester;
62+
63+
let pass = vec![
64+
"async function hi() { await thing() }",
65+
"async function hi() { await thing().then() }",
66+
"async function hi() { await thing().catch() }",
67+
"a = async () => (await something())",
68+
"a = async () => {
69+
try { await something() } catch (error) { somethingElse() }
70+
}",
71+
// <https://github.com/tc39/proposal-top-level-await>
72+
// Top level await is allowed now, so comment this out
73+
// "something().then(async () => await somethingElse())",
74+
"function foo() { hey.somethingElse(x => {}) }",
75+
"const isThenable = (obj) => {
76+
return obj && typeof obj.then === 'function';
77+
};",
78+
"function isThenable(obj) {
79+
return obj && typeof obj.then === 'function';
80+
}",
81+
];
82+
83+
let fail = vec![
84+
"function foo() { hey.then(x => {}) }",
85+
"function foo() { hey.then(function() { }).then() }",
86+
"function foo() { hey.then(function() { }).then(x).catch() }",
87+
"async function a() { hey.then(function() { }).then(function() { }) }",
88+
"function foo() { hey.catch(x => {}) }",
89+
"function foo() { hey.finally(x => {}) }",
90+
"something().then(async () => await somethingElse())",
91+
];
92+
93+
Tester::new(PreferAwaitToThen::NAME, pass, fail).test_and_snapshot();
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-promise(no-return-in-finally): Don't return in a finally callback
5+
╭─[no_return_in_finally.tsx:1:36]
6+
1Promise.resolve(1).finally(() => { return 2 })
7+
· ────────
8+
╰────
9+
help: Remove the return statement as nothing can consume the return value
10+
11+
eslint-plugin-promise(no-return-in-finally): Don't return in a finally callback
12+
╭─[no_return_in_finally.tsx:1:35]
13+
1Promise.reject(0).finally(() => { return 2 })
14+
· ────────
15+
╰────
16+
help: Remove the return statement as nothing can consume the return value
17+
18+
eslint-plugin-promise(no-return-in-finally): Don't return in a finally callback
19+
╭─[no_return_in_finally.tsx:1:27]
20+
1myPromise.finally(() => { return 2 });
21+
· ────────
22+
╰────
23+
help: Remove the return statement as nothing can consume the return value
24+
25+
eslint-plugin-promise(no-return-in-finally): Don't return in a finally callback
26+
╭─[no_return_in_finally.tsx:1:42]
27+
1Promise.resolve(1).finally(function () { return 2 })
28+
· ────────
29+
╰────
30+
help: Remove the return statement as nothing can consume the return value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
5+
╭─[prefer_await_to_then.tsx:1:18]
6+
1function foo() { hey.then(x => {}) }
7+
· ─────────────────
8+
╰────
9+
10+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
11+
╭─[prefer_await_to_then.tsx:1:18]
12+
1function foo() { hey.then(function() { }).then() }
13+
· ───────────────────────────────
14+
╰────
15+
16+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
17+
╭─[prefer_await_to_then.tsx:1:18]
18+
1function foo() { hey.then(function() { }).then() }
19+
· ────────────────────────
20+
╰────
21+
22+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
23+
╭─[prefer_await_to_then.tsx:1:18]
24+
1function foo() { hey.then(function() { }).then(x).catch() }
25+
· ────────────────────────────────────────
26+
╰────
27+
28+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
29+
╭─[prefer_await_to_then.tsx:1:18]
30+
1function foo() { hey.then(function() { }).then(x).catch() }
31+
· ────────────────────────────────
32+
╰────
33+
34+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
35+
╭─[prefer_await_to_then.tsx:1:18]
36+
1function foo() { hey.then(function() { }).then(x).catch() }
37+
· ────────────────────────
38+
╰────
39+
40+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
41+
╭─[prefer_await_to_then.tsx:1:22]
42+
1async function a() { hey.then(function() { }).then(function() { }) }
43+
· ─────────────────────────────────────────────
44+
╰────
45+
46+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
47+
╭─[prefer_await_to_then.tsx:1:22]
48+
1async function a() { hey.then(function() { }).then(function() { }) }
49+
· ────────────────────────
50+
╰────
51+
52+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
53+
╭─[prefer_await_to_then.tsx:1:18]
54+
1function foo() { hey.catch(x => {}) }
55+
· ──────────────────
56+
╰────
57+
58+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
59+
╭─[prefer_await_to_then.tsx:1:18]
60+
1function foo() { hey.finally(x => {}) }
61+
· ────────────────────
62+
╰────
63+
64+
eslint-plugin-promise(prefer-await-to-then): Prefer await to then()/catch()/finally()
65+
╭─[prefer_await_to_then.tsx:1:1]
66+
1something().then(async () => await somethingElse())
67+
· ───────────────────────────────────────────────────
68+
╰────

0 commit comments

Comments
 (0)