1use std::collections::{BTreeSet, HashMap, VecDeque};
15
16const BENIGN: &[&str] = &[
19 "settings", "config", "conf", "options", "opts", "util", "utils", "helper", "helpers", "model",
20 "models", "dto", "entity", "format", "fmt", "parse", "get", "load", "new", "default", "validate",
21 "valid", "render", "view", "build", "builder", "item", "entry", "record", "state", "context",
22 "ctx", "info", "meta", "data", "value", "node", "field", "name", "key", "id", "path", "kind",
23 "type", "status", "check", "init", "setup",
24];
25
26const EFFECTY: &[&str] = &[
29 "fetch", "http", "https", "client", "api", "sync", "request", "req", "download", "upload", "query",
30 "sql", "store", "save", "persist", "connect", "conn", "socket", "send", "recv", "read", "write",
31 "open", "file", "fs", "io", "net", "tcp", "udp", "dns", "url", "host", "port", "cmd", "command",
32 "shell", "process", "proc", "exec", "spawn", "env", "clock", "time", "now", "rand", "random",
33 "log", "logger", "trace", "db",
34];
35
36fn tokenize(name: &str) -> Vec<String> {
38 let mut out = Vec::new();
39 let mut cur = String::new();
40 let mut prev_lower = false;
41 for ch in name.chars() {
42 if ch == '_' || ch == ':' {
43 if !cur.is_empty() {
44 out.push(std::mem::take(&mut cur));
45 }
46 prev_lower = false;
47 continue;
48 }
49 if ch.is_uppercase() && prev_lower && !cur.is_empty() {
51 out.push(std::mem::take(&mut cur));
52 }
53 cur.push(ch.to_ascii_lowercase());
54 prev_lower = ch.is_lowercase() || ch.is_ascii_digit();
55 }
56 if !cur.is_empty() {
57 out.push(cur);
58 }
59 out
60}
61
62fn leaf(qual: &str) -> &str {
64 qual.rsplit("::").next().unwrap_or(qual)
65}
66
67fn module_of(qual: &str) -> &str {
69 match qual.rfind("::") {
70 Some(i) => &qual[..i],
71 None => "",
72 }
73}
74
75fn has_token(name: &str, lexicon: &[&str]) -> Option<String> {
76 tokenize(name).into_iter().find(|t| lexicon.contains(&t.as_str()))
77}
78
79fn salience(effect: &str) -> i64 {
85 match effect {
86 "Net" | "Exec" | "Db" | "Llm" | "Ipc" => 5,
87 "Fs" | "Env" => 3,
88 _ => 0,
89 }
90}
91
92fn hops_factor(hops: usize) -> i64 {
93 match hops {
94 1 => 2,
95 2..=4 => 3,
96 5..=6 => 2,
97 _ => 1, }
99}
100
101pub struct Find {
104 pub func: String,
105 pub effect: String,
106 pub hops: usize,
107 pub source: String,
108 pub source_loc: String,
110 pub benign_token: String,
112 pub score: i64,
113}
114
115fn is_test(qual: &str) -> bool {
121 let mut segs: Vec<&str> = qual.split("::").collect();
122 segs.pop(); segs.iter().any(|s| {
124 let l = s.to_ascii_lowercase();
125 l == "test" || l == "tests" || s.ends_with("Test") || s.ends_with("Tests")
126 })
127}
128
129fn set_has<E: AsRef<str>>(set: &BTreeSet<E>, e: &str) -> bool {
132 set.iter().any(|x| x.as_ref() == e)
133}
134
135fn nearest_source<'a, E: AsRef<str>>(
139 func: &'a str,
140 effect: &str,
141 direct: &'a HashMap<String, BTreeSet<E>>,
142 inferred: &'a HashMap<String, BTreeSet<E>>,
143 calls: &'a HashMap<String, BTreeSet<String>>,
144) -> Option<(usize, &'a str)> {
145 let mut seen: BTreeSet<&str> = BTreeSet::new();
146 let mut q: VecDeque<(&str, usize)> = VecDeque::new();
147 seen.insert(func);
148 q.push_back((func, 0));
149 while let Some((cur, d)) = q.pop_front() {
150 if d >= 1 && direct.get(cur).is_some_and(|s| set_has(s, effect)) {
153 return Some((d, cur));
154 }
155 if let Some(cs) = calls.get(cur) {
156 for c in cs {
157 let cc = c.as_str();
158 if !seen.contains(cc) && inferred.get(cc).is_some_and(|s| set_has(s, effect)) {
159 seen.insert(cc);
160 q.push_back((cc, d + 1));
161 }
162 }
163 }
164 }
165 None
166}
167
168pub fn best_finds<E: AsRef<str>>(
177 inferred: &HashMap<String, BTreeSet<E>>,
178 direct: &HashMap<String, BTreeSet<E>>,
179 calls: &HashMap<String, BTreeSet<String>>,
180 loc: &HashMap<String, String>,
181 top_n: usize,
182) -> Vec<Find> {
183 let mut quals: Vec<&String> = inferred.keys().collect();
186 quals.sort();
187
188 let mut cands: Vec<Find> = Vec::new();
189
190 for f in quals {
191 let inf = &inferred[f];
192 if is_test(f) {
193 continue;
194 }
195 let f_leaf = leaf(f);
196 let f_mod = module_of(f);
197 if has_token(f_leaf, EFFECTY).is_some() || has_token(f_mod, EFFECTY).is_some() {
199 continue;
200 }
201 let empty = BTreeSet::new();
202 let dir = direct.get(f).unwrap_or(&empty);
203 let mut effects: Vec<String> = inf
206 .iter()
207 .map(|e| e.as_ref().to_string())
208 .filter(|e| e != "Unknown" && !set_has(dir, e))
209 .collect();
210 effects.sort();
211 for e in effects {
212 let sal = salience(&e);
213 if sal == 0 {
214 continue;
215 }
216 let Some((hops, s)) = nearest_source(f, &e, direct, inferred, calls) else {
217 continue; };
219 let benign = has_token(f_leaf, BENIGN);
220 let benignity = if benign.is_some() { 3 } else { 1 };
221 let crossing = if module_of(s) != f_mod { 2 } else { 1 };
222 let score = sal * benignity * hops_factor(hops) * crossing;
223 if score == 0 {
224 continue;
225 }
226 cands.push(Find {
227 func: f.clone(),
228 effect: e,
229 hops,
230 source: s.to_string(),
231 source_loc: loc.get(s).cloned().unwrap_or_default(),
232 benign_token: benign.unwrap_or_default(),
233 score,
234 });
235 }
236 }
237
238 cands.sort_by(|a, b| {
242 b.score
243 .cmp(&a.score)
244 .then_with(|| a.hops.cmp(&b.hops))
245 .then_with(|| a.func.cmp(&b.func))
246 });
247
248 let mut seen_fns: BTreeSet<String> = BTreeSet::new();
251 let mut out: Vec<Find> = Vec::new();
252 for c in cands {
253 if out.len() >= top_n {
254 break;
255 }
256 if seen_fns.insert(c.func.clone()) {
257 out.push(c);
258 }
259 }
260 out
261}
262
263pub fn any_effectful<E: AsRef<str>>(inferred: &HashMap<String, BTreeSet<E>>) -> bool {
267 inferred.values().any(|s| s.iter().any(|e| e.as_ref() != "Unknown"))
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn set(items: &[&'static str]) -> BTreeSet<&'static str> {
275 items.iter().copied().collect()
276 }
277 fn sset(items: &[&str]) -> BTreeSet<String> {
278 items.iter().map(|s| s.to_string()).collect()
279 }
280
281 #[test]
282 fn tokenize_splits_all_boundaries() {
283 assert_eq!(tokenize("settings::Settings::needsUpdate"), vec!["settings", "settings", "needs", "update"]);
284 assert_eq!(tokenize("api_client::latest_version"), vec!["api", "client", "latest", "version"]);
285 }
286
287 #[test]
288 fn benign_deep_inherited_beats_shallow_effecty() {
289 let mut direct: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
295 let mut inferred: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
296 let mut calls: HashMap<String, BTreeSet<String>> = HashMap::new();
297 let loc: HashMap<String, String> = HashMap::new();
298
299 direct.insert("net_layer::do_send".into(), set(&["Net"]));
300 inferred.insert("net_layer::do_send".into(), set(&["Net"]));
301
302 inferred.insert("core::sync_state".into(), set(&["Net"]));
303 calls.insert("core::sync_state".into(), sset(&["net_layer::do_send"]));
304
305 inferred.insert("core::refresh".into(), set(&["Net"]));
306 calls.insert("core::refresh".into(), sset(&["core::sync_state"]));
307
308 inferred.insert("settings::Settings::load".into(), set(&["Net"]));
310 calls.insert("settings::Settings::load".into(), sset(&["core::refresh"]));
311
312 inferred.insert("api::fetch".into(), set(&["Net"]));
314 calls.insert("api::fetch".into(), sset(&["net_layer::do_send"]));
315
316 assert!(any_effectful(&inferred));
317 let got = best_finds(&inferred, &direct, &calls, &loc, 1);
318 assert_eq!(got.len(), 1);
319 assert_eq!(got[0].func, "settings::Settings::load");
320 assert_eq!(got[0].effect, "Net");
321 assert_eq!(got[0].hops, 3);
322 assert_eq!(got[0].source, "net_layer::do_send");
323 assert_eq!(got[0].benign_token, "load");
324 }
325
326 #[test]
327 fn dedup_one_row_per_function_top_n() {
328 let mut direct: HashMap<String, BTreeSet<String>> = HashMap::new();
333 let mut inferred: HashMap<String, BTreeSet<String>> = HashMap::new();
334 let mut calls: HashMap<String, BTreeSet<String>> = HashMap::new();
335 let loc: HashMap<String, String> = HashMap::new();
336
337 direct.insert("net_layer::do_send".into(), sset(&["Net"]));
338 inferred.insert("net_layer::do_send".into(), sset(&["Net"]));
339
340 inferred.insert("core::sync_state".into(), sset(&["Net"]));
343 calls.insert("core::sync_state".into(), sset(&["net_layer::do_send"]));
344 inferred.insert("core::download_step".into(), sset(&["Net"]));
345 calls.insert("core::download_step".into(), sset(&["core::sync_state"]));
346 inferred.insert("settings::Settings::load".into(), sset(&["Net"]));
347 calls.insert("settings::Settings::load".into(), sset(&["core::download_step"]));
348
349 inferred.insert("model::render".into(), sset(&["Net"]));
351 calls.insert("model::render".into(), sset(&["net_layer::do_send"]));
352
353 let got = best_finds(&inferred, &direct, &calls, &loc, 10);
354 assert_eq!(got.len(), 2, "two distinct benign functions, one row each");
355 assert_eq!(got[0].func, "settings::Settings::load"); assert_eq!(got[1].func, "model::render");
357 assert_eq!(best_finds(&inferred, &direct, &calls, &loc, 1).len(), 1);
359 assert_eq!(got.iter().map(|f| &f.func).collect::<BTreeSet<_>>().len(), got.len());
361 }
362
363 #[test]
364 fn fallback_when_nothing_qualifies() {
365 let mut direct: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
368 let mut inferred: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
369 let calls: HashMap<String, BTreeSet<String>> = HashMap::new();
370 let loc: HashMap<String, String> = HashMap::new();
371 direct.insert("net::client::send".into(), set(&["Net"]));
372 inferred.insert("net::client::send".into(), set(&["Net"]));
373
374 assert!(best_finds(&inferred, &direct, &calls, &loc, 1).is_empty());
375 assert!(any_effectful(&inferred), "effectful, so the caller emits the honest fallback");
376 }
377
378 #[test]
379 fn nothing_when_no_effects() {
380 let direct: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
382 let mut inferred: HashMap<String, BTreeSet<&'static str>> = HashMap::new();
383 let calls: HashMap<String, BTreeSet<String>> = HashMap::new();
384 let loc: HashMap<String, String> = HashMap::new();
385 inferred.insert("util::parse".into(), set(&["Unknown"]));
386 assert!(best_finds(&inferred, &direct, &calls, &loc, 1).is_empty());
387 assert!(!any_effectful(&inferred));
388 }
389}