X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=clippy_lints%2Fsrc%2Fnon_copy_const.rs;h=6c2563358cafe4d04f0e6c76281f562a376fdb72;hb=c86071898f76d0807cc9d9ac78a7aa4aff0688ae;hp=744cade461c0abc183367b5c33706690052453c8;hpb=42c36dc77b48771e112638b06443e8e08cacabfc;p=rust.git diff --git a/clippy_lints/src/non_copy_const.rs b/clippy_lints/src/non_copy_const.rs index 744cade461c..6c2563358ca 100644 --- a/clippy_lints/src/non_copy_const.rs +++ b/clippy_lints/src/non_copy_const.rs @@ -1,25 +1,35 @@ //! Checks for uses of const which the type is not `Freeze` (`Cell`-free). //! -//! This lint is **deny** by default. +//! This lint is **warn** by default. use std::ptr; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::in_constant; +use if_chain::if_chain; use rustc_hir::def::{DefKind, Res}; -use rustc_hir::{Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind, UnOp}; +use rustc_hir::def_id::DefId; +use rustc_hir::{ + BodyId, Expr, ExprKind, HirId, Impl, ImplItem, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind, UnOp, +}; +use rustc_infer::traits::specialization_graph; use rustc_lint::{LateContext, LateLintPass, Lint}; +use rustc_middle::mir::interpret::{ConstValue, ErrorHandled}; use rustc_middle::ty::adjustment::Adjust; -use rustc_middle::ty::{Ty, TypeFlags}; +use rustc_middle::ty::{self, AssocKind, Const, Ty}; use rustc_session::{declare_lint_pass, declare_tool_lint}; use rustc_span::{InnerSpan, Span, DUMMY_SP}; use rustc_typeck::hir_ty_to_ty; -use crate::utils::{in_constant, is_copy, qpath_res, span_lint_and_then}; - +// FIXME: this is a correctness problem but there's no suitable +// warn-by-default category. declare_clippy_lint! { - /// **What it does:** Checks for declaration of `const` items which is interior + /// ### What it does + /// Checks for declaration of `const` items which is interior /// mutable (e.g., contains a `Cell`, `Mutex`, `AtomicXxxx`, etc.). /// - /// **Why is this bad?** Consts are copied everywhere they are referenced, i.e., + /// ### Why is this bad? + /// Consts are copied everywhere they are referenced, i.e., /// every time you refer to the const a fresh instance of the `Cell` or `Mutex` /// or `AtomicXxxx` will be created, which defeats the whole purpose of using /// these types in the first place. @@ -27,12 +37,25 @@ /// The `const` should better be replaced by a `static` item if a global /// variable is wanted, or replaced by a `const fn` if a constructor is wanted. /// - /// **Known problems:** A "non-constant" const item is a legacy way to supply an + /// ### Known problems + /// A "non-constant" const item is a legacy way to supply an /// initialized value to downstream `static` items (e.g., the /// `std::sync::ONCE_INIT` constant). In this case the use of `const` is legit, /// and this lint should be suppressed. /// - /// **Example:** + /// Even though the lint avoids triggering on a constant whose type has enums that have variants + /// with interior mutability, and its value uses non interior mutable variants (see + /// [#3962](https://github.com/rust-lang/rust-clippy/issues/3962) and + /// [#3825](https://github.com/rust-lang/rust-clippy/issues/3825) for examples); + /// it complains about associated constants without default values only based on its types; + /// which might not be preferable. + /// There're other enums plus associated constants cases that the lint cannot handle. + /// + /// Types that have underlying or potential interior mutability trigger the lint whether + /// the interior mutable field is used or not. See issues + /// [#5812](https://github.com/rust-lang/rust-clippy/issues/5812) and + /// + /// ### Example /// ```rust /// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; /// @@ -47,24 +70,36 @@ /// assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance /// ``` pub DECLARE_INTERIOR_MUTABLE_CONST, - correctness, + style, "declaring `const` with interior mutability" } +// FIXME: this is a correctness problem but there's no suitable +// warn-by-default category. declare_clippy_lint! { - /// **What it does:** Checks if `const` items which is interior mutable (e.g., + /// ### What it does + /// Checks if `const` items which is interior mutable (e.g., /// contains a `Cell`, `Mutex`, `AtomicXxxx`, etc.) has been borrowed directly. /// - /// **Why is this bad?** Consts are copied everywhere they are referenced, i.e., + /// ### Why is this bad? + /// Consts are copied everywhere they are referenced, i.e., /// every time you refer to the const a fresh instance of the `Cell` or `Mutex` /// or `AtomicXxxx` will be created, which defeats the whole purpose of using /// these types in the first place. /// /// The `const` value should be stored inside a `static` item. /// - /// **Known problems:** None + /// ### Known problems + /// When an enum has variants with interior mutability, use of its non + /// interior mutable variants can generate false positives. See issue + /// [#3962](https://github.com/rust-lang/rust-clippy/issues/3962) /// - /// **Example:** + /// Types that have underlying or potential interior mutability trigger the lint whether + /// the interior mutable field is used or not. See issues + /// [#5812](https://github.com/rust-lang/rust-clippy/issues/5812) and + /// [#3825](https://github.com/rust-lang/rust-clippy/issues/3825) + /// + /// ### Example /// ```rust /// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; /// const CONST_ATOM: AtomicUsize = AtomicUsize::new(12); @@ -79,15 +114,93 @@ /// assert_eq!(STATIC_ATOM.load(SeqCst), 9); // use a `static` item to refer to the same instance /// ``` pub BORROW_INTERIOR_MUTABLE_CONST, - correctness, + style, "referencing `const` with interior mutability" } -#[allow(dead_code)] +fn is_unfrozen<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { + // Ignore types whose layout is unknown since `is_freeze` reports every generic types as `!Freeze`, + // making it indistinguishable from `UnsafeCell`. i.e. it isn't a tool to prove a type is + // 'unfrozen'. However, this code causes a false negative in which + // a type contains a layout-unknown type, but also a unsafe cell like `const CELL: Cell`. + // Yet, it's better than `ty.has_type_flags(TypeFlags::HAS_TY_PARAM | TypeFlags::HAS_PROJECTION)` + // since it works when a pointer indirection involves (`Cell<*const T>`). + // Making up a `ParamEnv` where every generic params and assoc types are `Freeze`is another option; + // but I'm not sure whether it's a decent way, if possible. + cx.tcx.layout_of(cx.param_env.and(ty)).is_ok() && !ty.is_freeze(cx.tcx.at(DUMMY_SP), cx.param_env) +} + +fn is_value_unfrozen_raw<'tcx>( + cx: &LateContext<'tcx>, + result: Result, ErrorHandled>, + ty: Ty<'tcx>, +) -> bool { + fn inner<'tcx>(cx: &LateContext<'tcx>, val: &'tcx Const<'tcx>) -> bool { + match val.ty.kind() { + // the fact that we have to dig into every structs to search enums + // leads us to the point checking `UnsafeCell` directly is the only option. + ty::Adt(ty_def, ..) if Some(ty_def.did) == cx.tcx.lang_items().unsafe_cell_type() => true, + ty::Array(..) | ty::Adt(..) | ty::Tuple(..) => { + let val = cx.tcx.destructure_const(cx.param_env.and(val)); + val.fields.iter().any(|field| inner(cx, field)) + }, + _ => false, + } + } + + result.map_or_else( + |err| { + // Consider `TooGeneric` cases as being unfrozen. + // This causes a false positive where an assoc const whose type is unfrozen + // have a value that is a frozen variant with a generic param (an example is + // `declare_interior_mutable_const::enums::BothOfCellAndGeneric::GENERIC_VARIANT`). + // However, it prevents a number of false negatives that is, I think, important: + // 1. assoc consts in trait defs referring to consts of themselves + // (an example is `declare_interior_mutable_const::traits::ConcreteTypes::ANOTHER_ATOMIC`). + // 2. a path expr referring to assoc consts whose type is doesn't have + // any frozen variants in trait defs (i.e. without substitute for `Self`). + // (e.g. borrowing `borrow_interior_mutable_const::trait::ConcreteTypes::ATOMIC`) + // 3. similar to the false positive above; + // but the value is an unfrozen variant, or the type has no enums. (An example is + // `declare_interior_mutable_const::enums::BothOfCellAndGeneric::UNFROZEN_VARIANT` + // and `declare_interior_mutable_const::enums::BothOfCellAndGeneric::NO_ENUM`). + // One might be able to prevent these FNs correctly, and replace this with `false`; + // e.g. implementing `has_frozen_variant` described above, and not running this function + // when the type doesn't have any frozen variants would be the 'correct' way for the 2nd + // case (that actually removes another suboptimal behavior (I won't say 'false positive') where, + // similar to 2., but with the a frozen variant) (e.g. borrowing + // `borrow_interior_mutable_const::enums::AssocConsts::TO_BE_FROZEN_VARIANT`). + // I chose this way because unfrozen enums as assoc consts are rare (or, hopefully, none). + err == ErrorHandled::TooGeneric + }, + |val| inner(cx, Const::from_value(cx.tcx, val, ty)), + ) +} + +fn is_value_unfrozen_poly<'tcx>(cx: &LateContext<'tcx>, body_id: BodyId, ty: Ty<'tcx>) -> bool { + let result = cx.tcx.const_eval_poly(body_id.hir_id.owner.to_def_id()); + is_value_unfrozen_raw(cx, result, ty) +} + +fn is_value_unfrozen_expr<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId, def_id: DefId, ty: Ty<'tcx>) -> bool { + let substs = cx.typeck_results().node_substs(hir_id); + + let result = cx.tcx.const_eval_resolve( + cx.param_env, + ty::Unevaluated { + def: ty::WithOptConstParam::unknown(def_id), + substs, + promoted: None, + }, + None, + ); + is_value_unfrozen_raw(cx, result, ty) +} + #[derive(Copy, Clone)] enum Source { Item { item: Span }, - Assoc { item: Span, ty: Span }, + Assoc { item: Span }, Expr { expr: Span }, } @@ -109,31 +222,20 @@ fn lint(&self) -> (&'static Lint, &'static str, Span) { } } -fn verify_ty_bound<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, ty: Ty<'tcx>, source: Source) { - if ty.is_freeze(cx.tcx, cx.param_env, DUMMY_SP) || is_copy(cx, ty) { - // An `UnsafeCell` is `!Copy`, and an `UnsafeCell` is also the only type which - // is `!Freeze`, thus if our type is `Copy` we can be sure it must be `Freeze` - // as well. - return; - } - +fn lint(cx: &LateContext<'_>, source: Source) { let (lint, msg, span) = source.lint(); - span_lint_and_then(cx, lint, span, msg, |db| { + span_lint_and_then(cx, lint, span, msg, |diag| { if span.from_expansion() { return; // Don't give suggestions into macros. } match source { Source::Item { .. } => { let const_kw_span = span.from_inner(InnerSpan::new(0, 5)); - db.span_label(const_kw_span, "make this a static item (maybe with lazy_static)"); - }, - Source::Assoc { ty: ty_span, .. } => { - if ty.flags.intersects(TypeFlags::HAS_FREE_LOCAL_NAMES) { - db.span_label(ty_span, &format!("consider requiring `{}` to be `Copy`", ty)); - } + diag.span_label(const_kw_span, "make this a static item (maybe with lazy_static)"); }, + Source::Assoc { .. } => (), Source::Expr { .. } => { - db.help("assign this const to a local or static variable, and use the variable here"); + diag.help("assign this const to a local or static variable, and use the variable here"); }, } }); @@ -141,48 +243,105 @@ fn verify_ty_bound<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, ty: Ty<'tcx>, source: S declare_lint_pass!(NonCopyConst => [DECLARE_INTERIOR_MUTABLE_CONST, BORROW_INTERIOR_MUTABLE_CONST]); -impl<'a, 'tcx> LateLintPass<'a, 'tcx> for NonCopyConst { - fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, it: &'tcx Item<'_>) { - if let ItemKind::Const(hir_ty, ..) = &it.kind { +impl<'tcx> LateLintPass<'tcx> for NonCopyConst { + fn check_item(&mut self, cx: &LateContext<'tcx>, it: &'tcx Item<'_>) { + if let ItemKind::Const(hir_ty, body_id) = it.kind { let ty = hir_ty_to_ty(cx.tcx, hir_ty); - verify_ty_bound(cx, ty, Source::Item { item: it.span }); + + if is_unfrozen(cx, ty) && is_value_unfrozen_poly(cx, body_id, ty) { + lint(cx, Source::Item { item: it.span }); + } } } - fn check_trait_item(&mut self, cx: &LateContext<'a, 'tcx>, trait_item: &'tcx TraitItem<'_>) { - if let TraitItemKind::Const(hir_ty, ..) = &trait_item.kind { + fn check_trait_item(&mut self, cx: &LateContext<'tcx>, trait_item: &'tcx TraitItem<'_>) { + if let TraitItemKind::Const(hir_ty, body_id_opt) = &trait_item.kind { let ty = hir_ty_to_ty(cx.tcx, hir_ty); - verify_ty_bound( - cx, - ty, - Source::Assoc { - ty: hir_ty.span, - item: trait_item.span, - }, - ); + + // Normalize assoc types because ones originated from generic params + // bounded other traits could have their bound. + let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty); + if is_unfrozen(cx, normalized) + // When there's no default value, lint it only according to its type; + // in other words, lint consts whose value *could* be unfrozen, not definitely is. + // This feels inconsistent with how the lint treats generic types, + // which avoids linting types which potentially become unfrozen. + // One could check whether a unfrozen type have a *frozen variant* + // (like `body_id_opt.map_or_else(|| !has_frozen_variant(...), ...)`), + // and do the same as the case of generic types at impl items. + // Note that it isn't sufficient to check if it has an enum + // since all of that enum's variants can be unfrozen: + // i.e. having an enum doesn't necessary mean a type has a frozen variant. + // And, implementing it isn't a trivial task; it'll probably end up + // re-implementing the trait predicate evaluation specific to `Freeze`. + && body_id_opt.map_or(true, |body_id| is_value_unfrozen_poly(cx, body_id, normalized)) + { + lint(cx, Source::Assoc { item: trait_item.span }); + } } } - fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, impl_item: &'tcx ImplItem<'_>) { - if let ImplItemKind::Const(hir_ty, ..) = &impl_item.kind { - let item_hir_id = cx.tcx.hir().get_parent_node(impl_item.hir_id); + fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) { + if let ImplItemKind::Const(hir_ty, body_id) = &impl_item.kind { + let item_hir_id = cx.tcx.hir().get_parent_node(impl_item.hir_id()); let item = cx.tcx.hir().expect_item(item_hir_id); - // Ensure the impl is an inherent impl. - if let ItemKind::Impl { of_trait: None, .. } = item.kind { - let ty = hir_ty_to_ty(cx.tcx, hir_ty); - verify_ty_bound( - cx, - ty, - Source::Assoc { - ty: hir_ty.span, - item: impl_item.span, - }, - ); + + match &item.kind { + ItemKind::Impl(Impl { + of_trait: Some(of_trait_ref), + .. + }) => { + if_chain! { + // Lint a trait impl item only when the definition is a generic type, + // assuming an assoc const is not meant to be an interior mutable type. + if let Some(of_trait_def_id) = of_trait_ref.trait_def_id(); + if let Some(of_assoc_item) = specialization_graph::Node::Trait(of_trait_def_id) + .item(cx.tcx, impl_item.ident, AssocKind::Const, of_trait_def_id); + if cx + .tcx + .layout_of(cx.tcx.param_env(of_trait_def_id).and( + // Normalize assoc types because ones originated from generic params + // bounded other traits could have their bound at the trait defs; + // and, in that case, the definition is *not* generic. + cx.tcx.normalize_erasing_regions( + cx.tcx.param_env(of_trait_def_id), + cx.tcx.type_of(of_assoc_item.def_id), + ), + )) + .is_err(); + // If there were a function like `has_frozen_variant` described above, + // we should use here as a frozen variant is a potential to be frozen + // similar to unknown layouts. + // e.g. `layout_of(...).is_err() || has_frozen_variant(...);` + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty); + if is_unfrozen(cx, normalized); + if is_value_unfrozen_poly(cx, *body_id, normalized); + then { + lint( + cx, + Source::Assoc { + item: impl_item.span, + }, + ); + } + } + }, + ItemKind::Impl(Impl { of_trait: None, .. }) => { + let ty = hir_ty_to_ty(cx.tcx, hir_ty); + // Normalize assoc types originated from generic params. + let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty); + + if is_unfrozen(cx, ty) && is_value_unfrozen_poly(cx, *body_id, normalized) { + lint(cx, Source::Assoc { item: impl_item.span }); + } + }, + _ => (), } } } - fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { if let ExprKind::Path(qpath) = &expr.kind { // Only lint if we use the const item inside a function. if in_constant(cx, expr.hir_id) { @@ -190,8 +349,8 @@ fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { } // Make sure it is a const item. - match qpath_res(cx, qpath, expr.hir_id) { - Res::Def(DefKind::Const | DefKind::AssocConst, _) => {}, + let item_def_id = match cx.qpath_res(qpath, expr.hir_id) { + Res::Def(DefKind::Const | DefKind::AssocConst, did) => did, _ => return, }; @@ -211,8 +370,21 @@ fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { needs_check_adjustment = false; }, ExprKind::Field(..) => { - dereferenced_expr = parent_expr; needs_check_adjustment = true; + + // Check whether implicit dereferences happened; + // if so, no need to go further up + // because of the same reason as the `ExprKind::Unary` case. + if cx + .typeck_results() + .expr_adjustments(dereferenced_expr) + .iter() + .any(|adj| matches!(adj.kind, Adjust::Deref(_))) + { + break; + } + + dereferenced_expr = parent_expr; }, ExprKind::Index(e, _) if ptr::eq(&**e, cur_expr) => { // `e[i]` => desugared to `*Index::index(&e, i)`, @@ -221,7 +393,7 @@ fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { needs_check_adjustment = false; break; }, - ExprKind::Unary(UnOp::UnDeref, _) => { + ExprKind::Unary(UnOp::Deref, _) => { // `*e` => desugared to `*Deref::deref(&e)`, // meaning `e` must be referenced. // no need to go further up since a method call is involved now. @@ -237,13 +409,13 @@ fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { } let ty = if needs_check_adjustment { - let adjustments = cx.tables.expr_adjustments(dereferenced_expr); - if let Some(i) = adjustments.iter().position(|adj| match adj.kind { - Adjust::Borrow(_) | Adjust::Deref(_) => true, - _ => false, - }) { + let adjustments = cx.typeck_results().expr_adjustments(dereferenced_expr); + if let Some(i) = adjustments + .iter() + .position(|adj| matches!(adj.kind, Adjust::Borrow(_) | Adjust::Deref(_))) + { if i == 0 { - cx.tables.expr_ty(dereferenced_expr) + cx.typeck_results().expr_ty(dereferenced_expr) } else { adjustments[i - 1].target } @@ -252,10 +424,12 @@ fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { return; } } else { - cx.tables.expr_ty(dereferenced_expr) + cx.typeck_results().expr_ty(dereferenced_expr) }; - verify_ty_bound(cx, ty, Source::Expr { expr: expr.span }); + if is_unfrozen(cx, ty) && is_value_unfrozen_expr(cx, expr.hir_id, item_def_id, ty) { + lint(cx, Source::Expr { expr: expr.span }); + } } } }