Skip to content

Commit e1b32ee

Browse files
committed
fix a FP in interior_mutable_const
fix a false positive in two `interior_mutable_const` lints where a constant with enums gets linted even if it uses a clearly unfrozen variant. Note that the code uses the MIR interpreter, which the author of #3962 thought unlikely to be a solution. This might be over-engineering; but, I think it's important to be able to work with the 'http' crate (#3825).
1 parent 5f33bc0 commit e1b32ee

File tree

6 files changed

+540
-36
lines changed

6 files changed

+540
-36
lines changed

clippy_lints/src/non_copy_const.rs

+136-36
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
use std::ptr;
66

77
use rustc_hir::def::{DefKind, Res};
8-
use rustc_hir::{Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind, UnOp};
8+
use rustc_hir::def_id::DefId;
9+
use rustc_hir::{
10+
BodyId, Expr, ExprKind, HirId, ImplItem, ImplItemKind, Item, ItemKind, Node, TraitItem, TraitItemKind, UnOp,
11+
};
912
use rustc_infer::traits::specialization_graph;
1013
use rustc_lint::{LateContext, LateLintPass, Lint};
14+
use rustc_middle::mir::interpret::{ConstValue, ErrorHandled};
1115
use rustc_middle::ty::adjustment::Adjust;
12-
use rustc_middle::ty::{AssocKind, Ty};
16+
use rustc_middle::ty::{self, AssocKind, Const, Ty};
1317
use rustc_session::{declare_lint_pass, declare_tool_lint};
1418
use rustc_span::{InnerSpan, Span, DUMMY_SP};
1519
use rustc_typeck::hir_ty_to_ty;
@@ -36,15 +40,18 @@ declare_clippy_lint! {
3640
/// `std::sync::ONCE_INIT` constant). In this case the use of `const` is legit,
3741
/// and this lint should be suppressed.
3842
///
39-
/// When an enum has variants with interior mutability, use of its non interior mutable
40-
/// variants can generate false positives. See issue
41-
/// [#3962](https://github.com/rust-lang/rust-clippy/issues/3962)
43+
/// Even though the lint avoids triggering on a constant whose type has enums that have variants
44+
/// with interior mutability, and its value uses non interior mutable variants (see
45+
/// [#3962](https://github.com/rust-lang/rust-clippy/issues/3962) and
46+
/// [#3825](https://github.com/rust-lang/rust-clippy/issues/3825) for examples);
47+
/// it complains about associated constants without default values only based on its types;
48+
/// which might not be preferable.
49+
/// There're other enums plus associated constants cases that the lint cannot handle.
4250
///
4351
/// Types that have underlying or potential interior mutability trigger the lint whether
4452
/// the interior mutable field is used or not. See issues
4553
/// [#5812](https://github.com/rust-lang/rust-clippy/issues/5812) and
46-
/// [#3825](https://github.com/rust-lang/rust-clippy/issues/3825)
47-
///
54+
///
4855
/// **Example:**
4956
/// ```rust
5057
/// use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
@@ -105,6 +112,79 @@ declare_clippy_lint! {
105112
"referencing `const` with interior mutability"
106113
}
107114

115+
fn is_unfrozen<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
116+
// Ignore types whose layout is unknown since `is_freeze` reports every generic types as `!Freeze`,
117+
// making it indistinguishable from `UnsafeCell`. i.e. it isn't a tool to prove a type is
118+
// 'unfrozen'. However, this code causes a false negative in which
119+
// a type contains a layout-unknown type, but also a unsafe cell like `const CELL: Cell<T>`.
120+
// Yet, it's better than `ty.has_type_flags(TypeFlags::HAS_TY_PARAM | TypeFlags::HAS_PROJECTION)`
121+
// since it works when a pointer indirection involves (`Cell<*const T>`).
122+
// Making up a `ParamEnv` where every generic params and assoc types are `Freeze`is another option;
123+
// but I'm not sure whether it's a decent way, if possible.
124+
cx.tcx.layout_of(cx.param_env.and(ty)).is_ok() && !ty.is_freeze(cx.tcx.at(DUMMY_SP), cx.param_env)
125+
}
126+
127+
fn is_value_unfrozen_raw<'tcx>(
128+
cx: &LateContext<'tcx>,
129+
result: Result<ConstValue<'tcx>, ErrorHandled>,
130+
ty: Ty<'tcx>,
131+
) -> bool {
132+
fn inner<'tcx>(cx: &LateContext<'tcx>, val: &'tcx Const<'tcx>) -> bool {
133+
match val.ty.kind() {
134+
// the fact that we have to dig into every structs to search enums
135+
// leads us to the point checking `UnsafeCell` directly is the only option.
136+
ty::Adt(ty_def, ..) if Some(ty_def.did) == cx.tcx.lang_items().unsafe_cell_type() => true,
137+
ty::Array(..) | ty::Adt(..) | ty::Tuple(..) => {
138+
let val = cx.tcx.destructure_const(cx.param_env.and(val));
139+
val.fields.iter().any(|field| inner(cx, field))
140+
},
141+
_ => false,
142+
}
143+
}
144+
145+
result.map_or_else(
146+
|err| {
147+
// Consider `TooGeneric` cases as being unfrozen.
148+
// This causes a false positive where an assoc const whose type is unfrozen
149+
// have a value that is a frozen variant with a generic param (an example is
150+
// `declare_interior_mutable_const::enums::BothOfCellAndGeneric::GENERIC_VARIANT`).
151+
// However, it prevents a number of false negatives that is, I think, important:
152+
// 1. assoc consts in trait defs referring to consts of themselves
153+
// (an example is `declare_interior_mutable_const::traits::ConcreteTypes::ANOTHER_ATOMIC`).
154+
// 2. a path expr referring to assoc consts whose type is doesn't have
155+
// any frozen variants in trait defs (i.e. without substitute for `Self`).
156+
// (e.g. borrowing `borrow_interior_mutable_const::trait::ConcreteTypes::ATOMIC`)
157+
// 3. similar to the false positive above;
158+
// but the value is an unfrozen variant, or the type has no enums. (An example is
159+
// `declare_interior_mutable_const::enums::BothOfCellAndGeneric::UNFROZEN_VARIANT`
160+
// and `declare_interior_mutable_const::enums::BothOfCellAndGeneric::NO_ENUM`).
161+
// One might be able to prevent these FNs correctly, and replace this with `false`;
162+
// e.g. implementing `has_frozen_variant` described above, and not running this function
163+
// when the type doesn't have any frozen variants would be the 'correct' way for the 2nd
164+
// case (that actually removes another suboptimal behavior (I won't say 'false positive') where,
165+
// similar to 2., but with the a frozen variant) (e.g. borrowing
166+
// `borrow_interior_mutable_const::enums::AssocConsts::TO_BE_FROZEN_VARIANT`).
167+
// I chose this way because unfrozen enums as assoc consts are rare (or, hopefully, none).
168+
err == ErrorHandled::TooGeneric
169+
},
170+
|val| inner(cx, Const::from_value(cx.tcx, val, ty)),
171+
)
172+
}
173+
174+
fn is_value_unfrozen_poly<'tcx>(cx: &LateContext<'tcx>, body_id: BodyId, ty: Ty<'tcx>) -> bool {
175+
let result = cx.tcx.const_eval_poly(body_id.hir_id.owner.to_def_id());
176+
is_value_unfrozen_raw(cx, result, ty)
177+
}
178+
179+
fn is_value_unfrozen_expr<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId, def_id: DefId, ty: Ty<'tcx>) -> bool {
180+
let substs = cx.typeck_results().node_substs(hir_id);
181+
182+
let result = cx
183+
.tcx
184+
.const_eval_resolve(cx.param_env, ty::WithOptConstParam::unknown(def_id), substs, None, None);
185+
is_value_unfrozen_raw(cx, result, ty)
186+
}
187+
108188
#[derive(Copy, Clone)]
109189
enum Source {
110190
Item { item: Span },
@@ -130,19 +210,7 @@ impl Source {
130210
}
131211
}
132212

133-
fn verify_ty_bound<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, source: Source) {
134-
// Ignore types whose layout is unknown since `is_freeze` reports every generic types as `!Freeze`,
135-
// making it indistinguishable from `UnsafeCell`. i.e. it isn't a tool to prove a type is
136-
// 'unfrozen'. However, this code causes a false negative in which
137-
// a type contains a layout-unknown type, but also a unsafe cell like `const CELL: Cell<T>`.
138-
// Yet, it's better than `ty.has_type_flags(TypeFlags::HAS_TY_PARAM | TypeFlags::HAS_PROJECTION)`
139-
// since it works when a pointer indirection involves (`Cell<*const T>`).
140-
// Making up a `ParamEnv` where every generic params and assoc types are `Freeze`is another option;
141-
// but I'm not sure whether it's a decent way, if possible.
142-
if cx.tcx.layout_of(cx.param_env.and(ty)).is_err() || ty.is_freeze(cx.tcx.at(DUMMY_SP), cx.param_env) {
143-
return;
144-
}
145-
213+
fn lint(cx: &LateContext<'_>, source: Source) {
146214
let (lint, msg, span) = source.lint();
147215
span_lint_and_then(cx, lint, span, msg, |diag| {
148216
if span.from_expansion() {
@@ -165,24 +233,44 @@ declare_lint_pass!(NonCopyConst => [DECLARE_INTERIOR_MUTABLE_CONST, BORROW_INTER
165233

166234
impl<'tcx> LateLintPass<'tcx> for NonCopyConst {
167235
fn check_item(&mut self, cx: &LateContext<'tcx>, it: &'tcx Item<'_>) {
168-
if let ItemKind::Const(hir_ty, ..) = &it.kind {
236+
if let ItemKind::Const(hir_ty, body_id) = it.kind {
169237
let ty = hir_ty_to_ty(cx.tcx, hir_ty);
170-
verify_ty_bound(cx, ty, Source::Item { item: it.span });
238+
239+
if is_unfrozen(cx, ty) && is_value_unfrozen_poly(cx, body_id, ty) {
240+
lint(cx, Source::Item { item: it.span });
241+
}
171242
}
172243
}
173244

174245
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, trait_item: &'tcx TraitItem<'_>) {
175-
if let TraitItemKind::Const(hir_ty, ..) = &trait_item.kind {
246+
if let TraitItemKind::Const(hir_ty, body_id_opt) = &trait_item.kind {
176247
let ty = hir_ty_to_ty(cx.tcx, hir_ty);
248+
177249
// Normalize assoc types because ones originated from generic params
178250
// bounded other traits could have their bound.
179251
let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty);
180-
verify_ty_bound(cx, normalized, Source::Assoc { item: trait_item.span });
252+
if is_unfrozen(cx, normalized)
253+
// When there's no default value, lint it only according to its type;
254+
// in other words, lint consts whose value *could* be unfrozen, not definitely is.
255+
// This feels inconsistent with how the lint treats generic types,
256+
// which avoids linting types which potentially become unfrozen.
257+
// One could check whether a unfrozen type have a *frozen variant*
258+
// (like `body_id_opt.map_or_else(|| !has_frozen_variant(...), ...)`),
259+
// and do the same as the case of generic types at impl items.
260+
// Note that it isn't sufficient to check if it has an enum
261+
// since all of that enum's variants can be unfrozen:
262+
// i.e. having an enum doesn't necessary mean a type has a frozen variant.
263+
// And, implementing it isn't a trivial task; it'll probably end up
264+
// re-implementing the trait predicate evaluation specific to `Freeze`.
265+
&& body_id_opt.map_or(true, |body_id| is_value_unfrozen_poly(cx, body_id, normalized))
266+
{
267+
lint(cx, Source::Assoc { item: trait_item.span });
268+
}
181269
}
182270
}
183271

184272
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) {
185-
if let ImplItemKind::Const(hir_ty, ..) = &impl_item.kind {
273+
if let ImplItemKind::Const(hir_ty, body_id) = &impl_item.kind {
186274
let item_hir_id = cx.tcx.hir().get_parent_node(impl_item.hir_id);
187275
let item = cx.tcx.hir().expect_item(item_hir_id);
188276

@@ -209,24 +297,34 @@ impl<'tcx> LateLintPass<'tcx> for NonCopyConst {
209297
),
210298
))
211299
.is_err();
300+
// If there were a function like `has_frozen_variant` described above,
301+
// we should use here as a frozen variant is a potential to be frozen
302+
// similar to unknown layouts.
303+
// e.g. `layout_of(...).is_err() || has_frozen_variant(...);`
212304
then {
213305
let ty = hir_ty_to_ty(cx.tcx, hir_ty);
214306
let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty);
215-
verify_ty_bound(
216-
cx,
217-
normalized,
218-
Source::Assoc {
219-
item: impl_item.span,
220-
},
221-
);
307+
if is_unfrozen(cx, normalized)
308+
&& is_value_unfrozen_poly(cx, *body_id, normalized)
309+
{
310+
lint(
311+
cx,
312+
Source::Assoc {
313+
item: impl_item.span,
314+
},
315+
);
316+
}
222317
}
223318
}
224319
},
225320
ItemKind::Impl { of_trait: None, .. } => {
226321
let ty = hir_ty_to_ty(cx.tcx, hir_ty);
227322
// Normalize assoc types originated from generic params.
228323
let normalized = cx.tcx.normalize_erasing_regions(cx.param_env, ty);
229-
verify_ty_bound(cx, normalized, Source::Assoc { item: impl_item.span });
324+
325+
if is_unfrozen(cx, ty) && is_value_unfrozen_poly(cx, *body_id, normalized) {
326+
lint(cx, Source::Assoc { item: impl_item.span });
327+
}
230328
},
231329
_ => (),
232330
}
@@ -241,8 +339,8 @@ impl<'tcx> LateLintPass<'tcx> for NonCopyConst {
241339
}
242340

243341
// Make sure it is a const item.
244-
match qpath_res(cx, qpath, expr.hir_id) {
245-
Res::Def(DefKind::Const | DefKind::AssocConst, _) => {},
342+
let item_def_id = match qpath_res(cx, qpath, expr.hir_id) {
343+
Res::Def(DefKind::Const | DefKind::AssocConst, did) => did,
246344
_ => return,
247345
};
248346

@@ -319,7 +417,9 @@ impl<'tcx> LateLintPass<'tcx> for NonCopyConst {
319417
cx.typeck_results().expr_ty(dereferenced_expr)
320418
};
321419

322-
verify_ty_bound(cx, ty, Source::Expr { expr: expr.span });
420+
if is_unfrozen(cx, ty) && is_value_unfrozen_expr(cx, expr.hir_id, item_def_id, ty) {
421+
lint(cx, Source::Expr { expr: expr.span });
422+
}
323423
}
324424
}
325425
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// this file solely exists to test constants defined in foreign crates.
2+
// As the most common case is the `http` crate, it replicates `http::HeadewrName`'s structure.
3+
4+
#![allow(clippy::declare_interior_mutable_const)]
5+
6+
use std::sync::atomic::AtomicUsize;
7+
8+
enum Private<T> {
9+
ToBeUnfrozen(T),
10+
Frozen(usize),
11+
}
12+
13+
pub struct Wrapper(Private<AtomicUsize>);
14+
15+
pub const WRAPPED_PRIVATE_UNFROZEN_VARIANT: Wrapper = Wrapper(Private::ToBeUnfrozen(AtomicUsize::new(6)));
16+
pub const WRAPPED_PRIVATE_FROZEN_VARIANT: Wrapper = Wrapper(Private::Frozen(7));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// aux-build:helper.rs
2+
3+
#![warn(clippy::borrow_interior_mutable_const)]
4+
#![allow(clippy::declare_interior_mutable_const)]
5+
6+
// this file (mostly) replicates its `declare` counterpart. Please see it for more discussions.
7+
8+
extern crate helper;
9+
10+
use std::cell::Cell;
11+
use std::sync::atomic::AtomicUsize;
12+
13+
enum OptionalCell {
14+
Unfrozen(Cell<bool>),
15+
Frozen,
16+
}
17+
18+
const UNFROZEN_VARIANT: OptionalCell = OptionalCell::Unfrozen(Cell::new(true));
19+
const FROZEN_VARIANT: OptionalCell = OptionalCell::Frozen;
20+
21+
fn borrow_optional_cell() {
22+
let _ = &UNFROZEN_VARIANT; //~ ERROR interior mutability
23+
let _ = &FROZEN_VARIANT;
24+
}
25+
26+
trait AssocConsts {
27+
const TO_BE_UNFROZEN_VARIANT: OptionalCell;
28+
const TO_BE_FROZEN_VARIANT: OptionalCell;
29+
30+
const DEFAULTED_ON_UNFROZEN_VARIANT: OptionalCell = OptionalCell::Unfrozen(Cell::new(false));
31+
const DEFAULTED_ON_FROZEN_VARIANT: OptionalCell = OptionalCell::Frozen;
32+
33+
fn function() {
34+
// This is the "suboptimal behavior" mentioned in `is_value_unfrozen`
35+
// caused by a similar reason to unfrozen types without any default values
36+
// get linted even if it has frozen variants'.
37+
let _ = &Self::TO_BE_FROZEN_VARIANT; //~ ERROR interior mutable
38+
39+
// The lint ignores default values because an impl of this trait can set
40+
// an unfrozen variant to `DEFAULTED_ON_FROZEN_VARIANT` and use the default impl for `function`.
41+
let _ = &Self::DEFAULTED_ON_FROZEN_VARIANT; //~ ERROR interior mutable
42+
}
43+
}
44+
45+
impl AssocConsts for u64 {
46+
const TO_BE_UNFROZEN_VARIANT: OptionalCell = OptionalCell::Unfrozen(Cell::new(false));
47+
const TO_BE_FROZEN_VARIANT: OptionalCell = OptionalCell::Frozen;
48+
49+
fn function() {
50+
let _ = &<Self as AssocConsts>::TO_BE_UNFROZEN_VARIANT; //~ ERROR interior mutable
51+
let _ = &<Self as AssocConsts>::TO_BE_FROZEN_VARIANT;
52+
let _ = &Self::DEFAULTED_ON_UNFROZEN_VARIANT; //~ ERROR interior mutable
53+
let _ = &Self::DEFAULTED_ON_FROZEN_VARIANT;
54+
}
55+
}
56+
57+
trait AssocTypes {
58+
type ToBeUnfrozen;
59+
60+
const TO_BE_UNFROZEN_VARIANT: Option<Self::ToBeUnfrozen>;
61+
const TO_BE_FROZEN_VARIANT: Option<Self::ToBeUnfrozen>;
62+
63+
// there's no need to test here because it's the exactly same as `trait::AssocTypes`
64+
fn function();
65+
}
66+
67+
impl AssocTypes for u64 {
68+
type ToBeUnfrozen = AtomicUsize;
69+
70+
const TO_BE_UNFROZEN_VARIANT: Option<Self::ToBeUnfrozen> = Some(Self::ToBeUnfrozen::new(4)); //~ ERROR interior mutable
71+
const TO_BE_FROZEN_VARIANT: Option<Self::ToBeUnfrozen> = None;
72+
73+
fn function() {
74+
let _ = &<Self as AssocTypes>::TO_BE_UNFROZEN_VARIANT; //~ ERROR interior mutable
75+
let _ = &<Self as AssocTypes>::TO_BE_FROZEN_VARIANT;
76+
}
77+
}
78+
79+
enum BothOfCellAndGeneric<T> {
80+
Unfrozen(Cell<*const T>),
81+
Generic(*const T),
82+
Frozen(usize),
83+
}
84+
85+
impl<T> BothOfCellAndGeneric<T> {
86+
const UNFROZEN_VARIANT: BothOfCellAndGeneric<T> = BothOfCellAndGeneric::Unfrozen(Cell::new(std::ptr::null())); //~ ERROR interior mutable
87+
const GENERIC_VARIANT: BothOfCellAndGeneric<T> = BothOfCellAndGeneric::Generic(std::ptr::null()); //~ ERROR interior mutable
88+
const FROZEN_VARIANT: BothOfCellAndGeneric<T> = BothOfCellAndGeneric::Frozen(5);
89+
90+
fn function() {
91+
let _ = &Self::UNFROZEN_VARIANT; //~ ERROR interior mutability
92+
let _ = &Self::GENERIC_VARIANT; //~ ERROR interior mutability
93+
let _ = &Self::FROZEN_VARIANT;
94+
}
95+
}
96+
97+
fn main() {
98+
// constants defined in foreign crates
99+
let _ = &helper::WRAPPED_PRIVATE_UNFROZEN_VARIANT; //~ ERROR interior mutability
100+
let _ = &helper::WRAPPED_PRIVATE_FROZEN_VARIANT;
101+
}

0 commit comments

Comments
 (0)