Skip to content

Commit 29282db

Browse files
committed
Add unused_async_trait_impl lint
1 parent 80fce9b commit 29282db

File tree

7 files changed

+262
-0
lines changed

7 files changed

+262
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7089,6 +7089,7 @@ Released 2018-09-13
70897089
[`unstable_as_mut_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#unstable_as_mut_slice
70907090
[`unstable_as_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#unstable_as_slice
70917091
[`unused_async`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_async
7092+
[`unused_async_trait_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_async_trait_impl
70927093
[`unused_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_collect
70937094
[`unused_enumerate_index`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_enumerate_index
70947095
[`unused_format_specs`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_format_specs

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
768768
crate::unnested_or_patterns::UNNESTED_OR_PATTERNS_INFO,
769769
crate::unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME_INFO,
770770
crate::unused_async::UNUSED_ASYNC_INFO,
771+
crate::unused_async_trait_impl::UNUSED_ASYNC_TRAIT_IMPL_INFO,
771772
crate::unused_io_amount::UNUSED_IO_AMOUNT_INFO,
772773
crate::unused_peekable::UNUSED_PEEKABLE_INFO,
773774
crate::unused_result_ok::UNUSED_RESULT_OK_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ mod unneeded_struct_pattern;
375375
mod unnested_or_patterns;
376376
mod unsafe_removed_from_name;
377377
mod unused_async;
378+
mod unused_async_trait_impl;
378379
mod unused_io_amount;
379380
mod unused_peekable;
380381
mod unused_result_ok;
@@ -852,6 +853,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
852853
Box::new(|_| Box::new(volatile_composites::VolatileComposites)),
853854
Box::new(|_| Box::<replace_box::ReplaceBox>::default()),
854855
Box::new(move |_| Box::new(manual_ilog2::ManualIlog2::new(conf))),
856+
Box::new(|_| Box::new(unused_async_trait_impl::UnusedAsyncTraitImpl)),
855857
// add late passes here, used by `cargo dev new_lint`
856858
];
857859
store.late_passes.extend(late_lints);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use clippy_utils::diagnostics::span_lint_hir_and_then;
2+
use clippy_utils::is_def_id_trait_method;
3+
use clippy_utils::source::SpanRangeExt;
4+
use clippy_utils::usage::is_todo_unimplemented_stub;
5+
use rustc_errors::Applicability;
6+
use rustc_hir::intravisit::{FnKind, Visitor, walk_expr, walk_fn};
7+
use rustc_hir::{
8+
Body, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Defaultness, Expr, ExprKind, FnDecl, IsAsync, Node,
9+
TraitItem, YieldSource,
10+
};
11+
use rustc_lint::{LateContext, LateLintPass};
12+
use rustc_middle::hir::nested_filter;
13+
use rustc_session::impl_lint_pass;
14+
use rustc_span::Span;
15+
use rustc_span::def_id::LocalDefId;
16+
17+
declare_clippy_lint! {
18+
/// ### What it does
19+
/// Checks for trait function implementations that are declared `async` but have no `.await`s inside of them.
20+
///
21+
/// ### Why is this bad?
22+
/// Async functions with no async code create computational overhead.
23+
/// Even though the trait requires the function to return a future,
24+
/// returning a `core::future::ready` with the result is more efficient.
25+
///
26+
/// ### Example
27+
/// ```no_run
28+
/// trait AsyncTrait {
29+
/// async fn get_random_number() -> i64;
30+
/// }
31+
///
32+
/// impl AsyncTrait for () {
33+
/// async fn get_random_number() -> i64 {
34+
/// 4 // Chosen by fair dice roll. Guaranteed to be random.
35+
/// }
36+
/// }
37+
/// ```
38+
///
39+
/// Use instead:
40+
/// ```no_run
41+
/// trait AsyncTrait {
42+
/// async fn get_random_number() -> i64;
43+
/// }
44+
///
45+
/// impl AsyncTrait for () {
46+
/// fn get_random_number() -> impl Future<Output = i64> {
47+
/// core::future::ready(4) // Chosen by fair dice roll. Guaranteed to be random.
48+
/// }
49+
/// }
50+
/// ```
51+
///
52+
/// ### Note
53+
/// An `async` block generates code that defers execution until the Future is polled.
54+
/// When using `core::future::ready` the code is executed immediately.
55+
#[clippy::version = "1.94.0"]
56+
pub UNUSED_ASYNC_TRAIT_IMPL,
57+
pedantic,
58+
"finds async trait impl functions with no await statements"
59+
}
60+
61+
pub struct UnusedAsyncTraitImpl;
62+
63+
impl_lint_pass!(UnusedAsyncTraitImpl => [UNUSED_ASYNC_TRAIT_IMPL]);
64+
65+
struct AsyncFnVisitor<'a, 'tcx> {
66+
cx: &'a LateContext<'tcx>,
67+
found_await: bool,
68+
async_depth: usize,
69+
}
70+
71+
impl<'tcx> Visitor<'tcx> for AsyncFnVisitor<'_, 'tcx> {
72+
type NestedFilter = nested_filter::OnlyBodies;
73+
74+
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) {
75+
if let ExprKind::Yield(_, YieldSource::Await { .. }) = ex.kind
76+
&& self.async_depth == 1
77+
{
78+
self.found_await = true;
79+
}
80+
81+
let is_async_block = matches!(
82+
ex.kind,
83+
ExprKind::Closure(Closure {
84+
kind: ClosureKind::Coroutine(CoroutineKind::Desugared(CoroutineDesugaring::Async, _)),
85+
..
86+
})
87+
);
88+
89+
if is_async_block {
90+
self.async_depth += 1;
91+
}
92+
93+
walk_expr(self, ex);
94+
95+
if is_async_block {
96+
self.async_depth -= 1;
97+
}
98+
}
99+
100+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
101+
self.cx.tcx
102+
}
103+
}
104+
105+
impl<'tcx> LateLintPass<'tcx> for UnusedAsyncTraitImpl {
106+
fn check_fn(
107+
&mut self,
108+
cx: &LateContext<'tcx>,
109+
fn_kind: FnKind<'tcx>,
110+
fn_decl: &'tcx FnDecl<'tcx>,
111+
body: &Body<'tcx>,
112+
span: Span,
113+
def_id: LocalDefId,
114+
) {
115+
if let IsAsync::Async(async_span) = fn_kind.asyncness()
116+
&& !span.from_expansion()
117+
&& is_def_id_trait_method(cx, def_id)
118+
&& !is_default_trait_impl(cx, def_id)
119+
&& !async_fn_contains_todo_unimplemented_macro(cx, body)
120+
{
121+
let mut visitor = AsyncFnVisitor {
122+
cx,
123+
found_await: false,
124+
async_depth: 0,
125+
};
126+
walk_fn(&mut visitor, fn_kind, fn_decl, body.id(), def_id);
127+
if !visitor.found_await {
128+
span_lint_hir_and_then(
129+
cx,
130+
UNUSED_ASYNC_TRAIT_IMPL,
131+
cx.tcx.local_def_id_to_hir_id(def_id),
132+
span,
133+
"unused `async` for async trait impl function with no await statements",
134+
|diag| {
135+
if let Some(output_src) = fn_decl.output.span().get_source_text(cx)
136+
&& let Some(body_src) = body.value.span.get_source_text(cx)
137+
{
138+
let output_str = output_src.as_str();
139+
let body_str = body_src.as_str();
140+
141+
let sugg = vec![
142+
(async_span, String::new()),
143+
(fn_decl.output.span(), format!("impl Future<Output = {output_str}>")),
144+
(body.value.span, format!("{{ core::future::ready({body_str}) }}")),
145+
];
146+
147+
diag.help("a Future can be constructed from the return value with `core::future::ready`");
148+
diag.multipart_suggestion(
149+
format!("consider removing the `async` from this function and returning `impl Future<Output = {output_str}>` instead"),
150+
sugg,
151+
Applicability::MachineApplicable
152+
);
153+
}
154+
},
155+
);
156+
}
157+
}
158+
}
159+
}
160+
161+
fn is_default_trait_impl(cx: &LateContext<'_>, def_id: LocalDefId) -> bool {
162+
matches!(
163+
cx.tcx.hir_node_by_def_id(def_id),
164+
Node::TraitItem(TraitItem {
165+
defaultness: Defaultness::Default { .. },
166+
..
167+
})
168+
)
169+
}
170+
171+
fn async_fn_contains_todo_unimplemented_macro(cx: &LateContext<'_>, body: &Body<'_>) -> bool {
172+
if let ExprKind::Closure(closure) = body.value.kind
173+
&& let ClosureKind::Coroutine(CoroutineKind::Desugared(CoroutineDesugaring::Async, _)) = closure.kind
174+
&& let body = cx.tcx.hir_body(closure.body)
175+
&& let ExprKind::Block(block, _) = body.value.kind
176+
&& block.stmts.is_empty()
177+
&& let Some(expr) = block.expr
178+
&& let ExprKind::DropTemps(inner) = expr.kind
179+
{
180+
return is_todo_unimplemented_stub(cx, inner);
181+
}
182+
183+
false
184+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#![warn(clippy::unused_async_trait_impl)]
2+
3+
trait HasAsyncMethod {
4+
async fn do_something() -> u32;
5+
}
6+
7+
struct Inefficient;
8+
struct Efficient;
9+
10+
impl HasAsyncMethod for Inefficient {
11+
fn do_something() -> impl Future<Output = u32> { core::future::ready({
12+
//~^ unused_async_trait_impl
13+
1
14+
}) }
15+
}
16+
17+
impl HasAsyncMethod for Efficient {
18+
fn do_something() -> impl Future<Output = u32> {
19+
core::future::ready(1)
20+
}
21+
}
22+
23+
fn main() {
24+
Inefficient::do_something();
25+
Efficient::do_something();
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#![warn(clippy::unused_async_trait_impl)]
2+
3+
trait HasAsyncMethod {
4+
async fn do_something() -> u32;
5+
}
6+
7+
struct Inefficient;
8+
struct Efficient;
9+
10+
impl HasAsyncMethod for Inefficient {
11+
async fn do_something() -> u32 {
12+
//~^ unused_async_trait_impl
13+
1
14+
}
15+
}
16+
17+
impl HasAsyncMethod for Efficient {
18+
fn do_something() -> impl Future<Output = u32> {
19+
core::future::ready(1)
20+
}
21+
}
22+
23+
fn main() {
24+
Inefficient::do_something();
25+
Efficient::do_something();
26+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: unused `async` for async trait impl function with no await statements
2+
--> tests/ui/unused_async_trait_impl.rs:11:5
3+
|
4+
LL | / async fn do_something() -> u32 {
5+
LL | |
6+
LL | | 1
7+
LL | | }
8+
| |_____^
9+
|
10+
= help: a Future can be constructed from the return value with `core::future::ready`
11+
= note: `-D clippy::unused-async-trait-impl` implied by `-D warnings`
12+
= help: to override `-D warnings` add `#[allow(clippy::unused_async_trait_impl)]`
13+
help: consider removing the `async` from this function and returning `impl Future<Output = u32>` instead
14+
|
15+
LL ~ fn do_something() -> impl Future<Output = u32> { core::future::ready({
16+
LL +
17+
LL + 1
18+
LL + }) }
19+
|
20+
21+
error: aborting due to 1 previous error
22+

0 commit comments

Comments
 (0)