]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/implicit_hasher.rs
New upstream version 1.63.0+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / implicit_hasher.rs
1 use std::borrow::Cow;
2 use std::collections::BTreeMap;
3
4 use rustc_errors::Diagnostic;
5 use rustc_hir as hir;
6 use rustc_hir::intravisit::{walk_body, walk_expr, walk_inf, walk_ty, Visitor};
7 use rustc_hir::{Body, Expr, ExprKind, GenericArg, Item, ItemKind, QPath, TyKind};
8 use rustc_lint::{LateContext, LateLintPass, LintContext};
9 use rustc_middle::hir::nested_filter;
10 use rustc_middle::lint::in_external_macro;
11 use rustc_middle::ty::{Ty, TypeckResults};
12 use rustc_session::{declare_lint_pass, declare_tool_lint};
13 use rustc_span::source_map::Span;
14 use rustc_span::symbol::sym;
15 use rustc_typeck::hir_ty_to_ty;
16
17 use if_chain::if_chain;
18
19 use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then};
20 use clippy_utils::source::{snippet, snippet_opt};
21 use clippy_utils::ty::is_type_diagnostic_item;
22
23 declare_clippy_lint! {
24 /// ### What it does
25 /// Checks for public `impl` or `fn` missing generalization
26 /// over different hashers and implicitly defaulting to the default hashing
27 /// algorithm (`SipHash`).
28 ///
29 /// ### Why is this bad?
30 /// `HashMap` or `HashSet` with custom hashers cannot be
31 /// used with them.
32 ///
33 /// ### Known problems
34 /// Suggestions for replacing constructors can contain
35 /// false-positives. Also applying suggestions can require modification of other
36 /// pieces of code, possibly including external crates.
37 ///
38 /// ### Example
39 /// ```rust
40 /// # use std::collections::HashMap;
41 /// # use std::hash::{Hash, BuildHasher};
42 /// # trait Serialize {};
43 /// impl<K: Hash + Eq, V> Serialize for HashMap<K, V> { }
44 ///
45 /// pub fn foo(map: &mut HashMap<i32, i32>) { }
46 /// ```
47 /// could be rewritten as
48 /// ```rust
49 /// # use std::collections::HashMap;
50 /// # use std::hash::{Hash, BuildHasher};
51 /// # trait Serialize {};
52 /// impl<K: Hash + Eq, V, S: BuildHasher> Serialize for HashMap<K, V, S> { }
53 ///
54 /// pub fn foo<S: BuildHasher>(map: &mut HashMap<i32, i32, S>) { }
55 /// ```
56 #[clippy::version = "pre 1.29.0"]
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 #[expect(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 Diagnostic,
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.def_id) {
116 return;
117 }
118
119 match item.kind {
120 ItemKind::Impl(impl_) => {
121 let mut vis = ImplicitHasherTypeVisitor::new(cx);
122 vis.visit_ty(impl_.self_ty);
123
124 for target in &vis.found {
125 if item.span.ctxt() != target.span().ctxt() {
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.ctxt(), item.span.parent())
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, 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(
171 cx,
172 Span::new(
173 item.span.lo(),
174 body.params[0].pat.span.lo(),
175 item.span.ctxt(),
176 item.span.parent(),
177 ),
178 )
179 .and_then(|snip| {
180 let i = snip.find("fn")?;
181 Some(item.span.lo() + BytePos((i + snip[i..].find('(')?) as u32))
182 })
183 .expect("failed to create span for type parameters");
184 Span::new(pos, pos, item.span.ctxt(), item.span.parent())
185 });
186
187 let mut ctr_vis = ImplicitHasherConstructorVisitor::new(cx, target);
188 ctr_vis.visit_body(body);
189
190 span_lint_and_then(
191 cx,
192 IMPLICIT_HASHER,
193 target.span(),
194 &format!(
195 "parameter of type `{}` should be generalized over different hashers",
196 target.type_name()
197 ),
198 move |diag| {
199 suggestion(cx, diag, generics.span, generics_suggestion_span, target, ctr_vis);
200 },
201 );
202 }
203 }
204 },
205 _ => {},
206 }
207 }
208 }
209
210 enum ImplicitHasherType<'tcx> {
211 HashMap(Span, Ty<'tcx>, Cow<'static, str>, Cow<'static, str>),
212 HashSet(Span, Ty<'tcx>, Cow<'static, str>),
213 }
214
215 impl<'tcx> ImplicitHasherType<'tcx> {
216 /// Checks that `ty` is a target type without a `BuildHasher`.
217 fn new(cx: &LateContext<'tcx>, hir_ty: &hir::Ty<'_>) -> Option<Self> {
218 if let TyKind::Path(QPath::Resolved(None, path)) = hir_ty.kind {
219 let params: Vec<_> = path
220 .segments
221 .last()
222 .as_ref()?
223 .args
224 .as_ref()?
225 .args
226 .iter()
227 .filter_map(|arg| match arg {
228 GenericArg::Type(ty) => Some(ty),
229 _ => None,
230 })
231 .collect();
232 let params_len = params.len();
233
234 let ty = hir_ty_to_ty(cx.tcx, hir_ty);
235
236 if is_type_diagnostic_item(cx, ty, sym::HashMap) && params_len == 2 {
237 Some(ImplicitHasherType::HashMap(
238 hir_ty.span,
239 ty,
240 snippet(cx, params[0].span, "K"),
241 snippet(cx, params[1].span, "V"),
242 ))
243 } else if is_type_diagnostic_item(cx, ty, sym::HashSet) && params_len == 1 {
244 Some(ImplicitHasherType::HashSet(
245 hir_ty.span,
246 ty,
247 snippet(cx, params[0].span, "T"),
248 ))
249 } else {
250 None
251 }
252 } else {
253 None
254 }
255 }
256
257 fn type_name(&self) -> &'static str {
258 match *self {
259 ImplicitHasherType::HashMap(..) => "HashMap",
260 ImplicitHasherType::HashSet(..) => "HashSet",
261 }
262 }
263
264 fn type_arguments(&self) -> String {
265 match *self {
266 ImplicitHasherType::HashMap(.., ref k, ref v) => format!("{}, {}", k, v),
267 ImplicitHasherType::HashSet(.., ref t) => format!("{}", t),
268 }
269 }
270
271 fn ty(&self) -> Ty<'tcx> {
272 match *self {
273 ImplicitHasherType::HashMap(_, ty, ..) | ImplicitHasherType::HashSet(_, ty, ..) => ty,
274 }
275 }
276
277 fn span(&self) -> Span {
278 match *self {
279 ImplicitHasherType::HashMap(span, ..) | ImplicitHasherType::HashSet(span, ..) => span,
280 }
281 }
282 }
283
284 struct ImplicitHasherTypeVisitor<'a, 'tcx> {
285 cx: &'a LateContext<'tcx>,
286 found: Vec<ImplicitHasherType<'tcx>>,
287 }
288
289 impl<'a, 'tcx> ImplicitHasherTypeVisitor<'a, 'tcx> {
290 fn new(cx: &'a LateContext<'tcx>) -> Self {
291 Self { cx, found: vec![] }
292 }
293 }
294
295 impl<'a, 'tcx> Visitor<'tcx> for ImplicitHasherTypeVisitor<'a, 'tcx> {
296 fn visit_ty(&mut self, t: &'tcx hir::Ty<'_>) {
297 if let Some(target) = ImplicitHasherType::new(self.cx, t) {
298 self.found.push(target);
299 }
300
301 walk_ty(self, t);
302 }
303
304 fn visit_infer(&mut self, inf: &'tcx hir::InferArg) {
305 if let Some(target) = ImplicitHasherType::new(self.cx, &inf.to_ty()) {
306 self.found.push(target);
307 }
308
309 walk_inf(self, inf);
310 }
311 }
312
313 /// Looks for default-hasher-dependent constructors like `HashMap::new`.
314 struct ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> {
315 cx: &'a LateContext<'tcx>,
316 maybe_typeck_results: Option<&'tcx TypeckResults<'tcx>>,
317 target: &'b ImplicitHasherType<'tcx>,
318 suggestions: BTreeMap<Span, String>,
319 }
320
321 impl<'a, 'b, 'tcx> ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> {
322 fn new(cx: &'a LateContext<'tcx>, target: &'b ImplicitHasherType<'tcx>) -> Self {
323 Self {
324 cx,
325 maybe_typeck_results: cx.maybe_typeck_results(),
326 target,
327 suggestions: BTreeMap::new(),
328 }
329 }
330 }
331
332 impl<'a, 'b, 'tcx> Visitor<'tcx> for ImplicitHasherConstructorVisitor<'a, 'b, 'tcx> {
333 type NestedFilter = nested_filter::OnlyBodies;
334
335 fn visit_body(&mut self, body: &'tcx Body<'_>) {
336 let old_maybe_typeck_results = self.maybe_typeck_results.replace(self.cx.tcx.typeck_body(body.id()));
337 walk_body(self, body);
338 self.maybe_typeck_results = old_maybe_typeck_results;
339 }
340
341 fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
342 if_chain! {
343 if let ExprKind::Call(fun, args) = e.kind;
344 if let ExprKind::Path(QPath::TypeRelative(ty, method)) = fun.kind;
345 if let TyKind::Path(QPath::Resolved(None, ty_path)) = ty.kind;
346 if let Some(ty_did) = ty_path.res.opt_def_id();
347 then {
348 if self.target.ty() != self.maybe_typeck_results.unwrap().expr_ty(e) {
349 return;
350 }
351
352 if self.cx.tcx.is_diagnostic_item(sym::HashMap, ty_did) {
353 if method.ident.name == sym::new {
354 self.suggestions
355 .insert(e.span, "HashMap::default()".to_string());
356 } else if method.ident.name == sym!(with_capacity) {
357 self.suggestions.insert(
358 e.span,
359 format!(
360 "HashMap::with_capacity_and_hasher({}, Default::default())",
361 snippet(self.cx, args[0].span, "capacity"),
362 ),
363 );
364 }
365 } else if self.cx.tcx.is_diagnostic_item(sym::HashSet, ty_did) {
366 if method.ident.name == sym::new {
367 self.suggestions
368 .insert(e.span, "HashSet::default()".to_string());
369 } else if method.ident.name == sym!(with_capacity) {
370 self.suggestions.insert(
371 e.span,
372 format!(
373 "HashSet::with_capacity_and_hasher({}, Default::default())",
374 snippet(self.cx, args[0].span, "capacity"),
375 ),
376 );
377 }
378 }
379 }
380 }
381
382 walk_expr(self, e);
383 }
384
385 fn nested_visit_map(&mut self) -> Self::Map {
386 self.cx.tcx.hir()
387 }
388 }