Skip to content

Commit 74b476e

Browse files
compiler-errorsdavidbarsky
authored andcommitted
attributes: add fake return to improve span on type error (#2270)
## Motivation Return type errors on instrumented async functions are a bit vague, since the type error originates within the macro itself due to the indirection of additional `async {}` blocks generated in the proc-macro (and due to the way that inference propagates around in Rust). This leads to a pretty difficult to understand error. For example: ```rust #[instrument] async fn foo() -> String { "" } ``` results in... ``` error[E0308]: mismatched types --> src/main.rs:1:1 | 1 | #[tracing::instrument] | ^^^^^^^^^^^^^^^^^^^^^^- help: try using a conversion method: `.to_string()` | | | expected struct `String`, found `&str` ``` ## Solution Installs a fake `return` statement as the first thing that happens in the auto-generated block of an instrumented async function. This causes the coercion machinery within rustc to infer the right return type (matching the the outer function) eagerly, as opposed to after the `async {}` block has been type-checked. This will cause us to both be able to point out the return type span correctly, and properly suggest fixes on the expressions that cause the type mismatch. After this change, the example code above compiles to: ``` error[E0308]: mismatched types --> src/main.rs:3:5 | 3 | "" | ^^- help: try using a conversion method: `.to_string()` | | | expected struct `String`, found `&str` | note: return type inferred to be `String` here --> src/main.rs:2:20 | 2 | async fn foo() -> String { | ^^^^^^ ```
1 parent ac4583a commit 74b476e

File tree

5 files changed

+213
-20
lines changed

5 files changed

+213
-20
lines changed

tracing-attributes/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ tracing-subscriber = { path = "../tracing-subscriber", version = "0.3.0", featur
5050
tokio-test = { version = "0.3.0" }
5151
tracing-core = { path = "../tracing-core", version = "0.1.28"}
5252
async-trait = "0.1.56"
53+
trybuild = "1.0.64"
54+
rustversion = "1.0.9"
5355

5456
[badges]
5557
maintenance = { status = "experimental" }

tracing-attributes/src/expand.rs

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use std::iter;
22

33
use proc_macro2::TokenStream;
44
use quote::{quote, quote_spanned, ToTokens};
5+
use syn::visit_mut::VisitMut;
56
use syn::{
67
punctuated::Punctuated, spanned::Spanned, Block, Expr, ExprAsync, ExprCall, FieldPat, FnArg,
78
Ident, Item, ItemFn, Pat, PatIdent, PatReference, PatStruct, PatTuple, PatTupleStruct, PatType,
8-
Path, Signature, Stmt, Token, TypePath,
9+
Path, ReturnType, Signature, Stmt, Token, Type, TypePath,
910
};
1011

1112
use crate::{
@@ -18,7 +19,7 @@ pub(crate) fn gen_function<'a, B: ToTokens + 'a>(
1819
input: MaybeItemFnRef<'a, B>,
1920
args: InstrumentArgs,
2021
instrumented_function_name: &str,
21-
self_type: Option<&syn::TypePath>,
22+
self_type: Option<&TypePath>,
2223
) -> proc_macro2::TokenStream {
2324
// these are needed ahead of time, as ItemFn contains the function body _and_
2425
// isn't representable inside a quote!/quote_spanned! macro
@@ -31,7 +32,7 @@ pub(crate) fn gen_function<'a, B: ToTokens + 'a>(
3132
} = input;
3233

3334
let Signature {
34-
output: return_type,
35+
output,
3536
inputs: params,
3637
unsafety,
3738
asyncness,
@@ -49,8 +50,35 @@ pub(crate) fn gen_function<'a, B: ToTokens + 'a>(
4950

5051
let warnings = args.warnings();
5152

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

6189
quote!(
6290
#(#attrs) *
63-
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type
91+
#vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #output
6492
#where_clause
6593
{
6694
#warnings
@@ -76,7 +104,7 @@ fn gen_block<B: ToTokens>(
76104
async_context: bool,
77105
mut args: InstrumentArgs,
78106
instrumented_function_name: &str,
79-
self_type: Option<&syn::TypePath>,
107+
self_type: Option<&TypePath>,
80108
) -> proc_macro2::TokenStream {
81109
// generate the span's name
82110
let span_name = args
@@ -393,11 +421,11 @@ impl RecordType {
393421
"Wrapping",
394422
];
395423

396-
/// Parse `RecordType` from [syn::Type] by looking up
424+
/// Parse `RecordType` from [Type] by looking up
397425
/// the [RecordType::TYPES_FOR_VALUE] array.
398-
fn parse_from_ty(ty: &syn::Type) -> Self {
426+
fn parse_from_ty(ty: &Type) -> Self {
399427
match ty {
400-
syn::Type::Path(syn::TypePath { path, .. })
428+
Type::Path(TypePath { path, .. })
401429
if path
402430
.segments
403431
.iter()
@@ -410,9 +438,7 @@ impl RecordType {
410438
{
411439
RecordType::Value
412440
}
413-
syn::Type::Reference(syn::TypeReference { elem, .. }) => {
414-
RecordType::parse_from_ty(elem)
415-
}
441+
Type::Reference(syn::TypeReference { elem, .. }) => RecordType::parse_from_ty(elem),
416442
_ => RecordType::Debug,
417443
}
418444
}
@@ -471,7 +497,7 @@ pub(crate) struct AsyncInfo<'block> {
471497
// statement that must be patched
472498
source_stmt: &'block Stmt,
473499
kind: AsyncKind<'block>,
474-
self_type: Option<syn::TypePath>,
500+
self_type: Option<TypePath>,
475501
input: &'block ItemFn,
476502
}
477503

@@ -606,11 +632,11 @@ impl<'block> AsyncInfo<'block> {
606632
if ident == "_self" {
607633
let mut ty = *ty.ty.clone();
608634
// extract the inner type if the argument is "&self" or "&mut self"
609-
if let syn::Type::Reference(syn::TypeReference { elem, .. }) = ty {
635+
if let Type::Reference(syn::TypeReference { elem, .. }) = ty {
610636
ty = *elem;
611637
}
612638

613-
if let syn::Type::Path(tp) = ty {
639+
if let Type::Path(tp) = ty {
614640
self_type = Some(tp);
615641
break;
616642
}
@@ -722,7 +748,7 @@ struct IdentAndTypesRenamer<'a> {
722748
idents: Vec<(Ident, Ident)>,
723749
}
724750

725-
impl<'a> syn::visit_mut::VisitMut for IdentAndTypesRenamer<'a> {
751+
impl<'a> VisitMut for IdentAndTypesRenamer<'a> {
726752
// we deliberately compare strings because we want to ignore the spans
727753
// If we apply clippy's lint, the behavior changes
728754
#[allow(clippy::cmp_owned)]
@@ -734,11 +760,11 @@ impl<'a> syn::visit_mut::VisitMut for IdentAndTypesRenamer<'a> {
734760
}
735761
}
736762

737-
fn visit_type_mut(&mut self, ty: &mut syn::Type) {
763+
fn visit_type_mut(&mut self, ty: &mut Type) {
738764
for (type_name, new_type) in &self.types {
739-
if let syn::Type::Path(TypePath { path, .. }) = ty {
765+
if let Type::Path(TypePath { path, .. }) = ty {
740766
if path_to_string(path) == *type_name {
741-
*ty = syn::Type::Path(new_type.clone());
767+
*ty = Type::Path(new_type.clone());
742768
}
743769
}
744770
}
@@ -751,10 +777,33 @@ struct AsyncTraitBlockReplacer<'a> {
751777
patched_block: Block,
752778
}
753779

754-
impl<'a> syn::visit_mut::VisitMut for AsyncTraitBlockReplacer<'a> {
780+
impl<'a> VisitMut for AsyncTraitBlockReplacer<'a> {
755781
fn visit_block_mut(&mut self, i: &mut Block) {
756782
if i == self.block {
757783
*i = self.patched_block.clone();
758784
}
759785
}
760786
}
787+
788+
// Replaces any `impl Trait` with `_` so it can be used as the type in
789+
// a `let` statement's LHS.
790+
struct ImplTraitEraser;
791+
792+
impl VisitMut for ImplTraitEraser {
793+
fn visit_type_mut(&mut self, t: &mut Type) {
794+
if let Type::ImplTrait(..) = t {
795+
*t = syn::TypeInfer {
796+
underscore_token: Token![_](t.span()),
797+
}
798+
.into();
799+
} else {
800+
syn::visit_mut::visit_type_mut(self, t);
801+
}
802+
}
803+
}
804+
805+
fn erase_impl_trait(ty: &Type) -> Type {
806+
let mut ty = ty.clone();
807+
ImplTraitEraser.visit_type_mut(&mut ty);
808+
ty
809+
}

tracing-attributes/tests/ui.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Only test on nightly, since UI tests are bound to change over time
2+
#[rustversion::stable]
3+
#[test]
4+
fn async_instrument() {
5+
let t = trybuild::TestCases::new();
6+
t.compile_fail("tests/ui/async_instrument.rs");
7+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#![allow(unreachable_code)]
2+
3+
#[tracing::instrument]
4+
async fn unit() {
5+
""
6+
}
7+
8+
#[tracing::instrument]
9+
async fn simple_mismatch() -> String {
10+
""
11+
}
12+
13+
// FIXME: this span is still pretty poor
14+
#[tracing::instrument]
15+
async fn opaque_unsatisfied() -> impl std::fmt::Display {
16+
("",)
17+
}
18+
19+
struct Wrapper<T>(T);
20+
21+
#[tracing::instrument]
22+
async fn mismatch_with_opaque() -> Wrapper<impl std::fmt::Display> {
23+
""
24+
}
25+
26+
#[tracing::instrument]
27+
async fn early_return_unit() {
28+
if true {
29+
return "";
30+
}
31+
}
32+
33+
#[tracing::instrument]
34+
async fn early_return() -> String {
35+
if true {
36+
return "";
37+
}
38+
String::new()
39+
}
40+
41+
#[tracing::instrument]
42+
async fn extra_semicolon() -> i32 {
43+
1;
44+
}
45+
46+
fn main() {}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
error[E0308]: mismatched types
2+
--> tests/ui/async_instrument.rs:5:5
3+
|
4+
5 | ""
5+
| ^^ expected `()`, found `&str`
6+
|
7+
note: return type inferred to be `()` here
8+
--> tests/ui/async_instrument.rs:4:10
9+
|
10+
4 | async fn unit() {
11+
| ^^^^
12+
13+
error[E0308]: mismatched types
14+
--> tests/ui/async_instrument.rs:10:5
15+
|
16+
10 | ""
17+
| ^^- help: try using a conversion method: `.to_string()`
18+
| |
19+
| expected struct `String`, found `&str`
20+
|
21+
note: return type inferred to be `String` here
22+
--> tests/ui/async_instrument.rs:9:31
23+
|
24+
9 | async fn simple_mismatch() -> String {
25+
| ^^^^^^
26+
27+
error[E0277]: `(&str,)` doesn't implement `std::fmt::Display`
28+
--> tests/ui/async_instrument.rs:14:1
29+
|
30+
14 | #[tracing::instrument]
31+
| ^^^^^^^^^^^^^^^^^^^^^^ `(&str,)` cannot be formatted with the default formatter
32+
|
33+
= help: the trait `std::fmt::Display` is not implemented for `(&str,)`
34+
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
35+
= note: this error originates in the attribute macro `tracing::instrument` (in Nightly builds, run with -Z macro-backtrace for more info)
36+
37+
error[E0308]: mismatched types
38+
--> tests/ui/async_instrument.rs:23:5
39+
|
40+
23 | ""
41+
| ^^ expected struct `Wrapper`, found `&str`
42+
|
43+
= note: expected struct `Wrapper<_>`
44+
found reference `&'static str`
45+
note: return type inferred to be `Wrapper<_>` here
46+
--> tests/ui/async_instrument.rs:22:36
47+
|
48+
22 | async fn mismatch_with_opaque() -> Wrapper<impl std::fmt::Display> {
49+
| ^^^^^^^
50+
help: try wrapping the expression in `Wrapper`
51+
|
52+
23 | Wrapper("")
53+
| ++++++++ +
54+
55+
error[E0308]: mismatched types
56+
--> tests/ui/async_instrument.rs:29:16
57+
|
58+
29 | return "";
59+
| ^^ expected `()`, found `&str`
60+
|
61+
note: return type inferred to be `()` here
62+
--> tests/ui/async_instrument.rs:27:10
63+
|
64+
27 | async fn early_return_unit() {
65+
| ^^^^^^^^^^^^^^^^^
66+
67+
error[E0308]: mismatched types
68+
--> tests/ui/async_instrument.rs:36:16
69+
|
70+
36 | return "";
71+
| ^^- help: try using a conversion method: `.to_string()`
72+
| |
73+
| expected struct `String`, found `&str`
74+
|
75+
note: return type inferred to be `String` here
76+
--> tests/ui/async_instrument.rs:34:28
77+
|
78+
34 | async fn early_return() -> String {
79+
| ^^^^^^
80+
81+
error[E0308]: mismatched types
82+
--> tests/ui/async_instrument.rs:42:35
83+
|
84+
42 | async fn extra_semicolon() -> i32 {
85+
| ___________________________________^
86+
43 | | 1;
87+
| | - help: remove this semicolon
88+
44 | | }
89+
| |_^ expected `i32`, found `()`

0 commit comments

Comments
 (0)