Skip to content

Commit 67f25c5

Browse files
committed
attributes: Apply fake return edge only to async fns
The fake return edge was added in tokio-rs#2270 to improve type errors in instrumented async functions. However, it meant that the user block was being modified outside of `gen_block`. This created a negative interaction with tokio-rs#1614, which suppressed a clippy lint internally while explicitly enabling it on the user block. The installed fake return edge generated this same lint, but the user had no way to suppress it: lint directives above the instrumentation were ignored because of the internal suppression, and lints inside the user block could not influence the fake return edge's scope. We now avoid modifying the user block outside of `gen_block`, and restrict the fake return edge to async functions. We also apply the same clippy lint suppression technique to the installed fake return edge. Closes tokio-rs#2410.
1 parent b28c935 commit 67f25c5

File tree

1 file changed

+63
-31
lines changed

1 file changed

+63
-31
lines changed

tracing-attributes/src/expand.rs

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,39 @@ use crate::{
1414
MaybeItemFn, MaybeItemFnRef,
1515
};
1616

17+
#[derive(Debug, Default)]
18+
struct AsyncContext {
19+
fake_return_edge: Option<proc_macro2::TokenStream>,
20+
}
21+
22+
impl AsyncContext {
23+
/// Adds a fake return statement that can be installed as the first thing in the
24+
/// function body, so that we eagerly infer that the return type is what was declared
25+
/// in the async fn signature.
26+
fn with_fake_return_edge(mut self, ident: &Ident, output: &ReturnType) -> Self {
27+
let (return_type, return_span) = if let ReturnType::Type(_, return_type) = &output {
28+
(erase_impl_trait(return_type), return_type.span())
29+
} else {
30+
// Point at function name if we don't have an explicit return type
31+
(syn::parse_quote! { () }, ident.span())
32+
};
33+
// The `#[allow(..)]` is given because the return statement is
34+
// unreachable, but does affect inference, so it needs to be written
35+
// exactly that way for it to do its magic.
36+
let fake_return_edge = quote_spanned! {return_span=>
37+
#[allow(unreachable_code, clippy::diverging_sub_expression, clippy::let_unit_value)]
38+
if false {
39+
let __tracing_attr_fake_return: #return_type =
40+
unreachable!("this is just for type inference, and is unreachable code");
41+
return __tracing_attr_fake_return;
42+
}
43+
};
44+
45+
self.fake_return_edge = Some(fake_return_edge);
46+
self
47+
}
48+
}
49+
1750
/// Given an existing function, generate an instrumented version of that function
1851
pub(crate) fn gen_function<'a, B: ToTokens + 'a>(
1952
input: MaybeItemFnRef<'a, B>,
@@ -51,37 +84,13 @@ pub(crate) fn gen_function<'a, B: ToTokens + 'a>(
5184

5285
let warnings = args.warnings();
5386

54-
let (return_type, return_span) = if let ReturnType::Type(_, return_type) = &output {
55-
(erase_impl_trait(return_type), return_type.span())
56-
} else {
57-
// Point at function name if we don't have an explicit return type
58-
(syn::parse_quote! { () }, ident.span())
59-
};
60-
// Install a fake return statement as the first thing in the function
61-
// body, so that we eagerly infer that the return type is what we
62-
// declared in the async fn signature.
63-
// The `#[allow(..)]` is given because the return statement is
64-
// unreachable, but does affect inference, so it needs to be written
65-
// exactly that way for it to do its magic.
66-
let fake_return_edge = quote_spanned! {return_span=>
67-
#[allow(unreachable_code, clippy::diverging_sub_expression, clippy::let_unit_value)]
68-
if false {
69-
let __tracing_attr_fake_return: #return_type =
70-
unreachable!("this is just for type inference, and is unreachable code");
71-
return __tracing_attr_fake_return;
72-
}
73-
};
74-
let block = quote! {
75-
{
76-
#fake_return_edge
77-
#block
78-
}
79-
};
87+
let async_context =
88+
asyncness.map(|_| AsyncContext::default().with_fake_return_edge(ident, output));
8089

8190
let body = gen_block(
82-
&block,
91+
block,
8392
params,
84-
asyncness.is_some(),
93+
async_context,
8594
args,
8695
instrumented_function_name,
8796
self_type,
@@ -103,7 +112,7 @@ pub(crate) fn gen_function<'a, B: ToTokens + 'a>(
103112
fn gen_block<B: ToTokens>(
104113
block: &B,
105114
params: &Punctuated<FnArg, Token![,]>,
106-
async_context: bool,
115+
async_context: Option<AsyncContext>,
107116
mut args: InstrumentArgs,
108117
instrumented_function_name: &str,
109118
self_type: Option<&TypePath>,
@@ -257,7 +266,30 @@ fn gen_block<B: ToTokens>(
257266
// If `err` is in args, instrument any resulting `Err`s.
258267
// If `ret` is in args, instrument any resulting `Ok`s when the function
259268
// returns `Result`s, otherwise instrument any resulting values.
260-
if async_context {
269+
if let Some(context) = async_context {
270+
let block = if let Some(fake_return_edge) = context.fake_return_edge {
271+
// Install the fake return edge.
272+
quote! {
273+
{
274+
// Because `quote` produces a stream of tokens _without_ whitespace,
275+
// the `if` and the block will appear directly next to each other.
276+
// This generates a clippy lint about suspicious `if/else` formatting.
277+
// Therefore, suppress the lint inside the generated code...
278+
#[allow(clippy::suspicious_else_formatting)]
279+
{
280+
#fake_return_edge
281+
// ...but turn the lint back on inside the function body.
282+
#[warn(clippy::suspicious_else_formatting)]
283+
#block
284+
}
285+
}
286+
}
287+
} else {
288+
// Re-quote the block for type matching.
289+
quote! {
290+
#block
291+
}
292+
};
261293
let mk_fut = match (err_event, ret_event) {
262294
(Some(err_event), Some(ret_event)) => quote_spanned!(block.span()=>
263295
async move {
@@ -697,7 +729,7 @@ impl<'block> AsyncInfo<'block> {
697729
let instrumented_block = gen_block(
698730
&async_expr.block,
699731
&self.input.sig.inputs,
700-
true,
732+
Some(AsyncContext::default()),
701733
args,
702734
instrumented_function_name,
703735
None,

0 commit comments

Comments
 (0)