]>
Commit | Line | Data |
---|---|---|
136023e0 | 1 | #![cfg_attr(bootstrap, allow(rustc::default_hash_types))] |
cdc7bbd5 XL |
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_def_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, 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(fun, args) = e.kind; | |
334 | if let ExprKind::Path(QPath::TypeRelative(ty, method)) = fun.kind; | |
335 | if let TyKind::Path(QPath::Resolved(None, ty_path)) = ty.kind; | |
336 | if let Some(ty_did) = ty_path.res.opt_def_id(); | |
337 | then { | |
338 | if !TyS::same_type(self.target.ty(), self.maybe_typeck_results.unwrap().expr_ty(e)) { | |
339 | return; | |
340 | } | |
341 | ||
342 | if match_def_path(self.cx, ty_did, &paths::HASHMAP) { | |
343 | if method.ident.name == sym::new { | |
344 | self.suggestions | |
345 | .insert(e.span, "HashMap::default()".to_string()); | |
346 | } else if method.ident.name == sym!(with_capacity) { | |
347 | self.suggestions.insert( | |
348 | e.span, | |
349 | format!( | |
350 | "HashMap::with_capacity_and_hasher({}, Default::default())", | |
351 | snippet(self.cx, args[0].span, "capacity"), | |
352 | ), | |
353 | ); | |
354 | } | |
355 | } else if match_def_path(self.cx, ty_did, &paths::HASHSET) { | |
356 | if method.ident.name == sym::new { | |
357 | self.suggestions | |
358 | .insert(e.span, "HashSet::default()".to_string()); | |
359 | } else if method.ident.name == sym!(with_capacity) { | |
360 | self.suggestions.insert( | |
361 | e.span, | |
362 | format!( | |
363 | "HashSet::with_capacity_and_hasher({}, Default::default())", | |
364 | snippet(self.cx, args[0].span, "capacity"), | |
365 | ), | |
366 | ); | |
367 | } | |
368 | } | |
369 | } | |
370 | } | |
371 | ||
372 | walk_expr(self, e); | |
373 | } | |
374 | ||
375 | fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> { | |
376 | NestedVisitorMap::OnlyBodies(self.cx.tcx.hir()) | |
377 | } | |
378 | } |