wowlab_tidy/languages/rust/rules/performance/
default_hasher.rs1use ra_ap_syntax::{ast, ast::HasGenericArgs};
2
3use super::super::support::path_names;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8 Example {
9 label: "HashMap<K, V> field with default hasher",
10 code: "struct S { m: HashMap<u32, u32> }",
11 pass: false,
12 },
13 Example {
14 label: "HashSet<T> parameter with default hasher",
15 code: "fn f(s: HashSet<u32>) { drop(s); }",
16 pass: false,
17 },
18 Example {
19 label: "std HashMap::new constructor",
20 code: "fn f() { let mut m = std::collections::HashMap::new(); m.insert(1, 1); }",
21 pass: false,
22 },
23 Example {
24 label: "HashSet::with_capacity constructor",
25 code: "fn f() { let s: std::collections::HashSet<u64> = HashSet::with_capacity(8); drop(s); }",
26 pass: false,
27 },
28 Example {
29 label: "map with explicit hasher type param",
30 code: "struct S { m: HashMap<u32, u32, FxBuildHasher> }",
31 pass: true,
32 },
33 Example {
34 label: "set with explicit hasher type param",
35 code: "struct S { s: HashSet<u32, FxBuildHasher> }",
36 pass: true,
37 },
38 Example {
39 label: "fast-hasher crate alias",
40 code: "fn f(m: foldhash::HashMap<u32, u32>) { drop(m); }",
41 pass: true,
42 },
43 Example {
44 label: "turbofish constructor with explicit hasher",
45 code: "fn f() { let _m = HashMap::<u32, u32, FxBuildHasher>::new(); }",
46 pass: true,
47 },
48 Example {
49 label: "BTreeMap does not hash",
50 code: "struct S { m: std::collections::BTreeMap<u32, u32> }",
51 pass: true,
52 },
53 Example {
54 label: "default hasher in test module",
55 code: "#[cfg(test)]\nmod tests {\n fn t() { let _m: HashMap<u32, u32> = HashMap::new(); }\n}",
56 pass: true,
57 },
58];
59
60crate::ast_rule!(
61 default_hasher,
62 "Flag std `HashMap`/`HashSet` types and constructors that use the default SipHash hasher.",
63 "SipHash buys DoS resistance that trusted internal keys do not need — a fast hasher (foldhash/FxHash) is significantly quicker.",
64);
65
66fn check_default_hasher(ctx: &AstCtx<'_>) -> Vec<Violation> {
67 let type_violations = ctx
68 .nodes::<ast::PathType>()
69 .filter(|path| !ctx.is_in_test(path))
70 .filter_map(|path_type| {
71 let path = path_type.path()?;
72
73 flags_type_path(&path).then(|| ctx.violation(&path_type, MSG_TYPE))
74 });
75 let constructor_violations = ctx
76 .nodes::<ast::CallExpr>()
77 .filter(|call| !ctx.is_in_test(call))
78 .filter_map(|call| {
79 let ast::Expr::PathExpr(function) = call.expr()? else {
80 return None;
81 };
82 let path = function.path()?;
83
84 flags_ctor_path(&path).then(|| ctx.violation(&function, MSG_CTOR))
85 });
86
87 type_violations.chain(constructor_violations).collect()
88}
89
90const FAST_HASH_MARKERS: &[&str] = &["ahash", "foldhash", "fxhash", "hashbrown", "rustc_hash"];
91const MAP_KEY_VALUE_ARGS: usize = 2;
92const SET_VALUE_ARGS: usize = 1;
93
94const MSG_TYPE: &str = "default SipHash hasher — trusted internal keys should use a fast hasher (foldhash/FxHash) or an explicit hasher type param";
95const MSG_CTOR: &str = "constructor builds a default-SipHash collection — use a fast hasher (foldhash/FxHash) for trusted internal keys";
96
97fn default_hasher_args(segment: &ast::PathSegment) -> Option<usize> {
98 let name_ref = segment.name_ref()?;
99 let name = name_ref.text();
100
101 if name == "HashMap" {
102 Some(MAP_KEY_VALUE_ARGS)
103 } else if name == "HashSet" {
104 Some(SET_VALUE_ARGS)
105 } else {
106 None
107 }
108}
109
110fn has_fast_marker(path: &ast::Path) -> bool {
111 path_names(path)
112 .iter()
113 .any(|name| FAST_HASH_MARKERS.contains(&name.as_str()))
114}
115
116fn count_type_args(segment: &ast::PathSegment) -> Option<usize> {
117 Some(
118 segment
119 .generic_arg_list()?
120 .generic_args()
121 .filter(|arg| matches!(arg, ast::GenericArg::TypeArg(_)))
122 .count(),
123 )
124}
125
126fn flags_type_path(path: &ast::Path) -> bool {
127 let Some(last) = path.segment() else {
128 return false;
129 };
130 let Some(expected) = default_hasher_args(&last) else {
131 return false;
132 };
133
134 !has_fast_marker(path) && count_type_args(&last) == Some(expected)
135}
136
137fn flags_ctor_path(path: &ast::Path) -> bool {
138 let Some(last) = path.segment() else {
139 return false;
140 };
141 let Some(last_name) = last.name_ref().map(|name| name.text().to_string()) else {
142 return false;
143 };
144
145 if last_name != "new" && last_name != "with_capacity" {
146 return false;
147 }
148
149 let Some(ty_seg) = path.qualifier().and_then(|qualifier| qualifier.segment()) else {
150 return false;
151 };
152 let Some(expected) = default_hasher_args(&ty_seg) else {
153 return false;
154 };
155
156 if has_fast_marker(path) {
157 return false;
158 }
159
160 count_type_args(&ty_seg).is_none_or(|n| n <= expected)
161}
162
163crate::tidy_ast_test!(check_default_hasher, {
164 crate::example_tests!(EXAMPLES, check_default_hasher);
165});