Skip to main content

wowlab_tidy/languages/rust/rules/performance/
default_hasher.rs

1use 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});