blob: 31cc004f6855d27ee2bd9ca40f7a4894e19446ab [file] [log] [blame] [edit]
use clippy_config::Conf;
use clippy_config::types::{DisallowedPathWithoutReplacement, create_disallowed_map};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::paths::{self, PathNS};
use rustc_hir as hir;
use rustc_hir::def_id::{DefId, DefIdMap};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::mir::CoroutineLayout;
use rustc_middle::ty::TyCtxt;
use rustc_session::impl_lint_pass;
use rustc_span::{Span, sym};
declare_clippy_lint! {
/// ### What it does
/// Checks for calls to `await` while holding a non-async-aware
/// `MutexGuard`.
///
/// ### Why is this bad?
/// The Mutex types found in [`std::sync`](https://doc.rust-lang.org/stable/std/sync/) and
/// [`parking_lot`](https://docs.rs/parking_lot/latest/parking_lot/) are
/// not designed to operate in an async context across await points.
///
/// There are two potential solutions. One is to use an async-aware `Mutex`
/// type. Many asynchronous foundation crates provide such a `Mutex` type.
/// The other solution is to ensure the mutex is unlocked before calling
/// `await`, either by introducing a scope or an explicit call to
/// [`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html).
///
/// ### Known problems
/// Will report false positive for explicitly dropped guards
/// ([#6446](https://github.com/rust-lang/rust-clippy/issues/6446)). A
/// workaround for this is to wrap the `.lock()` call in a block instead of
/// explicitly dropping the guard.
///
/// ### Example
/// ```no_run
/// # use std::sync::Mutex;
/// # async fn baz() {}
/// async fn foo(x: &Mutex<u32>) {
/// let mut guard = x.lock().unwrap();
/// *guard += 1;
/// baz().await;
/// }
///
/// async fn bar(x: &Mutex<u32>) {
/// let mut guard = x.lock().unwrap();
/// *guard += 1;
/// drop(guard); // explicit drop
/// baz().await;
/// }
/// ```
///
/// Use instead:
/// ```no_run
/// # use std::sync::Mutex;
/// # async fn baz() {}
/// async fn foo(x: &Mutex<u32>) {
/// {
/// let mut guard = x.lock().unwrap();
/// *guard += 1;
/// }
/// baz().await;
/// }
///
/// async fn bar(x: &Mutex<u32>) {
/// {
/// let mut guard = x.lock().unwrap();
/// *guard += 1;
/// } // guard dropped here at end of scope
/// baz().await;
/// }
/// ```
#[clippy::version = "1.45.0"]
pub AWAIT_HOLDING_LOCK,
suspicious,
"inside an async function, holding a `MutexGuard` while calling `await`"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for calls to `await` while holding a `RefCell`, `Ref`, or `RefMut`.
///
/// ### Why is this bad?
/// `RefCell` refs only check for exclusive mutable access
/// at runtime. Holding a `RefCell` ref across an await suspension point
/// risks panics from a mutable ref shared while other refs are outstanding.
///
/// ### Known problems
/// Will report false positive for explicitly dropped refs
/// ([#6353](https://github.com/rust-lang/rust-clippy/issues/6353)). A workaround for this is
/// to wrap the `.borrow[_mut]()` call in a block instead of explicitly dropping the ref.
///
/// ### Example
/// ```no_run
/// # use std::cell::RefCell;
/// # async fn baz() {}
/// async fn foo(x: &RefCell<u32>) {
/// let mut y = x.borrow_mut();
/// *y += 1;
/// baz().await;
/// }
///
/// async fn bar(x: &RefCell<u32>) {
/// let mut y = x.borrow_mut();
/// *y += 1;
/// drop(y); // explicit drop
/// baz().await;
/// }
/// ```
///
/// Use instead:
/// ```no_run
/// # use std::cell::RefCell;
/// # async fn baz() {}
/// async fn foo(x: &RefCell<u32>) {
/// {
/// let mut y = x.borrow_mut();
/// *y += 1;
/// }
/// baz().await;
/// }
///
/// async fn bar(x: &RefCell<u32>) {
/// {
/// let mut y = x.borrow_mut();
/// *y += 1;
/// } // y dropped here at end of scope
/// baz().await;
/// }
/// ```
#[clippy::version = "1.49.0"]
pub AWAIT_HOLDING_REFCELL_REF,
suspicious,
"inside an async function, holding a `RefCell` ref while calling `await`"
}
declare_clippy_lint! {
/// ### What it does
/// Allows users to configure types which should not be held across await
/// suspension points.
///
/// ### Why is this bad?
/// There are some types which are perfectly safe to use concurrently from
/// a memory access perspective, but that will cause bugs at runtime if
/// they are held in such a way.
///
/// ### Example
///
/// ```toml
/// await-holding-invalid-types = [
/// # You can specify a type name
/// "CustomLockType",
/// # You can (optionally) specify a reason
/// { path = "OtherCustomLockType", reason = "Relies on a thread local" }
/// ]
/// ```
///
/// ```no_run
/// # async fn baz() {}
/// struct CustomLockType;
/// struct OtherCustomLockType;
/// async fn foo() {
/// let _x = CustomLockType;
/// let _y = OtherCustomLockType;
/// baz().await; // Lint violation
/// }
/// ```
#[clippy::version = "1.62.0"]
pub AWAIT_HOLDING_INVALID_TYPE,
suspicious,
"holding a type across an await point which is not allowed to be held as per the configuration"
}
impl_lint_pass!(AwaitHolding => [AWAIT_HOLDING_LOCK, AWAIT_HOLDING_REFCELL_REF, AWAIT_HOLDING_INVALID_TYPE]);
pub struct AwaitHolding {
def_ids: DefIdMap<(&'static str, &'static DisallowedPathWithoutReplacement)>,
}
impl AwaitHolding {
pub(crate) fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self {
let (def_ids, _) = create_disallowed_map(
tcx,
&conf.await_holding_invalid_types,
PathNS::Type,
crate::disallowed_types::def_kind_predicate,
"type",
false,
);
Self { def_ids }
}
}
impl<'tcx> LateLintPass<'tcx> for AwaitHolding {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
if let hir::ExprKind::Closure(hir::Closure {
kind: hir::ClosureKind::Coroutine(hir::CoroutineKind::Desugared(hir::CoroutineDesugaring::Async, _)),
def_id,
..
}) = expr.kind
&& let Some(coroutine_layout) = cx.tcx.mir_coroutine_witnesses(*def_id)
{
self.check_interior_types(cx, coroutine_layout);
}
}
}
impl AwaitHolding {
fn check_interior_types(&self, cx: &LateContext<'_>, coroutine: &CoroutineLayout<'_>) {
for (ty_index, ty_cause) in coroutine.field_tys.iter_enumerated() {
if let rustc_middle::ty::Adt(adt, _) = ty_cause.ty.kind() {
let await_points = || {
coroutine
.variant_source_info
.iter_enumerated()
.filter_map(|(variant, source_info)| {
coroutine.variant_fields[variant]
.raw
.contains(&ty_index)
.then_some(source_info.span)
})
.collect::<Vec<_>>()
};
if is_mutex_guard(cx, adt.did()) {
span_lint_and_then(
cx,
AWAIT_HOLDING_LOCK,
ty_cause.source_info.span,
"this `MutexGuard` is held across an await point",
|diag| {
diag.help(
"consider using an async-aware `Mutex` type or ensuring the \
`MutexGuard` is dropped before calling `await`",
);
diag.span_note(
await_points(),
"these are all the await points this lock is held through",
);
},
);
} else if is_refcell_ref(cx, adt.did()) {
span_lint_and_then(
cx,
AWAIT_HOLDING_REFCELL_REF,
ty_cause.source_info.span,
"this `RefCell` reference is held across an await point",
|diag| {
diag.help("ensure the reference is dropped before calling `await`");
diag.span_note(
await_points(),
"these are all the await points this reference is held through",
);
},
);
} else if let Some(&(path, disallowed_path)) = self.def_ids.get(&adt.did()) {
emit_invalid_type(cx, ty_cause.source_info.span, path, disallowed_path);
}
}
}
}
}
fn emit_invalid_type(
cx: &LateContext<'_>,
span: Span,
path: &'static str,
disallowed_path: &'static DisallowedPathWithoutReplacement,
) {
span_lint_and_then(
cx,
AWAIT_HOLDING_INVALID_TYPE,
span,
format!("holding a disallowed type across an await point `{path}`"),
disallowed_path.diag_amendment(span),
);
}
fn is_mutex_guard(cx: &LateContext<'_>, def_id: DefId) -> bool {
match cx.tcx.get_diagnostic_name(def_id) {
Some(name) => matches!(name, sym::MutexGuard | sym::RwLockReadGuard | sym::RwLockWriteGuard),
None => paths::PARKING_LOT_GUARDS.iter().any(|guard| guard.matches(cx, def_id)),
}
}
fn is_refcell_ref(cx: &LateContext<'_>, def_id: DefId) -> bool {
matches!(
cx.tcx.get_diagnostic_name(def_id),
Some(sym::RefCellRef | sym::RefCellRefMut)
)
}