rust-lang / rust-clippy

A bunch of lints to catch common mistakes and improve your Rust code. Book: https://doc.rust-lang.org/clippy/
https://rust-lang.github.io/rust-clippy/
Other
11.3k stars 1.52k forks source link

`unused_async` doesn't work if an async fn without await has attribute #13199

Open hlf20010508 opened 1 month ago

hlf20010508 commented 1 month ago

Summary

I wrote a proc_macro_attribute, only added some trace to the code block, and the unused_async doesn't work anymore.

I tried to work around it in dylint, copied the code of unused_async.

If I remove condition !span.from_expansion() and use call_site for macro span, it worked.

let mut span = span;

if let Some(info) = span.macro_backtrace().next() {
    span = info.call_site
}

self.unused_async_fns.push(UnusedAsyncFn {
    await_in_async_block: visitor.await_in_async_block,
    fn_span: span,
    def_id,
});

Is there anything more should be considered for macros that I don't know?

Lint Name

unused_async

Reproducer

Attribute:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn add_trace(_args: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);

    let fn_attrs = &input.attrs;
    let fn_visibility = &input.vis;
    let fn_is_async = input.sig.asyncness.is_some();
    let fn_name = &input.sig.ident;
    let fn_generics = &input.sig.generics;
    let fn_inputs = &input.sig.inputs;
    let fn_output = &input.sig.output;
    let fn_where_clause = &input.sig.generics.where_clause;
    let fn_block = &input.block;

    let fn_name_str = &fn_name.to_string();

    let expanded = if fn_is_async {
        quote! {
            #(#fn_attrs)*
            #fn_visibility async fn #fn_name #fn_generics(#fn_inputs) #fn_output #fn_where_clause {
                let func_path = module_path!().to_string() + "::" + #fn_name_str;
                tracing::trace!("->|{}", func_path);
                let result = #fn_block;
                tracing::trace!("<-|{}", func_path);
                result
            }
        }
    } else {
        quote! {
            #(#fn_attrs)*
            #fn_visibility fn #fn_name #fn_generics(#fn_inputs) #fn_output #fn_where_clause {
                let func_path = module_path!().to_string() + "::" + #fn_name_str;
                tracing::trace!("->|{}", func_path);
                let result = #fn_block;
                tracing::trace!("<-|{}", func_path);
                result
            }
        }
    };

    TokenStream::from(expanded)
}

Fn:

#[add_trace]
pub async fn test() {
    println!("test");
}

Version

rustc 1.80.0-nightly (c987ad527 2024-05-01)
hlf20010508 commented 1 month ago

I also found that it doesn't work for

pub async fn test() {
    async {
        println!("test");
    }
    .await;
}
Alexendoo commented 1 month ago

The produced function can be detected as not from a macro expansion by preserving the token spans, e.g.

use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn add_trace(_args: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);

    let mut tokens = input.sig.to_token_stream();
    input.block.brace_token.surround(&mut tokens, |tokens| {
        let fn_name = input.sig.ident.to_string();
        let fn_block = &input.block;
        tokens.extend(quote! {
            let func_path = module_path!().to_string() + "::" + #fn_name;
            tracing::trace!("->|{}", func_path);
            let result = #fn_block;
            tracing::trace!("<-|{}", func_path);
            result
        });
    });

    tokens.into()
}