diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a511c3e..f0684ea44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1194,6 +1194,7 @@ Released 2018-09-13 [`mut_from_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_from_ref [`mut_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_mut [`mut_range_bound`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_range_bound +[`mutable_key_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutable_key_type [`mutex_atomic`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutex_atomic [`mutex_integer`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutex_integer [`naive_bytecount`]: https://rust-lang.github.io/rust-clippy/master/index.html#naive_bytecount diff --git a/README.md b/README.md index 46fbc9dc9..be5433029 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code. -[There are 340 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html) +[There are 341 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html) We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you: diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index c3712e13d..c83be8e2c 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -241,6 +241,7 @@ pub mod missing_doc; pub mod missing_inline; pub mod mul_add; pub mod multiple_crate_versions; +pub mod mut_key; pub mod mut_mut; pub mod mut_reference; pub mod mutable_debug_assertion; @@ -668,6 +669,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf &missing_inline::MISSING_INLINE_IN_PUBLIC_ITEMS, &mul_add::MANUAL_MUL_ADD, &multiple_crate_versions::MULTIPLE_CRATE_VERSIONS, + &mut_key::MUTABLE_KEY_TYPE, &mut_mut::MUT_MUT, &mut_reference::UNNECESSARY_MUT_PASSED, &mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL, @@ -940,6 +942,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf store.register_late_pass(|| box trait_bounds::TraitBounds); store.register_late_pass(|| box comparison_chain::ComparisonChain); store.register_late_pass(|| box mul_add::MulAddCheck); + store.register_late_pass(|| box mut_key::MutableKeyType); store.register_early_pass(|| box reference::DerefAddrOf); store.register_early_pass(|| box reference::RefInDeref); store.register_early_pass(|| box double_parens::DoubleParens); @@ -1224,6 +1227,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf LintId::of(&misc_early::UNNEEDED_FIELD_PATTERN), LintId::of(&misc_early::UNNEEDED_WILDCARD_PATTERN), LintId::of(&misc_early::ZERO_PREFIXED_LITERAL), + LintId::of(&mut_key::MUTABLE_KEY_TYPE), LintId::of(&mut_reference::UNNECESSARY_MUT_PASSED), LintId::of(&mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL), LintId::of(&mutex_atomic::MUTEX_ATOMIC), @@ -1533,6 +1537,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf LintId::of(&misc::CMP_NAN), LintId::of(&misc::FLOAT_CMP), LintId::of(&misc::MODULO_ONE), + LintId::of(&mut_key::MUTABLE_KEY_TYPE), LintId::of(&mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL), LintId::of(&non_copy_const::BORROW_INTERIOR_MUTABLE_CONST), LintId::of(&non_copy_const::DECLARE_INTERIOR_MUTABLE_CONST), diff --git a/clippy_lints/src/mut_key.rs b/clippy_lints/src/mut_key.rs new file mode 100644 index 000000000..3a0899032 --- /dev/null +++ b/clippy_lints/src/mut_key.rs @@ -0,0 +1,120 @@ +use crate::utils::{match_def_path, paths, span_lint, trait_ref_of_method, walk_ptrs_ty}; +use rustc::declare_lint_pass; +use rustc::hir; +use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass}; +use rustc::ty::{Adt, Dynamic, Opaque, Param, RawPtr, Ref, Ty, TypeAndMut}; +use rustc_session::declare_tool_lint; +use syntax::source_map::Span; + +declare_clippy_lint! { + /// **What it does:** Checks for sets/maps with mutable key types. + /// + /// **Why is this bad?** All of `HashMap`, `HashSet`, `BTreeMap` and + /// `BtreeSet` rely on either the hash or the order of keys be unchanging, + /// so having types with interior mutability is a bad idea. + /// + /// **Known problems:** We don't currently account for `Rc` or `Arc`, so + /// this may yield false positives. + /// + /// **Example:** + /// ```rust + /// use std::cmp::{PartialEq, Eq}; + /// use std::collections::HashSet; + /// use std::hash::{Hash, Hasher}; + /// use std::sync::atomic::AtomicUsize; + ///# #[allow(unused)] + /// + /// struct Bad(AtomicUsize); + /// impl PartialEq for Bad { + /// fn eq(&self, rhs: &Self) -> bool { + /// .. + /// ; unimplemented!(); + /// } + /// } + /// + /// impl Eq for Bad {} + /// + /// impl Hash for Bad { + /// fn hash(&self, h: &mut H) { + /// .. + /// ; unimplemented!(); + /// } + /// } + /// + /// fn main() { + /// let _: HashSet = HashSet::new(); + /// } + /// ``` + pub MUTABLE_KEY_TYPE, + correctness, + "Check for mutable Map/Set key type" +} + +declare_lint_pass!(MutableKeyType => [ MUTABLE_KEY_TYPE ]); + +impl<'a, 'tcx> LateLintPass<'a, 'tcx> for MutableKeyType { + fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::Item<'tcx>) { + if let hir::ItemKind::Fn(ref sig, ..) = item.kind { + check_sig(cx, item.hir_id, &sig.decl); + } + } + + fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::ImplItem<'tcx>) { + if let hir::ImplItemKind::Method(ref sig, ..) = item.kind { + if trait_ref_of_method(cx, item.hir_id).is_none() { + check_sig(cx, item.hir_id, &sig.decl); + } + } + } + + fn check_trait_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::TraitItem<'tcx>) { + if let hir::TraitItemKind::Method(ref sig, ..) = item.kind { + check_sig(cx, item.hir_id, &sig.decl); + } + } + + fn check_local(&mut self, cx: &LateContext<'_, '_>, local: &hir::Local) { + if let hir::PatKind::Wild = local.pat.kind { + return; + } + check_ty(cx, local.span, cx.tables.pat_ty(&*local.pat)); + } +} + +fn check_sig<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, item_hir_id: hir::HirId, decl: &hir::FnDecl) { + let fn_def_id = cx.tcx.hir().local_def_id(item_hir_id); + let fn_sig = cx.tcx.fn_sig(fn_def_id); + for (hir_ty, ty) in decl.inputs.iter().zip(fn_sig.inputs().skip_binder().iter()) { + check_ty(cx, hir_ty.span, ty); + } + check_ty( + cx, + decl.output.span(), + cx.tcx.erase_late_bound_regions(&fn_sig.output()), + ); +} + +// We want to lint 1. sets or maps with 2. not immutable key types and 3. no unerased +// generics (because the compiler cannot ensure immutability for unknown types). +fn check_ty<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, span: Span, ty: Ty<'tcx>) { + let ty = walk_ptrs_ty(ty); + if let Adt(def, substs) = ty.kind { + if [&paths::HASHMAP, &paths::BTREEMAP, &paths::HASHSET, &paths::BTREESET] + .iter() + .any(|path| match_def_path(cx, def.did, &**path)) + { + let key_type = substs.type_at(0); + if is_concrete_type(key_type) && !key_type.is_freeze(cx.tcx, cx.param_env, span) { + span_lint(cx, MUTABLE_KEY_TYPE, span, "mutable key type"); + } + } + } +} + +fn is_concrete_type(ty: Ty<'_>) -> bool { + match ty.kind { + RawPtr(TypeAndMut { ty: inner_ty, .. }) | Ref(_, inner_ty, _) => is_concrete_type(inner_ty), + Dynamic(..) | Opaque(..) | Param(..) => false, + _ => true, + } +} diff --git a/src/lintlist/mod.rs b/src/lintlist/mod.rs index 913e94166..bdbe06c5f 100644 --- a/src/lintlist/mod.rs +++ b/src/lintlist/mod.rs @@ -6,7 +6,7 @@ pub use lint::Lint; pub use lint::LINT_LEVELS; // begin lint list, do not remove this comment, it’s used in `update_lints` -pub const ALL_LINTS: [Lint; 340] = [ +pub const ALL_LINTS: [Lint; 341] = [ Lint { name: "absurd_extreme_comparisons", group: "correctness", @@ -1239,6 +1239,13 @@ pub const ALL_LINTS: [Lint; 340] = [ deprecation: None, module: "loops", }, + Lint { + name: "mutable_key_type", + group: "correctness", + desc: "Check for mutable Map/Set key type", + deprecation: None, + module: "mut_key", + }, Lint { name: "mutex_atomic", group: "perf", diff --git a/tests/ui/mut_key.rs b/tests/ui/mut_key.rs new file mode 100644 index 000000000..5ec9b05f5 --- /dev/null +++ b/tests/ui/mut_key.rs @@ -0,0 +1,37 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; + +struct Key(AtomicUsize); + +impl Clone for Key { + fn clone(&self) -> Self { + Key(AtomicUsize::new(self.0.load(Relaxed))) + } +} + +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + self.0.load(Relaxed) == other.0.load(Relaxed) + } +} + +impl Eq for Key {} + +impl Hash for Key { + fn hash(&self, h: &mut H) { + self.0.load(Relaxed).hash(h); + } +} + +fn should_not_take_this_arg(m: &mut HashMap, _n: usize) -> HashSet { + let _other: HashMap = HashMap::new(); + m.keys().cloned().collect() +} + +fn this_is_ok(m: &mut HashMap) {} + +fn main() { + let _ = should_not_take_this_arg(&mut HashMap::new(), 1); + this_is_ok(&mut HashMap::new()); +} diff --git a/tests/ui/mut_key.stderr b/tests/ui/mut_key.stderr new file mode 100644 index 000000000..ebdbfe990 --- /dev/null +++ b/tests/ui/mut_key.stderr @@ -0,0 +1,22 @@ +error: mutable key type + --> $DIR/mut_key.rs:27:32 + | +LL | fn should_not_take_this_arg(m: &mut HashMap, _n: usize) -> HashSet { + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[deny(clippy::mutable_key_type)]` on by default + +error: mutable key type + --> $DIR/mut_key.rs:27:72 + | +LL | fn should_not_take_this_arg(m: &mut HashMap, _n: usize) -> HashSet { + | ^^^^^^^^^^^^ + +error: mutable key type + --> $DIR/mut_key.rs:28:5 + | +LL | let _other: HashMap = HashMap::new(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 3 previous errors +