blob: 5f10e1968f1da8642208b5f25c89177555cbe168 [file] [log] [blame]
// Implementation for lints detecting interior mutability in constants.
//
// For `declare_interior_mutable_const` there are three strategies used to
// determine if a value has interior mutability:
// * A type-based check. This is the least accurate, but can always run.
// * A const-eval based check. This is the most accurate, but this requires that the value is
// defined and does not work with generics.
// * A HIR-tree based check. This is less accurate than const-eval, but it can be applied to generic
// values.
//
// For `borrow_interior_mutable_const` the same three strategies are applied
// when checking a constant's value, but field and array index projections at
// the borrow site are taken into account as well. As an example: `FOO.bar` may
// have interior mutability, but `FOO.baz` may not. When borrowing `FOO.baz` no
// warning will be issued.
//
// No matter the lint or strategy, a warning should only be issued if a value
// definitely contains interior mutability.
use clippy_config::Conf;
use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
use clippy_utils::is_in_const_context;
use clippy_utils::macros::macro_backtrace;
use clippy_utils::paths::{PathNS, lookup_path_str};
use clippy_utils::ty::{get_field_idx_by_name, implements_trait};
use rustc_data_structures::fx::FxHashMap;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_hir::{
Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, Node, StructTailExpr, TraitItem, TraitItemKind, UnOp,
};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::mir::{ConstValue, UnevaluatedConst};
use rustc_middle::ty::adjustment::{Adjust, Adjustment};
use rustc_middle::ty::{
self, AliasTyKind, EarlyBinder, GenericArgs, GenericArgsRef, Instance, Ty, TyCtxt, TypeFolder, TypeSuperFoldable,
TypeckResults, TypingEnv,
};
use rustc_session::impl_lint_pass;
use rustc_span::{DUMMY_SP, sym};
use std::collections::hash_map::Entry;
declare_clippy_lint! {
/// ### What it does
/// Checks for the declaration of named constant which contain interior mutability.
///
/// ### Why is this bad?
/// Named constants are copied at every use site which means any change to their value
/// will be lost after the newly created value is dropped. e.g.
///
/// ```rust
/// use core::sync::atomic::{AtomicUsize, Ordering};
/// const ATOMIC: AtomicUsize = AtomicUsize::new(0);
/// fn add_one() -> usize {
/// // This will always return `0` since `ATOMIC` is copied before it's used.
/// ATOMIC.fetch_add(1, Ordering::AcqRel)
/// }
/// ```
///
/// If shared modification of the value is desired, a `static` item is needed instead.
/// If that is not desired, a `const fn` constructor should be used to make it obvious
/// at the use site that a new value is created.
///
/// ### Known problems
/// Prior to `const fn` stabilization this was the only way to provide a value which
/// could initialize a `static` item (e.g. the `std::sync::ONCE_INIT` constant). In
/// this case the use of `const` is required and this lint should be suppressed.
///
/// There also exists types which contain private fields with interior mutability, but
/// no way to both create a value as a constant and modify any mutable field using the
/// type's public interface (e.g. `bytes::Bytes`). As there is no reasonable way to
/// scan a crate's interface to see if this is the case, all such types will be linted.
/// If this happens use the `ignore-interior-mutability` configuration option to allow
/// the type.
///
/// ### Example
/// ```no_run
/// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
///
/// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12);
/// CONST_ATOM.store(6, SeqCst); // the content of the atomic is unchanged
/// assert_eq!(CONST_ATOM.load(SeqCst), 12); // because the CONST_ATOM in these lines are distinct
/// ```
///
/// Use instead:
/// ```no_run
/// # use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
/// static STATIC_ATOM: AtomicUsize = AtomicUsize::new(15);
/// STATIC_ATOM.store(9, SeqCst);
/// assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance
/// ```
#[clippy::version = "pre 1.29.0"]
pub DECLARE_INTERIOR_MUTABLE_CONST,
style,
"declaring `const` with interior mutability"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for a borrow of a named constant with interior mutability.
///
/// ### Why is this bad?
/// Named constants are copied at every use site which means any change to their value
/// will be lost after the newly created value is dropped. e.g.
///
/// ```rust
/// use core::sync::atomic::{AtomicUsize, Ordering};
/// const ATOMIC: AtomicUsize = AtomicUsize::new(0);
/// fn add_one() -> usize {
/// // This will always return `0` since `ATOMIC` is copied before it's borrowed
/// // for use by `fetch_add`.
/// ATOMIC.fetch_add(1, Ordering::AcqRel)
/// }
/// ```
///
/// ### Known problems
/// This lint does not, and cannot in general, determine if the borrow of the constant
/// is used in a way which causes a mutation. e.g.
///
/// ```rust
/// use core::cell::Cell;
/// const CELL: Cell<usize> = Cell::new(0);
/// fn get_cell() -> Cell<usize> {
/// // This is fine. It borrows a copy of `CELL`, but never mutates it through the
/// // borrow.
/// CELL.clone()
/// }
/// ```
///
/// There also exists types which contain private fields with interior mutability, but
/// no way to both create a value as a constant and modify any mutable field using the
/// type's public interface (e.g. `bytes::Bytes`). As there is no reasonable way to
/// scan a crate's interface to see if this is the case, all such types will be linted.
/// If this happens use the `ignore-interior-mutability` configuration option to allow
/// the type.
///
/// ### Example
/// ```no_run
/// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
/// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12);
///
/// CONST_ATOM.store(6, SeqCst); // the content of the atomic is unchanged
/// assert_eq!(CONST_ATOM.load(SeqCst), 12); // because the CONST_ATOM in these lines are distinct
/// ```
///
/// Use instead:
/// ```no_run
/// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
/// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12);
///
/// static STATIC_ATOM: AtomicUsize = CONST_ATOM;
/// STATIC_ATOM.store(9, SeqCst);
/// assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance
/// ```
#[clippy::version = "pre 1.29.0"]
pub BORROW_INTERIOR_MUTABLE_CONST,
style,
"referencing `const` with interior mutability"
}
#[derive(Clone, Copy)]
enum IsFreeze {
/// The type and all possible values are `Freeze`
Yes,
/// The type itself is non-`Freeze`, but not all values are.
Maybe,
/// The type and all possible values are non-`Freeze`
No,
}
impl IsFreeze {
/// Merges the variants of a sum type (i.e. an enum).
fn from_variants(iter: impl Iterator<Item = Self>) -> Self {
iter.fold(Self::Yes, |x, y| match (x, y) {
(Self::Maybe, _) | (_, Self::Maybe) | (Self::No, Self::Yes) | (Self::Yes, Self::No) => Self::Maybe,
(Self::No, Self::No) => Self::No,
(Self::Yes, Self::Yes) => Self::Yes,
})
}
/// Merges the fields of a product type (e.g. a struct or tuple).
fn from_fields(mut iter: impl Iterator<Item = Self>) -> Self {
iter.try_fold(Self::Yes, |x, y| match (x, y) {
(Self::No, _) | (_, Self::No) => None,
(Self::Maybe, _) | (_, Self::Maybe) => Some(Self::Maybe),
(Self::Yes, Self::Yes) => Some(Self::Yes),
})
.unwrap_or(Self::No)
}
/// Checks if this is definitely `Freeze`.
fn is_freeze(self) -> bool {
matches!(self, Self::Yes)
}
/// Checks if this is definitely not `Freeze`.
fn is_not_freeze(self) -> bool {
matches!(self, Self::No)
}
}
/// What operation caused a borrow to occur.
#[derive(Clone, Copy)]
enum BorrowCause {
Borrow,
Deref,
Index,
AutoDeref,
AutoBorrow,
AutoDerefField,
}
impl BorrowCause {
fn note(self) -> Option<&'static str> {
match self {
Self::Borrow => None,
Self::Deref => Some("this deref expression is a call to `Deref::deref`"),
Self::Index => Some("this index expression is a call to `Index::index`"),
Self::AutoDeref => Some("there is a compiler inserted call to `Deref::deref` here"),
Self::AutoBorrow => Some("there is a compiler inserted borrow here"),
Self::AutoDerefField => {
Some("there is a compiler inserted call to `Deref::deref` when accessing this field")
},
}
}
}
/// The source of a borrow. Both what caused it and where.
struct BorrowSource<'tcx> {
expr: &'tcx Expr<'tcx>,
cause: BorrowCause,
}
impl<'tcx> BorrowSource<'tcx> {
fn new(tcx: TyCtxt<'tcx>, expr: &'tcx Expr<'tcx>, cause: BorrowCause) -> Self {
// Custom deref and index impls will always have an auto-borrow inserted since we
// never work with reference types.
let (expr, cause) = if matches!(cause, BorrowCause::AutoBorrow)
&& let Node::Expr(parent) = tcx.parent_hir_node(expr.hir_id)
{
match parent.kind {
ExprKind::Unary(UnOp::Deref, _) => (parent, BorrowCause::Deref),
ExprKind::Index(..) => (parent, BorrowCause::Index),
ExprKind::Field(..) => (parent, BorrowCause::AutoDerefField),
_ => (expr, cause),
}
} else {
(expr, cause)
};
Self { expr, cause }
}
}
pub struct NonCopyConst<'tcx> {
ignore_tys: DefIdSet,
// Cache checked types. We can recurse through a type multiple times so this
// can be hit quite frequently.
freeze_tys: FxHashMap<Ty<'tcx>, IsFreeze>,
}
impl_lint_pass!(NonCopyConst<'_> => [DECLARE_INTERIOR_MUTABLE_CONST, BORROW_INTERIOR_MUTABLE_CONST]);
impl<'tcx> NonCopyConst<'tcx> {
pub fn new(tcx: TyCtxt<'tcx>, conf: &'static Conf) -> Self {
Self {
ignore_tys: conf
.ignore_interior_mutability
.iter()
.flat_map(|ignored_ty| lookup_path_str(tcx, PathNS::Type, ignored_ty))
.collect(),
freeze_tys: FxHashMap::default(),
}
}
/// Checks if a value of the given type is `Freeze`, or may be depending on the value.
fn is_ty_freeze(&mut self, tcx: TyCtxt<'tcx>, typing_env: TypingEnv<'tcx>, ty: Ty<'tcx>) -> IsFreeze {
let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
match self.freeze_tys.entry(ty) {
Entry::Occupied(e) => *e.get(),
Entry::Vacant(e) => {
let e = e.insert(IsFreeze::Yes);
if ty.is_freeze(tcx, typing_env) {
return IsFreeze::Yes;
}
let is_freeze = match *ty.kind() {
ty::Adt(adt, _) if adt.is_unsafe_cell() => {
*e = IsFreeze::No;
return IsFreeze::No;
},
ty::Adt(adt, _) if self.ignore_tys.contains(&adt.did()) => return IsFreeze::Yes,
ty::Adt(adt, args) if adt.is_enum() => IsFreeze::from_variants(adt.variants().iter().map(|v| {
IsFreeze::from_fields(
v.fields
.iter()
.map(|f| self.is_ty_freeze(tcx, typing_env, f.ty(tcx, args))),
)
})),
// Workaround for `ManuallyDrop`-like unions.
ty::Adt(adt, args)
if adt.is_union()
&& adt.non_enum_variant().fields.iter().any(|f| {
tcx.layout_of(typing_env.as_query_input(f.ty(tcx, args)))
.is_ok_and(|l| l.layout.size().bytes() == 0)
}) =>
{
return IsFreeze::Yes;
},
// Rust doesn't have the concept of an active union field so we have
// to treat all fields as active.
ty::Adt(adt, args) => IsFreeze::from_fields(
adt.non_enum_variant()
.fields
.iter()
.map(|f| self.is_ty_freeze(tcx, typing_env, f.ty(tcx, args))),
),
ty::Array(ty, _) | ty::Pat(ty, _) => self.is_ty_freeze(tcx, typing_env, ty),
ty::Tuple(tys) => {
IsFreeze::from_fields(tys.iter().map(|ty| self.is_ty_freeze(tcx, typing_env, ty)))
},
// Treat type parameters as though they were `Freeze`.
ty::Param(_) | ty::Alias(..) => return IsFreeze::Yes,
// TODO: check other types.
_ => {
*e = IsFreeze::No;
return IsFreeze::No;
},
};
if !is_freeze.is_freeze() {
self.freeze_tys.insert(ty, is_freeze);
}
is_freeze
},
}
}
/// Checks if the given constant value is `Freeze`. Returns `Err` if the constant
/// cannot be read, but the result depends on the value.
fn is_value_freeze(
&mut self,
tcx: TyCtxt<'tcx>,
typing_env: TypingEnv<'tcx>,
ty: Ty<'tcx>,
val: ConstValue<'tcx>,
) -> Result<bool, ()> {
let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
match self.is_ty_freeze(tcx, typing_env, ty) {
IsFreeze::Yes => Ok(true),
IsFreeze::Maybe if matches!(ty.kind(), ty::Adt(..) | ty::Array(..) | ty::Tuple(..)) => {
for &(val, ty) in tcx
.try_destructure_mir_constant_for_user_output(val, ty)
.ok_or(())?
.fields
{
if !self.is_value_freeze(tcx, typing_env, ty, val)? {
return Ok(false);
}
}
Ok(true)
},
IsFreeze::Maybe | IsFreeze::No => Ok(false),
}
}
/// Checks if the given expression creates a value which is `Freeze`.
///
/// This will return `true` if the type is maybe `Freeze`, but it cannot be
/// determined for certain from the value.
///
/// `typing_env` and `gen_args` are from the constant's use site.
/// `typeck` and `e` are from the constant's definition site.
fn is_init_expr_freeze(
&mut self,
tcx: TyCtxt<'tcx>,
typing_env: TypingEnv<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
gen_args: GenericArgsRef<'tcx>,
e: &'tcx Expr<'tcx>,
) -> bool {
// Make sure to instantiate all types coming from `typeck` with `gen_args`.
let ty = EarlyBinder::bind(typeck.expr_ty(e)).instantiate(tcx, gen_args);
let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
match self.is_ty_freeze(tcx, typing_env, ty) {
IsFreeze::Yes => true,
IsFreeze::No => false,
IsFreeze::Maybe => match e.kind {
ExprKind::Block(b, _)
if !b.targeted_by_break
&& b.stmts.is_empty()
&& let Some(e) = b.expr =>
{
self.is_init_expr_freeze(tcx, typing_env, typeck, gen_args, e)
},
ExprKind::Path(ref p) => {
let res = typeck.qpath_res(p, e.hir_id);
let gen_args = EarlyBinder::bind(typeck.node_args(e.hir_id)).instantiate(tcx, gen_args);
match res {
Res::Def(DefKind::Const | DefKind::AssocConst, did)
if let Ok(val) =
tcx.const_eval_resolve(typing_env, UnevaluatedConst::new(did, gen_args), DUMMY_SP)
&& let Ok(is_freeze) = self.is_value_freeze(tcx, typing_env, ty, val) =>
{
is_freeze
},
Res::Def(DefKind::Const | DefKind::AssocConst, did)
if let Some((typeck, init)) = get_const_hir_value(tcx, typing_env, did, gen_args) =>
{
self.is_init_expr_freeze(tcx, typing_env, typeck, gen_args, init)
},
// Either this is a unit constructor, or some unknown value.
// In either case we consider the value to be `Freeze`.
_ => true,
}
},
ExprKind::Call(callee, args)
if let ExprKind::Path(p) = &callee.kind
&& let res = typeck.qpath_res(p, callee.hir_id)
&& matches!(res, Res::Def(DefKind::Ctor(..), _) | Res::SelfCtor(_)) =>
{
args.iter()
.all(|e| self.is_init_expr_freeze(tcx, typing_env, typeck, gen_args, e))
},
ExprKind::Struct(_, fields, StructTailExpr::None) => fields
.iter()
.all(|f| self.is_init_expr_freeze(tcx, typing_env, typeck, gen_args, f.expr)),
ExprKind::Tup(exprs) | ExprKind::Array(exprs) => exprs
.iter()
.all(|e| self.is_init_expr_freeze(tcx, typing_env, typeck, gen_args, e)),
ExprKind::Repeat(e, _) => self.is_init_expr_freeze(tcx, typing_env, typeck, gen_args, e),
_ => true,
},
}
}
/// Checks if the given expression (or a local projection of it) is both borrowed and
/// definitely a non-`Freeze` type.
fn is_non_freeze_expr_borrowed(
&mut self,
tcx: TyCtxt<'tcx>,
typing_env: TypingEnv<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
mut src_expr: &'tcx Expr<'tcx>,
) -> Option<BorrowSource<'tcx>> {
let mut parents = tcx.hir_parent_iter(src_expr.hir_id);
loop {
let ty = typeck.expr_ty(src_expr);
// Normalized as we need to check if this is an array later.
let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
let is_freeze = self.is_ty_freeze(tcx, typing_env, ty);
if is_freeze.is_freeze() {
return None;
}
if let [adjust, ..] = typeck.expr_adjustments(src_expr) {
return does_adjust_borrow(adjust)
.filter(|_| is_freeze.is_not_freeze())
.map(|cause| BorrowSource::new(tcx, src_expr, cause));
}
let Some((_, Node::Expr(use_expr))) = parents.next() else {
return None;
};
match use_expr.kind {
ExprKind::Field(..) => {},
ExprKind::Index(..) if ty.is_array() => {},
ExprKind::AddrOf(..) if is_freeze.is_not_freeze() => {
return Some(BorrowSource::new(tcx, use_expr, BorrowCause::Borrow));
},
// All other expressions use the value.
_ => return None,
}
src_expr = use_expr;
}
}
/// Checks if the given value (or a local projection of it) is both borrowed and
/// definitely non-`Freeze`. Returns `Err` if the constant cannot be read, but the
/// result depends on the value.
fn is_non_freeze_val_borrowed(
&mut self,
tcx: TyCtxt<'tcx>,
typing_env: TypingEnv<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
mut src_expr: &'tcx Expr<'tcx>,
mut val: ConstValue<'tcx>,
) -> Result<Option<BorrowSource<'tcx>>, ()> {
let mut parents = tcx.hir_parent_iter(src_expr.hir_id);
let mut ty = typeck.expr_ty(src_expr);
loop {
// Normalized as we need to check if this is an array later.
ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
if let [adjust, ..] = typeck.expr_adjustments(src_expr) {
let res = if let Some(cause) = does_adjust_borrow(adjust)
&& !self.is_value_freeze(tcx, typing_env, ty, val)?
{
Some(BorrowSource::new(tcx, src_expr, cause))
} else {
None
};
return Ok(res);
}
// Check only the type here as the result gets cached for each type.
if self.is_ty_freeze(tcx, typing_env, ty).is_freeze() {
return Ok(None);
}
let Some((_, Node::Expr(use_expr))) = parents.next() else {
return Ok(None);
};
let next_val = match use_expr.kind {
ExprKind::Field(_, name) => {
if let Some(idx) = get_field_idx_by_name(ty, name.name) {
tcx.try_destructure_mir_constant_for_user_output(val, ty)
.ok_or(())?
.fields
.get(idx)
} else {
return Ok(None);
}
},
ExprKind::Index(_, idx, _) if ty.is_array() => {
let val = tcx.try_destructure_mir_constant_for_user_output(val, ty).ok_or(())?;
if let Some(Constant::Int(idx)) = ConstEvalCtxt::with_env(tcx, typing_env, typeck).eval(idx) {
val.fields.get(idx as usize)
} else {
// It's some value in the array so check all of them.
for &(val, _) in val.fields {
if let Some(src) =
self.is_non_freeze_val_borrowed(tcx, typing_env, typeck, use_expr, val)?
{
return Ok(Some(src));
}
}
return Ok(None);
}
},
ExprKind::AddrOf(..) if !self.is_value_freeze(tcx, typing_env, ty, val)? => {
return Ok(Some(BorrowSource::new(tcx, use_expr, BorrowCause::Borrow)));
},
// All other expressions use the value.
_ => return Ok(None),
};
src_expr = use_expr;
if let Some(&(next_val, next_ty)) = next_val {
ty = next_ty;
val = next_val;
} else {
return Ok(None);
}
}
}
/// Checks if the given value (or a local projection of it) is both borrowed and
/// definitely non-`Freeze`.
///
/// `typing_env` and `init_args` are from the constant's use site.
/// `init_typeck` and `init_expr` are from the constant's definition site.
#[expect(clippy::too_many_arguments, clippy::too_many_lines)]
fn is_non_freeze_init_borrowed(
&mut self,
tcx: TyCtxt<'tcx>,
typing_env: TypingEnv<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
mut src_expr: &'tcx Expr<'tcx>,
mut init_typeck: &'tcx TypeckResults<'tcx>,
mut init_args: GenericArgsRef<'tcx>,
mut init_expr: &'tcx Expr<'tcx>,
) -> Option<BorrowSource<'tcx>> {
// Make sure to instantiate all types coming from `init_typeck` with `init_args`.
let mut parents = tcx.hir_parent_iter(src_expr.hir_id);
loop {
// First handle any adjustments since they are cheap to check.
if let [adjust, ..] = typeck.expr_adjustments(src_expr) {
return does_adjust_borrow(adjust)
.filter(|_| !self.is_init_expr_freeze(tcx, typing_env, init_typeck, init_args, init_expr))
.map(|cause| BorrowSource::new(tcx, src_expr, cause));
}
// Then read through constants and blocks on the init expression before
// applying the next use expression.
loop {
match init_expr.kind {
ExprKind::Block(b, _)
if !b.targeted_by_break
&& b.stmts.is_empty()
&& let Some(next_init) = b.expr =>
{
init_expr = next_init;
},
ExprKind::Path(ref init_path) => {
let next_init_args =
EarlyBinder::bind(init_typeck.node_args(init_expr.hir_id)).instantiate(tcx, init_args);
match init_typeck.qpath_res(init_path, init_expr.hir_id) {
Res::Def(DefKind::Ctor(..), _) => return None,
Res::Def(DefKind::Const | DefKind::AssocConst, did)
if let Ok(val) = tcx.const_eval_resolve(
typing_env,
UnevaluatedConst::new(did, next_init_args),
DUMMY_SP,
) && let Ok(res) =
self.is_non_freeze_val_borrowed(tcx, typing_env, init_typeck, src_expr, val) =>
{
return res;
},
Res::Def(DefKind::Const | DefKind::AssocConst, did)
if let Some((next_typeck, value)) =
get_const_hir_value(tcx, typing_env, did, next_init_args) =>
{
init_typeck = next_typeck;
init_args = next_init_args;
init_expr = value;
},
// There's no more that we can read from the init expression. Switch to a
// type based check.
_ => {
return self.is_non_freeze_expr_borrowed(tcx, typing_env, typeck, src_expr);
},
}
},
_ => break,
}
}
// Then a type check. Note we only check the type here as the result
// gets cached.
let ty = typeck.expr_ty(src_expr);
// Normalized as we need to check if this is an array later.
let ty = tcx.try_normalize_erasing_regions(typing_env, ty).unwrap_or(ty);
if self.is_ty_freeze(tcx, typing_env, ty).is_freeze() {
return None;
}
// Finally reduce the init expression using the next use expression.
let Some((_, Node::Expr(use_expr))) = parents.next() else {
return None;
};
init_expr = match &use_expr.kind {
ExprKind::Field(_, name) => match init_expr.kind {
ExprKind::Struct(_, fields, _)
if let Some(field) = fields.iter().find(|f| f.ident.name == name.name) =>
{
field.expr
},
ExprKind::Tup(fields)
if let Ok(idx) = name.as_str().parse::<usize>()
&& let Some(field) = fields.get(idx) =>
{
field
},
ExprKind::Call(callee, args)
if let ExprKind::Path(callee_path) = &callee.kind
&& matches!(
init_typeck.qpath_res(callee_path, callee.hir_id),
Res::Def(DefKind::Ctor(..), _) | Res::SelfCtor(_)
)
&& let Ok(idx) = name.as_str().parse::<usize>()
&& let Some(arg) = args.get(idx) =>
{
arg
},
// Revert to a type based check as we don't know the field's value.
_ => return self.is_non_freeze_expr_borrowed(tcx, typing_env, typeck, use_expr),
},
ExprKind::Index(_, idx, _) if ty.is_array() => match init_expr.kind {
ExprKind::Array(fields) => {
if let Some(Constant::Int(idx)) = ConstEvalCtxt::with_env(tcx, typing_env, typeck).eval(idx) {
// If the index is out of bounds it means the code
// unconditionally panics. In that case there is no borrow.
fields.get(idx as usize)?
} else {
// Unknown index, just run the check for all values.
return fields.iter().find_map(|f| {
self.is_non_freeze_init_borrowed(
tcx,
typing_env,
typeck,
use_expr,
init_typeck,
init_args,
f,
)
});
}
},
// Just assume the index expression doesn't panic here.
ExprKind::Repeat(field, _) => field,
_ => return self.is_non_freeze_expr_borrowed(tcx, typing_env, typeck, use_expr),
},
ExprKind::AddrOf(..)
if !self.is_init_expr_freeze(tcx, typing_env, init_typeck, init_args, init_expr) =>
{
return Some(BorrowSource::new(tcx, use_expr, BorrowCause::Borrow));
},
// All other expressions use the value.
_ => return None,
};
src_expr = use_expr;
}
}
}
impl<'tcx> LateLintPass<'tcx> for NonCopyConst<'tcx> {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
if let ItemKind::Const(ident, .., body_id) = item.kind
&& !ident.is_special()
&& let ty = cx.tcx.type_of(item.owner_id).instantiate_identity()
&& match self.is_ty_freeze(cx.tcx, cx.typing_env(), ty) {
IsFreeze::No => true,
IsFreeze::Yes => false,
IsFreeze::Maybe => match cx.tcx.const_eval_poly(item.owner_id.to_def_id()) {
Ok(val) if let Ok(is_freeze) = self.is_value_freeze(cx.tcx, cx.typing_env(), ty, val) => !is_freeze,
_ => !self.is_init_expr_freeze(
cx.tcx,
cx.typing_env(),
cx.tcx.typeck(item.owner_id),
GenericArgs::identity_for_item(cx.tcx, item.owner_id),
cx.tcx.hir_body(body_id).value,
),
},
}
&& !item.span.in_external_macro(cx.sess().source_map())
// Only needed when compiling `std`
&& !is_thread_local(cx, item)
{
span_lint_and_then(
cx,
DECLARE_INTERIOR_MUTABLE_CONST,
ident.span,
"named constant with interior mutability",
|diag| {
let Some(sync_trait) = cx.tcx.lang_items().sync_trait() else {
return;
};
if implements_trait(cx, ty, sync_trait, &[]) {
diag.help("did you mean to make this a `static` item");
} else {
diag.help("did you mean to make this a `thread_local!` item");
}
},
);
}
}
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) {
if let TraitItemKind::Const(_, body_id_opt) = item.kind
&& let ty = cx.tcx.type_of(item.owner_id).instantiate_identity()
&& match self.is_ty_freeze(cx.tcx, cx.typing_env(), ty) {
IsFreeze::No => true,
IsFreeze::Maybe if let Some(body_id) = body_id_opt => {
match cx.tcx.const_eval_poly(item.owner_id.to_def_id()) {
Ok(val) if let Ok(is_freeze) = self.is_value_freeze(cx.tcx, cx.typing_env(), ty, val) => {
!is_freeze
},
_ => !self.is_init_expr_freeze(
cx.tcx,
cx.typing_env(),
cx.tcx.typeck(item.owner_id),
GenericArgs::identity_for_item(cx.tcx, item.owner_id),
cx.tcx.hir_body(body_id).value,
),
}
},
IsFreeze::Yes | IsFreeze::Maybe => false,
}
&& !item.span.in_external_macro(cx.sess().source_map())
{
span_lint(
cx,
DECLARE_INTERIOR_MUTABLE_CONST,
item.ident.span,
"named constant with interior mutability",
);
}
}
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) {
if let ImplItemKind::Const(_, body_id) = item.kind
&& let ty = cx.tcx.type_of(item.owner_id).instantiate_identity()
&& match self.is_ty_freeze(cx.tcx, cx.typing_env(), ty) {
IsFreeze::Yes => false,
IsFreeze::No => {
// If this is a trait impl, check if the trait definition is the source
// of the cell.
if let Node::Item(parent_item) = cx.tcx.parent_hir_node(item.hir_id())
&& let ItemKind::Impl(impl_block) = parent_item.kind
&& let Some(of_trait) = impl_block.of_trait
&& let Some(trait_id) = of_trait.trait_def_id()
{
// Replace all instances of `<Self as Trait>::AssocType` with the
// unit type and check again. If the result is the same then the
// trait definition is the cause.
let ty = (ReplaceAssocFolder {
tcx: cx.tcx,
trait_id,
self_ty: cx.tcx.type_of(parent_item.owner_id).instantiate_identity(),
})
.fold_ty(cx.tcx.type_of(item.owner_id).instantiate_identity());
// `ty` may not be normalizable, but that should be fine.
!self.is_ty_freeze(cx.tcx, cx.typing_env(), ty).is_not_freeze()
} else {
true
}
},
// Even if this is from a trait, there are values which don't have
// interior mutability.
IsFreeze::Maybe => match cx.tcx.const_eval_poly(item.owner_id.to_def_id()) {
Ok(val) if let Ok(is_freeze) = self.is_value_freeze(cx.tcx, cx.typing_env(), ty, val) => !is_freeze,
_ => !self.is_init_expr_freeze(
cx.tcx,
cx.typing_env(),
cx.tcx.typeck(item.owner_id),
GenericArgs::identity_for_item(cx.tcx, item.owner_id),
cx.tcx.hir_body(body_id).value,
),
},
}
&& !item.span.in_external_macro(cx.sess().source_map())
{
span_lint(
cx,
DECLARE_INTERIOR_MUTABLE_CONST,
item.ident.span,
"named constant with interior mutability",
);
}
}
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
if let ExprKind::Path(qpath) = &e.kind
&& let typeck = cx.typeck_results()
&& let Res::Def(DefKind::Const | DefKind::AssocConst, did) = typeck.qpath_res(qpath, e.hir_id)
// As of `1.80` constant contexts can't borrow any type with interior mutability
&& !is_in_const_context(cx)
&& !self.is_ty_freeze(cx.tcx, cx.typing_env(), typeck.expr_ty(e)).is_freeze()
&& let Some(borrow_src) = {
// The extra block helps formatting a lot.
if let Ok(val) = cx.tcx.const_eval_resolve(
cx.typing_env(),
UnevaluatedConst::new(did, typeck.node_args(e.hir_id)),
DUMMY_SP,
) && let Ok(src) = self.is_non_freeze_val_borrowed(cx.tcx, cx.typing_env(), typeck, e, val)
{
src
} else if let init_args = typeck.node_args(e.hir_id)
&& let Some((init_typeck, init)) = get_const_hir_value(cx.tcx, cx.typing_env(), did, init_args)
{
self.is_non_freeze_init_borrowed(cx.tcx, cx.typing_env(), typeck, e, init_typeck, init_args, init)
} else {
self.is_non_freeze_expr_borrowed(cx.tcx, cx.typing_env(), typeck, e)
}
}
&& !borrow_src.expr.span.in_external_macro(cx.sess().source_map())
{
span_lint_and_then(
cx,
BORROW_INTERIOR_MUTABLE_CONST,
borrow_src.expr.span,
"borrow of a named constant with interior mutability",
|diag| {
if let Some(note) = borrow_src.cause.note() {
diag.note(note);
}
diag.help("this lint can be silenced by assigning the value to a local variable before borrowing");
},
);
}
}
}
struct ReplaceAssocFolder<'tcx> {
tcx: TyCtxt<'tcx>,
trait_id: DefId,
self_ty: Ty<'tcx>,
}
impl<'tcx> TypeFolder<TyCtxt<'tcx>> for ReplaceAssocFolder<'tcx> {
fn cx(&self) -> TyCtxt<'tcx> {
self.tcx
}
fn fold_ty(&mut self, ty: Ty<'tcx>) -> Ty<'tcx> {
if let ty::Alias(AliasTyKind::Projection, ty) = ty.kind()
&& ty.trait_def_id(self.tcx) == self.trait_id
&& ty.self_ty() == self.self_ty
{
self.tcx.types.unit
} else {
ty.super_fold_with(self)
}
}
}
fn is_thread_local(cx: &LateContext<'_>, it: &Item<'_>) -> bool {
macro_backtrace(it.span).any(|macro_call| {
matches!(
cx.tcx.get_diagnostic_name(macro_call.def_id),
Some(sym::thread_local_macro)
)
})
}
/// Checks if the adjustment causes a borrow of the original value. Returns
/// `None` if the value is consumed instead of borrowed.
fn does_adjust_borrow(adjust: &Adjustment<'_>) -> Option<BorrowCause> {
match adjust.kind {
Adjust::Borrow(_) => Some(BorrowCause::AutoBorrow),
// Custom deref calls `<T as Deref>::deref(&x)` resulting in a borrow.
Adjust::Deref(Some(_)) => Some(BorrowCause::AutoDeref),
// All other adjustments read the value.
_ => None,
}
}
/// Attempts to get the value of a constant as a HIR expression. Also gets the
/// `TypeckResults` associated with the constant's body.
fn get_const_hir_value<'tcx>(
tcx: TyCtxt<'tcx>,
typing_env: TypingEnv<'tcx>,
did: DefId,
args: GenericArgsRef<'tcx>,
) -> Option<(&'tcx TypeckResults<'tcx>, &'tcx Expr<'tcx>)> {
let did = did.as_local()?;
let (did, body_id) = match tcx.hir_node(tcx.local_def_id_to_hir_id(did)) {
Node::Item(item) if let ItemKind::Const(.., body_id) = item.kind => (did, body_id),
Node::ImplItem(item) if let ImplItemKind::Const(.., body_id) = item.kind => (did, body_id),
Node::TraitItem(_)
if let Ok(Some(inst)) = Instance::try_resolve(tcx, typing_env, did.into(), args)
&& let Some(did) = inst.def_id().as_local() =>
{
match tcx.hir_node(tcx.local_def_id_to_hir_id(did)) {
Node::ImplItem(item) if let ImplItemKind::Const(.., body_id) = item.kind => (did, body_id),
Node::TraitItem(item) if let TraitItemKind::Const(.., Some(body_id)) = item.kind => (did, body_id),
_ => return None,
}
},
_ => return None,
};
Some((tcx.typeck(did), tcx.hir_body(body_id).value))
}