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