Skip to content

Commit b781645

Browse files
committed
Auto merge of #116184 - compiler-errors:afit-lint, r=tmandry
Add `async_fn_in_trait` lint cc #115822 (comment) Mostly unsure what the messaging should be. Feedback required. r? `@tmandry`
2 parents afe67fa + 2f52490 commit b781645

33 files changed

+308
-60
lines changed

compiler/rustc_lint/messages.ftl

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ lint_array_into_iter =
55
.use_explicit_into_iter_suggestion =
66
or use `IntoIterator::into_iter(..)` instead of `.into_iter()` to explicitly iterate by value
77
8+
lint_async_fn_in_trait = use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified
9+
.note = you can suppress this lint if you plan to use the trait only in your own code, or do not care about auto traits like `Send` on the `Future`
10+
.suggestion = you can alternatively desugar to a normal `fn` that returns `impl Future` and add any desired bounds such as `Send`
11+
812
lint_atomic_ordering_fence = memory fences cannot have `Relaxed` ordering
913
.help = consider using ordering modes `Acquire`, `Release`, `AcqRel` or `SeqCst`
1014
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use crate::lints::AsyncFnInTraitDiag;
2+
use crate::LateContext;
3+
use crate::LateLintPass;
4+
use rustc_hir as hir;
5+
use rustc_trait_selection::traits::error_reporting::suggestions::suggest_desugaring_async_fn_to_impl_future_in_trait;
6+
7+
declare_lint! {
8+
/// The `async_fn_in_trait` lint detects use of `async fn` in the
9+
/// definition of a publicly-reachable trait.
10+
///
11+
/// ### Example
12+
///
13+
/// ```rust
14+
/// # #![feature(async_fn_in_trait)]
15+
/// pub trait Trait {
16+
/// async fn method(&self);
17+
/// }
18+
/// # fn main() {}
19+
/// ```
20+
///
21+
/// {{produces}}
22+
///
23+
/// ### Explanation
24+
///
25+
/// When `async fn` is used in a trait definition, the trait does not
26+
/// promise that the opaque [`Future`] returned by the associated function
27+
/// or method will implement any [auto traits] such as [`Send`]. This may
28+
/// be surprising and may make the associated functions or methods on the
29+
/// trait less useful than intended. On traits exposed publicly from a
30+
/// crate, this may affect downstream crates whose authors cannot alter
31+
/// the trait definition.
32+
///
33+
/// For example, this code is invalid:
34+
///
35+
/// ```rust,compile_fail
36+
/// # #![feature(async_fn_in_trait)]
37+
/// pub trait Trait {
38+
/// async fn method(&self) {}
39+
/// }
40+
///
41+
/// fn test<T: Trait>(x: T) {
42+
/// fn spawn<T: Send>(_: T) {}
43+
/// spawn(x.method()); // Not OK.
44+
/// }
45+
/// ```
46+
///
47+
/// This lint exists to warn authors of publicly-reachable traits that
48+
/// they may want to consider desugaring the `async fn` to a normal `fn`
49+
/// that returns an opaque `impl Future<..> + Send` type.
50+
///
51+
/// For example, instead of:
52+
///
53+
/// ```rust
54+
/// # #![feature(async_fn_in_trait)]
55+
/// pub trait Trait {
56+
/// async fn method(&self) {}
57+
/// }
58+
/// ```
59+
///
60+
/// The author of the trait may want to write:
61+
///
62+
///
63+
/// ```rust
64+
/// # #![feature(return_position_impl_trait_in_trait)]
65+
/// use core::future::Future;
66+
/// pub trait Trait {
67+
/// fn method(&self) -> impl Future<Output = ()> + Send { async {} }
68+
/// }
69+
/// ```
70+
///
71+
/// This still allows the use of `async fn` within impls of the trait.
72+
/// However, it also means that the trait will never be compatible with
73+
/// impls where the returned [`Future`] of the method does not implement
74+
/// `Send`.
75+
///
76+
/// Conversely, if the trait is used only locally, if it is never used in
77+
/// generic functions, or if it is only used in single-threaded contexts
78+
/// that do not care whether the returned [`Future`] implements [`Send`],
79+
/// then the lint may be suppressed.
80+
///
81+
/// [`Future`]: https://doc.rust-lang.org/core/future/trait.Future.html
82+
/// [`Send`]: https://doc.rust-lang.org/core/marker/trait.Send.html
83+
/// [auto traits]: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits
84+
pub ASYNC_FN_IN_TRAIT,
85+
Warn,
86+
"use of `async fn` in definition of a publicly-reachable trait"
87+
}
88+
89+
declare_lint_pass!(
90+
/// Lint for use of `async fn` in the definition of a publicly-reachable
91+
/// trait.
92+
AsyncFnInTrait => [ASYNC_FN_IN_TRAIT]
93+
);
94+
95+
impl<'tcx> LateLintPass<'tcx> for AsyncFnInTrait {
96+
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'tcx>) {
97+
if let hir::TraitItemKind::Fn(sig, body) = item.kind
98+
&& let hir::IsAsync::Async(async_span) = sig.header.asyncness
99+
{
100+
// RTN can be used to bound `async fn` in traits in a better way than "always"
101+
if cx.tcx.features().return_type_notation {
102+
return;
103+
}
104+
105+
// Only need to think about library implications of reachable traits
106+
if !cx.tcx.effective_visibilities(()).is_reachable(item.owner_id.def_id) {
107+
return;
108+
}
109+
110+
let hir::FnRetTy::Return(hir::Ty { kind: hir::TyKind::OpaqueDef(def, ..), .. }) =
111+
sig.decl.output
112+
else {
113+
// This should never happen, but let's not ICE.
114+
return;
115+
};
116+
let sugg = suggest_desugaring_async_fn_to_impl_future_in_trait(
117+
cx.tcx,
118+
sig,
119+
body,
120+
def.owner_id.def_id,
121+
" + Send",
122+
);
123+
cx.tcx.emit_spanned_lint(ASYNC_FN_IN_TRAIT, item.hir_id(), async_span, AsyncFnInTraitDiag {
124+
sugg
125+
});
126+
}
127+
}
128+
}

compiler/rustc_lint/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ extern crate rustc_session;
5050
extern crate tracing;
5151

5252
mod array_into_iter;
53+
mod async_fn_in_trait;
5354
pub mod builtin;
5455
mod context;
5556
mod deref_into_dyn_supertrait;
@@ -96,6 +97,7 @@ use rustc_session::lint::builtin::{
9697
};
9798

9899
use array_into_iter::ArrayIntoIter;
100+
use async_fn_in_trait::AsyncFnInTrait;
99101
use builtin::*;
100102
use deref_into_dyn_supertrait::*;
101103
use drop_forget_useless::*;
@@ -234,6 +236,7 @@ late_lint_methods!(
234236
MapUnitFn: MapUnitFn,
235237
MissingDebugImplementations: MissingDebugImplementations,
236238
MissingDoc: MissingDoc,
239+
AsyncFnInTrait: AsyncFnInTrait,
237240
]
238241
]
239242
);

compiler/rustc_lint/src/lints.rs

+21
Original file line numberDiff line numberDiff line change
@@ -1818,3 +1818,24 @@ pub struct UnusedAllocationDiag;
18181818
#[derive(LintDiagnostic)]
18191819
#[diag(lint_unused_allocation_mut)]
18201820
pub struct UnusedAllocationMutDiag;
1821+
1822+
pub struct AsyncFnInTraitDiag {
1823+
pub sugg: Option<Vec<(Span, String)>>,
1824+
}
1825+
1826+
impl<'a> DecorateLint<'a, ()> for AsyncFnInTraitDiag {
1827+
fn decorate_lint<'b>(
1828+
self,
1829+
diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
1830+
) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
1831+
diag.note(fluent::lint_note);
1832+
if let Some(sugg) = self.sugg {
1833+
diag.multipart_suggestion(fluent::lint_suggestion, sugg, Applicability::MaybeIncorrect);
1834+
}
1835+
diag
1836+
}
1837+
1838+
fn msg(&self) -> rustc_errors::DiagnosticMessage {
1839+
fluent::lint_async_fn_in_trait
1840+
}
1841+
}

compiler/rustc_trait_selection/src/traits/error_reporting/suggestions.rs

+69-53
Original file line numberDiff line numberDiff line change
@@ -4000,14 +4000,6 @@ impl<'tcx> TypeErrCtxtExt<'tcx> for TypeErrCtxt<'_, 'tcx> {
40004000

40014001
// ... whose signature is `async` (i.e. this is an AFIT)
40024002
let (sig, body) = item.expect_fn();
4003-
let hir::IsAsync::Async(async_span) = sig.header.asyncness else {
4004-
return;
4005-
};
4006-
let Ok(async_span) =
4007-
self.tcx.sess.source_map().span_extend_while(async_span, |c| c.is_whitespace())
4008-
else {
4009-
return;
4010-
};
40114003
let hir::FnRetTy::Return(hir::Ty { kind: hir::TyKind::OpaqueDef(def, ..), .. }) =
40124004
sig.decl.output
40134005
else {
@@ -4021,55 +4013,17 @@ impl<'tcx> TypeErrCtxtExt<'tcx> for TypeErrCtxt<'_, 'tcx> {
40214013
return;
40224014
}
40234015

4024-
let future = self.tcx.hir().item(*def).expect_opaque_ty();
4025-
let Some(hir::GenericBound::LangItemTrait(_, _, _, generics)) = future.bounds.get(0) else {
4026-
// `async fn` should always lower to a lang item bound... but don't ICE.
4027-
return;
4028-
};
4029-
let Some(hir::TypeBindingKind::Equality { term: hir::Term::Ty(future_output_ty) }) =
4030-
generics.bindings.get(0).map(|binding| binding.kind)
4031-
else {
4032-
// Also should never happen.
4016+
let Some(sugg) = suggest_desugaring_async_fn_to_impl_future_in_trait(
4017+
self.tcx,
4018+
*sig,
4019+
*body,
4020+
opaque_def_id.expect_local(),
4021+
&format!(" + {auto_trait}"),
4022+
) else {
40334023
return;
40344024
};
40354025

40364026
let function_name = self.tcx.def_path_str(fn_def_id);
4037-
4038-
let mut sugg = if future_output_ty.span.is_empty() {
4039-
vec![
4040-
(async_span, String::new()),
4041-
(
4042-
future_output_ty.span,
4043-
format!(" -> impl std::future::Future<Output = ()> + {auto_trait}"),
4044-
),
4045-
]
4046-
} else {
4047-
vec![
4048-
(
4049-
future_output_ty.span.shrink_to_lo(),
4050-
"impl std::future::Future<Output = ".to_owned(),
4051-
),
4052-
(future_output_ty.span.shrink_to_hi(), format!("> + {auto_trait}")),
4053-
(async_span, String::new()),
4054-
]
4055-
};
4056-
4057-
// If there's a body, we also need to wrap it in `async {}`
4058-
if let hir::TraitFn::Provided(body) = body {
4059-
let body = self.tcx.hir().body(*body);
4060-
let body_span = body.value.span;
4061-
let body_span_without_braces =
4062-
body_span.with_lo(body_span.lo() + BytePos(1)).with_hi(body_span.hi() - BytePos(1));
4063-
if body_span_without_braces.is_empty() {
4064-
sugg.push((body_span_without_braces, " async {} ".to_owned()));
4065-
} else {
4066-
sugg.extend([
4067-
(body_span_without_braces.shrink_to_lo(), "async {".to_owned()),
4068-
(body_span_without_braces.shrink_to_hi(), "} ".to_owned()),
4069-
]);
4070-
}
4071-
}
4072-
40734027
err.multipart_suggestion(
40744028
format!(
40754029
"`{auto_trait}` can be made part of the associated future's \
@@ -4321,3 +4275,65 @@ impl<'tcx> TypeFolder<TyCtxt<'tcx>> for ReplaceImplTraitFolder<'tcx> {
43214275
self.tcx
43224276
}
43234277
}
4278+
4279+
pub fn suggest_desugaring_async_fn_to_impl_future_in_trait<'tcx>(
4280+
tcx: TyCtxt<'tcx>,
4281+
sig: hir::FnSig<'tcx>,
4282+
body: hir::TraitFn<'tcx>,
4283+
opaque_def_id: LocalDefId,
4284+
add_bounds: &str,
4285+
) -> Option<Vec<(Span, String)>> {
4286+
let hir::IsAsync::Async(async_span) = sig.header.asyncness else {
4287+
return None;
4288+
};
4289+
let Ok(async_span) = tcx.sess.source_map().span_extend_while(async_span, |c| c.is_whitespace())
4290+
else {
4291+
return None;
4292+
};
4293+
4294+
let future = tcx.hir().get_by_def_id(opaque_def_id).expect_item().expect_opaque_ty();
4295+
let Some(hir::GenericBound::LangItemTrait(_, _, _, generics)) = future.bounds.get(0) else {
4296+
// `async fn` should always lower to a lang item bound... but don't ICE.
4297+
return None;
4298+
};
4299+
let Some(hir::TypeBindingKind::Equality { term: hir::Term::Ty(future_output_ty) }) =
4300+
generics.bindings.get(0).map(|binding| binding.kind)
4301+
else {
4302+
// Also should never happen.
4303+
return None;
4304+
};
4305+
4306+
let mut sugg = if future_output_ty.span.is_empty() {
4307+
vec![
4308+
(async_span, String::new()),
4309+
(
4310+
future_output_ty.span,
4311+
format!(" -> impl std::future::Future<Output = ()>{add_bounds}"),
4312+
),
4313+
]
4314+
} else {
4315+
vec![
4316+
(future_output_ty.span.shrink_to_lo(), "impl std::future::Future<Output = ".to_owned()),
4317+
(future_output_ty.span.shrink_to_hi(), format!(">{add_bounds}")),
4318+
(async_span, String::new()),
4319+
]
4320+
};
4321+
4322+
// If there's a body, we also need to wrap it in `async {}`
4323+
if let hir::TraitFn::Provided(body) = body {
4324+
let body = tcx.hir().body(body);
4325+
let body_span = body.value.span;
4326+
let body_span_without_braces =
4327+
body_span.with_lo(body_span.lo() + BytePos(1)).with_hi(body_span.hi() - BytePos(1));
4328+
if body_span_without_braces.is_empty() {
4329+
sugg.push((body_span_without_braces, " async {} ".to_owned()));
4330+
} else {
4331+
sugg.extend([
4332+
(body_span_without_braces.shrink_to_lo(), "async {".to_owned()),
4333+
(body_span_without_braces.shrink_to_hi(), "} ".to_owned()),
4334+
]);
4335+
}
4336+
}
4337+
4338+
Some(sugg)
4339+
}

tests/ui/async-await/in-trait/async-associated-types.rs

+2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ use std::fmt::Debug;
99
trait MyTrait<'a, 'b, T> where Self: 'a, T: Debug + Sized + 'b {
1010
type MyAssoc;
1111

12+
#[allow(async_fn_in_trait)]
1213
async fn foo(&'a self, key: &'b T) -> Self::MyAssoc;
1314
}
1415

1516
impl<'a, 'b, T: Debug + Sized + 'b, U: 'a> MyTrait<'a, 'b, T> for U {
1617
type MyAssoc = (&'a U, &'b T);
1718

19+
#[allow(async_fn_in_trait)]
1820
async fn foo(&'a self, key: &'b T) -> (&'a U, &'b T) {
1921
(self, key)
2022
}

tests/ui/async-await/in-trait/async-default-fn-overridden.rs

+2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
use std::future::Future;
77

88
trait AsyncTrait {
9+
#[allow(async_fn_in_trait)]
910
async fn default_impl() {
1011
assert!(false);
1112
}
1213

14+
#[allow(async_fn_in_trait)]
1315
async fn call_default_impl() {
1416
Self::default_impl().await
1517
}

tests/ui/async-await/in-trait/async-example-desugared-extra.rs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::pin::Pin;
1010
use std::task::Poll;
1111

1212
pub trait MyTrait {
13+
#[allow(async_fn_in_trait)]
1314
async fn foo(&self) -> i32;
1415
}
1516

tests/ui/async-await/in-trait/async-example-desugared.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use std::future::Future;
99

1010
trait MyTrait {
11+
#[allow(async_fn_in_trait)]
1112
async fn foo(&self) -> i32;
1213
}
1314

tests/ui/async-await/in-trait/async-example.rs

+3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
#![allow(incomplete_features)]
66

77
trait MyTrait {
8+
#[allow(async_fn_in_trait)]
89
async fn foo(&self) -> i32;
10+
11+
#[allow(async_fn_in_trait)]
912
async fn bar(&self) -> i32;
1013
}
1114

tests/ui/async-await/in-trait/async-lifetimes-and-bounds.rs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use std::fmt::Debug;
88

99
trait MyTrait<'a, 'b, T> {
10+
#[allow(async_fn_in_trait)]
1011
async fn foo(&'a self, key: &'b T) -> (&'a Self, &'b T) where T: Debug + Sized;
1112
}
1213

0 commit comments

Comments
 (0)