Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5c469d5
Add unused_async_trait_impl lint
Wassasin Dec 15, 2025
6544c2c
Adjusted description and use declare_lint_pass
Wassasin Jan 13, 2026
e73d51a
Remove the execution of unused_async_trait_impl tests, added todo!() …
Wassasin Jan 13, 2026
aef2e2e
Simplified by using check_impl_item instead of check_fn
Wassasin Jan 13, 2026
a647f23
Use std_or_core to determine the builtin_crate instead of assuming core
Wassasin Jan 13, 2026
2e36afc
Added no_std version of test
Wassasin Jan 13, 2026
5a84626
Use snippet_with_applicability to ensure the suggestion can be given …
Wassasin Jan 13, 2026
359fb83
Improved lint description
Wassasin Jan 14, 2026
8e0b18f
Fix lint span
Wassasin Jan 14, 2026
d7bda9b
Slightly shorten snippet_with_applicability-line
Wassasin Jan 14, 2026
7ad6dd6
Fix indentation for body block
Wassasin Jan 14, 2026
5af570e
Fix whitespace around the removed async keyword
Wassasin Jan 14, 2026
0b93c4f
Added indented testcase
Wassasin Jan 14, 2026
5ed514f
Clarify how the construction of the body snippet works
Wassasin Jan 14, 2026
1d66093
Made example slightly more complicate to demonstrate correct indentation
Wassasin Jan 14, 2026
aa69ef5
Expanded on testcases
Wassasin Jan 14, 2026
1fc329c
Moved unused_async_trait_impl into unused_async
Wassasin May 10, 2026
ba9f66a
Fix dogfood issue
Wassasin May 10, 2026
e1696b9
Add backticks around await in lint description
Wassasin May 13, 2026
b56d50a
Update help relating to future::ready
Wassasin May 13, 2026
0cd53ac
Change applicability to 'MaybeIncorrect'
Wassasin May 13, 2026
98edbc2
Use async body inner expression if it does not contain multiple state…
Wassasin May 13, 2026
175b982
Use span_lint_and_then instead of span_lint_hir_and_then
Wassasin May 13, 2026
4078b17
Manually trim braces
Wassasin May 13, 2026
b62462c
Replace tail expression instead of entire function body with future::…
Wassasin May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7374,6 +7374,7 @@ Released 2018-09-13
[`unstable_as_mut_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#unstable_as_mut_slice
[`unstable_as_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#unstable_as_slice
[`unused_async`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_async
[`unused_async_trait_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_async_trait_impl
[`unused_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_collect
[`unused_enumerate_index`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_enumerate_index
[`unused_format_specs`]: https://rust-lang.github.io/rust-clippy/master/index.html#unused_format_specs
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::unnested_or_patterns::UNNESTED_OR_PATTERNS_INFO,
crate::unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME_INFO,
crate::unused_async::UNUSED_ASYNC_INFO,
crate::unused_async::UNUSED_ASYNC_TRAIT_IMPL_INFO,
crate::unused_io_amount::UNUSED_IO_AMOUNT_INFO,
crate::unused_peekable::UNUSED_PEEKABLE_INFO,
crate::unused_result_ok::UNUSED_RESULT_OK_INFO,
Expand Down
136 changes: 133 additions & 3 deletions clippy_lints/src/unused_async.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use clippy_utils::diagnostics::span_lint_hir_and_then;
use clippy_utils::is_def_id_trait_method;
use clippy_utils::source::{
HasSession, indent_of, reindent_multiline, snippet_block_with_applicability, snippet_with_applicability,
};
use clippy_utils::usage::is_todo_unimplemented_stub;
use rustc_errors::Applicability;
use rustc_hir::def::DefKind;
use rustc_hir::intravisit::{FnKind, Visitor, walk_expr, walk_fn};
use rustc_hir::{
Body, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Defaultness, Expr, ExprKind, FnDecl, HirId, Node,
TraitItem, YieldSource,
Body, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Defaultness, Expr, ExprKind, FnDecl, HirId,
ImplItem, ImplItemKind, IsAsync, Node, TraitItem, YieldSource,
};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::hir::nested_filter;
Expand Down Expand Up @@ -43,7 +47,52 @@ declare_clippy_lint! {
"finds async functions with no await statements"
}

impl_lint_pass!(UnusedAsync => [UNUSED_ASYNC]);
declare_clippy_lint! {
/// ### What it does
/// Checks for trait method implementations that are declared `async` but have no `.await`s inside of them.
///
/// ### Why is this bad?
/// Async functions with no async code create computational overhead.
/// Even though the trait requires the method to return a future,
/// returning a `core::future::ready` with the result is more efficient
/// as it reduces the number of states in the Future state machine by at least one.
///
/// Note that the behaviour is slightly different when using `core::future::ready`,
/// as the value is computed immediately and stored in a future for later retrieval at the first (and only valid) call to `poll`.
/// An `async` block generates code that completely defers the computation of this value until the Future is polled.
///
/// ### Example
/// ```no_run
/// trait AsyncTrait {
/// async fn get_random_number() -> i64;
/// }
///
/// impl AsyncTrait for () {
/// async fn get_random_number() -> i64 {
/// 4 // Chosen by fair dice roll. Guaranteed to be random.
/// }
/// }
/// ```
///
/// Use instead:
/// ```no_run
/// trait AsyncTrait {
/// async fn get_random_number() -> i64;
/// }
///
/// impl AsyncTrait for () {
/// fn get_random_number() -> impl Future<Output = i64> {
/// core::future::ready(4) // Chosen by fair dice roll. Guaranteed to be random.
/// }
/// }
/// ```
#[clippy::version = "1.97.0"]
pub UNUSED_ASYNC_TRAIT_IMPL,
pedantic,
"finds async trait impl functions with no await statements"
}

impl_lint_pass!(UnusedAsync => [UNUSED_ASYNC, UNUSED_ASYNC_TRAIT_IMPL]);

#[derive(Default)]
pub struct UnusedAsync {
Expand Down Expand Up @@ -194,6 +243,87 @@ impl<'tcx> LateLintPass<'tcx> for UnusedAsync {
);
}
}

fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) {
if let ImplItemKind::Fn(ref sig, body_id) = impl_item.kind
&& let IsAsync::Async(async_span) = sig.header.asyncness
&& let body = cx.tcx.hir_body(body_id)
&& !async_fn_contains_todo_unimplemented_macro(cx, body)
{
let mut visitor = AsyncFnVisitor {
cx,
found_await: false,
await_in_async_block: None,
async_depth: 0,
};
visitor.visit_nested_body(body_id);

if !visitor.found_await
&& let Some(builtin_crate) = clippy_utils::std_or_core(cx)
{
span_lint_hir_and_then(
cx,
UNUSED_ASYNC_TRAIT_IMPL,
cx.tcx.local_def_id_to_hir_id(impl_item.owner_id.def_id),
Comment thread
Wassasin marked this conversation as resolved.
Outdated
impl_item.span,
"unused `async` for async trait impl function with no await statements",
Comment thread
Wassasin marked this conversation as resolved.
Outdated
|diag| {
let mut app = Applicability::MachineApplicable;
Comment thread
Wassasin marked this conversation as resolved.
Outdated

let async_span = cx.sess().source_map().span_extend_while_whitespace(async_span);

let signature_snippet = snippet_with_applicability(cx, sig.decl.output.span(), "_", &mut app);

// Fetch body snippet and truncate excess indentation. Like this:
// {
// 4
// }
let body_snippet = snippet_block_with_applicability(cx, body.value.span, "_", None, &mut app);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to make this even nicer would be to strip away the curlies if the function body is just a single expression. I'd be insane to ask you to do that now, on top of everything you've already done, so could you please just leave a TODO comment for this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it but the expansion is a bit messy. Initially I used the span of the expression, but we are losing any comments that might be been put in the body. I opted to remove the curly braces manually from the original body span.

Copy link
Copy Markdown
Author

@Wassasin Wassasin May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this commit for the difference between expressions & manually trimming the braces

// Wrap body snippet in `std::future::ready(...)` and indent everything by one level, like this:
// core::future::ready({
// 4
// })
let new_body_inner_snippet: String = reindent_multiline(
&format!("{builtin_crate}::future::ready({body_snippet})"),
false,
Some(4),
);

let sugg = vec![
(async_span, String::new()),
(
sig.decl.output.span(),
format!("impl Future<Output = {signature_snippet}>"),
),
(
body.value.span,
// Wrap the entire snippet in fresh curly braces and indent everything except the first
// line by the indentation level of the original body snippet, like this:
// {
// <indent> core::future::ready({
// <indent> 4
// <indent> }
// <indent> }
reindent_multiline(
&format!("{{\n{new_body_inner_snippet}\n}}"),
true,
indent_of(cx, body.value.span),
),
),
];
diag.help(format!(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a note, as it doesn't by itself say what to do, but rather gives some context around the fix. See https://doc.rust-lang.org/clippy/development/emitting_lints.html#how-to-choose-between-notes-help-messages-and-suggestions

Suggested change
diag.help(format!(
diag.note(format!(

"a Future can be constructed from the return value with `{builtin_crate}::future::ready`"
Comment thread
Wassasin marked this conversation as resolved.
Outdated
));
diag.multipart_suggestion(
format!("consider removing the `async` from this function and returning `impl Future<Output = {signature_snippet}>` instead"),
sugg,
app
);
Comment on lines +299 to +302
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it seems like rustfmt gave up on formatting this part because of a format literal that's too long. Breaking it up should fix this:

Suggested change
format!("consider removing the `async` from this function and returning `impl Future<Output = {signature_snippet}>` instead"),
sugg,
app
);
format!("consider removing the `async` from this function \
and returning `impl Future<Output = {signature_snippet}>` instead"),
sugg,
app
);

},
);
}
}
}
}

fn is_default_trait_impl(cx: &LateContext<'_>, def_id: LocalDefId) -> bool {
Expand Down
81 changes: 81 additions & 0 deletions tests/ui/unused_async_trait_impl.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#![warn(clippy::unused_async_trait_impl)]

trait HasAsyncMethod {
async fn do_something() -> u32;
}

struct Inefficient;
struct Efficient;
struct Stub;

impl HasAsyncMethod for Inefficient {
fn do_something() -> impl Future<Output = u32> {
std::future::ready({
//~^ unused_async_trait_impl
1
})
}
}

impl HasAsyncMethod for Efficient {
fn do_something() -> impl Future<Output = u32> {
std::future::ready(1)
}
}

impl HasAsyncMethod for Stub {
async fn do_something() -> u32 {
todo!() // Do not emit the lint in this case.
}
}

// Test to check if the identation of the various snippets goes as intended.
mod indented {
struct Indented;

impl crate::HasAsyncMethod for Indented {
fn do_something() -> impl Future<Output = u32> {
std::future::ready({
//~^ unused_async_trait_impl
let mut x = 0;
for y in 0..64 {
x = (x + 1) * y;
}

let fake_fut = async {
if x == 0 {
panic!("Fake example");
}
};

x
})
}
}

struct Complex<T>(std::marker::PhantomData<T>);

impl<T> crate::HasAsyncMethod for Complex<T>
where
T: Sized,
{
fn do_something() -> impl Future<Output = u32> {
std::future::ready({
//~^ unused_async_trait_impl
5
})
}
}
}

trait HasDefaultAsyncMethod {
// The lint should not suggest a change for trait fn's as changing that decl
// implies a less restrictive Future type.
async fn do_something() -> u32 {
0
}
}

impl HasDefaultAsyncMethod for Stub {
// Nothing!
}
75 changes: 75 additions & 0 deletions tests/ui/unused_async_trait_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#![warn(clippy::unused_async_trait_impl)]

trait HasAsyncMethod {
async fn do_something() -> u32;
}

struct Inefficient;
struct Efficient;
struct Stub;

impl HasAsyncMethod for Inefficient {
async fn do_something() -> u32 {
//~^ unused_async_trait_impl
1
}
}

impl HasAsyncMethod for Efficient {
fn do_something() -> impl Future<Output = u32> {
std::future::ready(1)
}
}

impl HasAsyncMethod for Stub {
async fn do_something() -> u32 {
todo!() // Do not emit the lint in this case.
}
}

// Test to check if the identation of the various snippets goes as intended.
mod indented {
struct Indented;

impl crate::HasAsyncMethod for Indented {
async fn do_something() -> u32 {
//~^ unused_async_trait_impl
let mut x = 0;
for y in 0..64 {
x = (x + 1) * y;
}

let fake_fut = async {
if x == 0 {
panic!("Fake example");
}
};

x
}
}

struct Complex<T>(std::marker::PhantomData<T>);

impl<T> crate::HasAsyncMethod for Complex<T>
where
T: Sized,
{
async fn do_something() -> u32 {
//~^ unused_async_trait_impl
5
}
}
}

trait HasDefaultAsyncMethod {
// The lint should not suggest a change for trait fn's as changing that decl
// implies a less restrictive Future type.
async fn do_something() -> u32 {
0
}
}

impl HasDefaultAsyncMethod for Stub {
// Nothing!
}
Loading