Skip to content

Commit dad39b6

Browse files
committed
Move implicit_hasher to its own module
1 parent f231b59 commit dad39b6

File tree

3 files changed

+387
-377
lines changed

3 files changed

+387
-377
lines changed

clippy_lints/src/implicit_hasher.rs

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
#![allow(rustc::default_hash_types)]
2+
3+
use std::borrow::Cow;
4+
use std::collections::BTreeMap;
5+
6+
use rustc_errors::DiagnosticBuilder;
7+
use rustc_hir as hir;
8+
use rustc_hir::intravisit::{walk_body, walk_expr, walk_ty, NestedVisitorMap, Visitor};
9+
use rustc_hir::{Body, Expr, ExprKind, GenericArg, Item, ItemKind, QPath, TyKind};
10+
use rustc_lint::{LateContext, LateLintPass, LintContext};
11+
use rustc_middle::hir::map::Map;
12+
use rustc_middle::lint::in_external_macro;
13+
use rustc_middle::ty::{Ty, TyS, TypeckResults};
14+
use rustc_session::{declare_lint_pass, declare_tool_lint};
15+
use rustc_span::source_map::Span;
16+
use rustc_span::symbol::sym;
17+
use rustc_typeck::hir_ty_to_ty;
18+
19+
use if_chain::if_chain;
20+
21+
use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then};
22+
use clippy_utils::paths;
23+
use clippy_utils::source::{snippet, snippet_opt};
24+
use clippy_utils::ty::is_type_diagnostic_item;
25+
use clippy_utils::{differing_macro_contexts, match_path};
26+
27+
declare_clippy_lint! {
28+
/// **What it does:** Checks for public `impl` or `fn` missing generalization
29+
/// over different hashers and implicitly defaulting to the default hashing
30+
/// algorithm (`SipHash`).
31+
///
32+
/// **Why is this bad?** `HashMap` or `HashSet` with custom hashers cannot be
33+
/// used with them.
34+
///
35+
/// **Known problems:** Suggestions for replacing constructors can contain
36+
/// false-positives. Also applying suggestions can require modification of other
37+
/// pieces of code, possibly including external crates.
38+
///
39+
/// **Example:**
40+
/// ```rust
41+
/// # use std::collections::HashMap;
42+
/// # use std::hash::{Hash, BuildHasher};
43+
/// # trait Serialize {};
44+
/// impl<K: Hash + Eq, V> Serialize for HashMap<K, V> { }
45+
///
46+
/// pub fn foo(map: &mut HashMap<i32, i32>) { }
47+
/// ```
48+
/// could be rewritten as
49+
/// ```rust
50+
/// # use std::collections::HashMap;
51+
/// # use std::hash::{Hash, BuildHasher};
52+
/// # trait Serialize {};
53+
/// impl<K: Hash + Eq, V, S: BuildHasher> Serialize for HashMap<K, V, S> { }
54+
///
55+
/// pub fn foo<S: BuildHasher>(map: &mut HashMap<i32, i32, S>) { }
56+
/// ```
57+
pub IMPLICIT_HASHER,
58+
pedantic,
59+
"missing generalization over different hashers"
60+
}
61+
62+
declare_lint_pass!(ImplicitHasher => [IMPLICIT_HASHER]);
63+
64+
impl<'tcx> LateLintPass<'tcx> for ImplicitHasher {
65+
#[allow(clippy::cast_possible_truncation, clippy::too_many_lines)]
66+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
67+
use rustc_span::BytePos;
68+
69+
fn suggestion<'tcx>(
70+
cx: &LateContext<'tcx>,
71+
diag: &mut DiagnosticBuilder<'_>,
72+
generics_span: Span,
73+
generics_suggestion_span: Span,
74+
target: &ImplicitHasherType<'_>,
75+
vis: ImplicitHasherConstructorVisitor<'_, '_, '_>,
76+
) {
77+
let generics_snip = snippet(cx, generics_span, "");
78+
// trim `<` `>`
79+
let generics_snip = if generics_snip.is_empty() {
80+
""
81+
} else {
82+
&generics_snip[1..generics_snip.len() - 1]
83+
};
84+
85+
multispan_sugg(
86+
diag,
87+
"consider adding a type parameter",
88+
vec![
89+
(
90+
generics_suggestion_span,
91+
format!(
92+
"<{}{}S: ::std::hash::BuildHasher{}>",
93+
generics_snip,
94+
if generics_snip.is_empty() { "" } else { ", " },
95+
if vis.suggestions.is_empty() {
96+
""
97+
} else {
98+
// request users to add `Default` bound so that generic constructors can be used
99+
" + Default"
100+
},
101+
),
102+
),
103+
(
104+
target.span(),
105+
format!("{}<{}, S>", target.type_name(), target.type_arguments(),),
106+
),
107+
],
108+
);
109+
110+
if !vis.suggestions.is_empty() {
111+
multispan_sugg(diag, "...and use generic constructor", vis.suggestions);
112+
}
113+
}
114+
115+
if !cx.access_levels.is_exported(item.hir_id()) {
116+
return;
117+
}
118+
119+
match item.kind {
120+
ItemKind::Impl(ref impl_) => {
121+
let mut vis = ImplicitHasherTypeVisitor::new(cx);
122+
vis.visit_ty(impl_.self_ty);
123+
124+
for target in &vis.found {
125+
if differing_macro_contexts(item.span, target.span()) {
126+
return;
127+
}
128+
129+
let generics_suggestion_span = impl_.generics.span.substitute_dummy({
130+
let pos = snippet_opt(cx, item.span.until(target.span()))
131+
.and_then(|snip| Some(item.span.lo() + BytePos(snip.find("impl")? as u32 + 4)));
132+
if let Some(pos) = pos {
133+
Span::new(pos, pos, item.span.data().ctxt)
134+
} else {
135+
return;
136+
}
137+
});
138+
139+
let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target);
140+
for item in impl_.items.iter().map(|item| cx.tcx.hir().impl_item(item.id)) {
141+
ctr_vis.visit_impl_item(item);
142+
}
143+
144+
span_lint_and_then(
145+
cx,
146+
IMPLICIT_HASHER,
147+
target.span(),
148+
&format!(
149+
"impl for `{}` should be generalized over different hashers",
150+
target.type_name()
151+
),
152+
move |diag| {
153+
suggestion(cx, diag, impl_.generics.span, generics_suggestion_span, target, ctr_vis);
154+
},
155+
);
156+
}
157+
},
158+
ItemKind::Fn(ref sig, ref generics, body_id) => {
159+
let body = cx.tcx.hir().body(body_id);
160+
161+
for ty in sig.decl.inputs {
162+
let mut vis = ImplicitHasherTypeVisitor::new(cx);
163+
vis.visit_ty(ty);
164+
165+
for target in &vis.found {
166+
if in_external_macro(cx.sess(), generics.span) {
167+
continue;
168+
}
169+
let generics_suggestion_span = generics.span.substitute_dummy({
170+
let pos = snippet_opt(cx, item.span.until(body.params[0].pat.span))
171+
.and_then(|snip| {
172+
let i = snip.find("fn")?;
173+
Some(item.span.lo() + BytePos((i + (&snip[i..]).find('(')?) as u32))
174+
})
175+
.expect("failed to create span for type parameters");
176+
Span::new(pos, pos, item.span.data().ctxt)
177+
});
178+
179+
let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target);
180+
ctr_vis.visit_body(body);
181+
182+
span_lint_and_then(
183+
cx,
184+
IMPLICIT_HASHER,
185+
target.span(),
186+
&format!(
187+
"parameter of type `{}` should be generalized over different hashers",
188+
target.type_name()
189+
),
190+
move |diag| {
191+
suggestion(cx, diag, generics.span, generics_suggestion_span, target, ctr_vis);
192+
},
193+
);
194+
}
195+
}
196+
},
197+
_ => {},
198+
}
199+
}
200+
}
201+
202+
enum ImplicitHasherType<'tcx> {
203+
HashMap(Span, Ty<'tcx>, Cow<'static, str>, Cow<'static, str>),
204+
HashSet(Span, Ty<'tcx>, Cow<'static, str>),
205+
}
206+
207+
impl<'tcx> ImplicitHasherType<'tcx> {
208+
/// Checks that `ty` is a target type without a `BuildHasher`.
209+
fn new(cx: &LateContext<'tcx>, hir_ty: &hir::Ty<'_>) -> Option<Self> {
210+
if let TyKind::Path(QPath::Resolved(None, ref path)) = hir_ty.kind {
211+
let params: Vec<_> = path
212+
.segments
213+
.last()
214+
.as_ref()?
215+
.args
216+
.as_ref()?
217+
.args
218+
.iter()
219+
.filter_map(|arg| match arg {
220+
GenericArg::Type(ty) => Some(ty),
221+
_ => None,
222+
})
223+
.collect();
224+
let params_len = params.len();
225+
226+
let ty = hir_ty_to_ty(cx.tcx, hir_ty);
227+
228+
if is_type_diagnostic_item(cx, ty, sym::hashmap_type) && params_len == 2 {
229+
Some(ImplicitHasherType::HashMap(
230+
hir_ty.span,
231+
ty,
232+
snippet(cx, params[0].span, "K"),
233+
snippet(cx, params[1].span, "V"),
234+
))
235+
} else if is_type_diagnostic_item(cx, ty, sym::hashset_type) && params_len == 1 {
236+
Some(ImplicitHasherType::HashSet(
237+
hir_ty.span,
238+
ty,
239+
snippet(cx, params[0].span, "T"),
240+
))
241+
} else {
242+
None
243+
}
244+
} else {
245+
None
246+
}
247+
}
248+
249+
fn type_name(&self) -> &'static str {
250+
match *self {
251+
ImplicitHasherType::HashMap(..) => "HashMap",
252+
ImplicitHasherType::HashSet(..) => "HashSet",
253+
}
254+
}
255+
256+
fn type_arguments(&self) -> String {
257+
match *self {
258+
ImplicitHasherType::HashMap(.., ref k, ref v) => format!("{}, {}", k, v),
259+
ImplicitHasherType::HashSet(.., ref t) => format!("{}", t),
260+
}
261+
}
262+
263+
fn ty(&self) -> Ty<'tcx> {
264+
match *self {
265+
ImplicitHasherType::HashMap(_, ty, ..) | ImplicitHasherType::HashSet(_, ty, ..) => ty,
266+
}
267+
}
268+
269+
fn span(&self) -> Span {
270+
match *self {
271+
ImplicitHasherType::HashMap(span, ..) | ImplicitHasherType::HashSet(span, ..) => span,
272+
}
273+
}
274+
}
275+
276+
struct ImplicitHasherTypeVisitor<'a, 'tcx> {
277+
cx: &'a LateContext<'tcx>,
278+
found: Vec<ImplicitHasherType<'tcx>>,
279+
}
280+
281+
impl<'a, 'tcx> ImplicitHasherTypeVisitor<'a, 'tcx> {
282+
fn new(cx: &'a LateContext<'tcx>) -> Self {
283+
Self { cx, found: vec![] }
284+
}
285+
}
286+
287+
impl<'a, 'tcx> Visitor<'tcx> for ImplicitHasherTypeVisitor<'a, 'tcx> {
288+
type Map = Map<'tcx>;
289+
290+
fn visit_ty(&mut self, t: &'tcx hir::Ty<'_>) {
291+
if let Some(target) = ImplicitHasherType::new(self.cx, t) {
292+
self.found.push(target);
293+
}
294+
295+
walk_ty(self, t);
296+
}
297+
298+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
299+
NestedVisitorMap::None
300+
}
301+
}
302+
303+
/// Looks for default-hasher-dependent constructors like `HashMap::new`.
304+
struct ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> {
305+
cx: &'a LateContext<'tcx>,
306+
maybe_typeck_results: Option<&'tcx TypeckResults<'tcx>>,
307+
target: &'b ImplicitHasherType<'tcx>,
308+
suggestions: BTreeMap<Span, String>,
309+
}
310+
311+
impl<'a, 'b, 'tcx> ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> {
312+
fn new(cx: &'a LateContext<'tcx>, target: &'b ImplicitHasherType<'tcx>) -> Self {
313+
Self {
314+
cx,
315+
maybe_typeck_results: cx.maybe_typeck_results(),
316+
target,
317+
suggestions: BTreeMap::new(),
318+
}
319+
}
320+
}
321+
322+
impl<'a, 'b, 'tcx> Visitor<'tcx> for ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> {
323+
type Map = Map<'tcx>;
324+
325+
fn visit_body(&mut self, body: &'tcx Body<'_>) {
326+
let old_maybe_typeck_results = self.maybe_typeck_results.replace(self.cx.tcx.typeck_body(body.id()));
327+
walk_body(self, body);
328+
self.maybe_typeck_results = old_maybe_typeck_results;
329+
}
330+
331+
fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
332+
if_chain! {
333+
if let ExprKind::Call(ref fun, ref args) = e.kind;
334+
if let ExprKind::Path(QPath::TypeRelative(ref ty, ref method)) = fun.kind;
335+
if let TyKind::Path(QPath::Resolved(None, ty_path)) = ty.kind;
336+
then {
337+
if !TyS::same_type(self.target.ty(), self.maybe_typeck_results.unwrap().expr_ty(e)) {
338+
return;
339+
}
340+
341+
if match_path(ty_path, &paths::HASHMAP) {
342+
if method.ident.name == sym::new {
343+
self.suggestions
344+
.insert(e.span, "HashMap::default()".to_string());
345+
} else if method.ident.name == sym!(with_capacity) {
346+
self.suggestions.insert(
347+
e.span,
348+
format!(
349+
"HashMap::with_capacity_and_hasher({}, Default::default())",
350+
snippet(self.cx, args[0].span, "capacity"),
351+
),
352+
);
353+
}
354+
} else if match_path(ty_path, &paths::HASHSET) {
355+
if method.ident.name == sym::new {
356+
self.suggestions
357+
.insert(e.span, "HashSet::default()".to_string());
358+
} else if method.ident.name == sym!(with_capacity) {
359+
self.suggestions.insert(
360+
e.span,
361+
format!(
362+
"HashSet::with_capacity_and_hasher({}, Default::default())",
363+
snippet(self.cx, args[0].span, "capacity"),
364+
),
365+
);
366+
}
367+
}
368+
}
369+
}
370+
371+
walk_expr(self, e);
372+
}
373+
374+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
375+
NestedVisitorMap::OnlyBodies(self.cx.tcx.hir())
376+
}
377+
}

0 commit comments

Comments
 (0)