1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
13
14use std::collections::VecDeque;
15
16use crate::effects::EffectStore;
17use crate::error::Result;
18use crate::index::IndexStore;
19use crate::schema::{EffectCategory, EffectDecl, TransitiveEffect};
20
21pub fn propagate_transitive<I: IndexStore, E: EffectStore>(
25 index: &I,
26 effects: &E,
27 ref_name: &str,
28 symbol_ids: &[String],
29) -> Result<usize> {
30 let mut callees_of: HashMap<String, Vec<String>> = HashMap::new();
33 let mut decls: HashMap<String, EffectDecl> = HashMap::new();
34 let mut queue: VecDeque<String> = symbol_ids.iter().cloned().collect();
35 while let Some(sym) = queue.pop_front() {
36 if callees_of.contains_key(&sym) {
37 continue;
38 }
39 if let Some(decl) = effects.get_effects(ref_name, &sym)? {
40 decls.insert(sym.clone(), decl);
41 }
42 let callees = index.get_callees(ref_name, &sym)?;
43 queue.extend(callees.iter().cloned());
44 callees_of.insert(sym, callees);
45 }
46
47 let mut updated: usize = 0;
48 for (sym, new_transitive) in transitive_effects(&callees_of, &decls, symbol_ids) {
49 let Some(mut decl) = decls.remove(&sym) else {
52 continue;
53 };
54 if !transitive_eq(&decl.transitive, &new_transitive) {
55 decl.transitive = new_transitive;
56 effects.put_effects(ref_name, &sym, &decl, "asd-transitive")?;
60 updated += 1;
61 }
62 }
63
64 Ok(updated)
65}
66
67pub fn declared_effect_blast_radius(
83 decls: &[(String, EffectDecl)],
84) -> HashMap<EffectCategory, Vec<(String, usize)>> {
85 let mut declarers: HashMap<EffectCategory, Vec<&str>> = HashMap::new();
87 for (symbol_id, decl) in decls {
88 for e in &decl.declared {
89 declarers
90 .entry(e.effect.clone())
91 .or_default()
92 .push(symbol_id.as_str());
93 }
94 }
95
96 let mut inherits_via: HashMap<&EffectCategory, HashMap<&str, Vec<&str>>> = HashMap::new();
99 for (symbol_id, decl) in decls {
100 for t in &decl.transitive {
101 let edges = inherits_via.entry(&t.effect).or_default();
102 for child in &t.via {
103 edges
104 .entry(child.as_str())
105 .or_default()
106 .push(symbol_id.as_str());
107 }
108 }
109 }
110
111 let mut out: HashMap<EffectCategory, Vec<(String, usize)>> = HashMap::new();
112 for (cat, cat_declarers) in declarers {
113 let edges = inherits_via.get(&cat);
114 let mut ranked: Vec<(String, usize)> = cat_declarers
115 .into_iter()
116 .map(|declarer| {
117 let mut visited: HashSet<&str> = HashSet::new();
119 let mut queue: VecDeque<&str> = VecDeque::new();
120 visited.insert(declarer);
121 queue.push_back(declarer);
122 while let Some(cur) = queue.pop_front() {
123 let Some(parents) = edges.and_then(|e| e.get(cur)) else {
124 continue;
125 };
126 for parent in parents {
127 if visited.insert(parent) {
128 queue.push_back(parent);
129 }
130 }
131 }
132 (declarer.to_string(), visited.len() - 1)
134 })
135 .collect();
136 ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
137 out.insert(cat, ranked);
138 }
139 out
140}
141
142pub fn transitive_effects(
153 callees_of: &HashMap<String, Vec<String>>,
154 decls: &HashMap<String, EffectDecl>,
155 symbol_ids: &[String],
156) -> Vec<(String, Vec<TransitiveEffect>)> {
157 let callees = |sym: &str| callees_of.get(sym).map_or(&[][..], Vec::as_slice);
158 let (components, component_of) = call_graph_components(symbol_ids, callees_of);
159
160 let mut reach: Vec<BTreeSet<EffectCategory>> = Vec::with_capacity(components.len());
163 for (id, members) in components.iter().enumerate() {
164 let mut reached = BTreeSet::new();
165 for &member in members {
166 if let Some(decl) = decls.get(member) {
167 reached.extend(decl.declared.iter().map(|e| e.effect.clone()));
168 }
169 for callee in callees(member) {
170 let other = component_of[callee.as_str()];
171 if other != id {
172 reached.extend(reach[other].iter().cloned());
173 }
174 }
175 }
176 reach.push(reached);
177 }
178
179 symbol_ids
180 .iter()
181 .map(|sym| {
182 let own: HashSet<&EffectCategory> = decls
183 .get(sym)
184 .map(|d| d.declared.iter().map(|e| &e.effect).collect())
185 .unwrap_or_default();
186 let mut via: BTreeMap<EffectCategory, BTreeSet<String>> = BTreeMap::new();
187 for callee in callees(sym) {
188 if callee == sym {
190 continue;
191 }
192 for category in &reach[component_of[callee.as_str()]] {
193 if !own.contains(category) {
194 via.entry(category.clone())
195 .or_default()
196 .insert(callee.clone());
197 }
198 }
199 }
200 let mut transitive: Vec<TransitiveEffect> = via
201 .into_iter()
202 .map(|(effect, via)| TransitiveEffect {
203 effect,
204 via: via.into_iter().collect(),
205 qualifiers: serde_json::Value::Null,
206 })
207 .collect();
208 transitive.sort_by(|a, b| {
209 a.effect
210 .as_str()
211 .cmp(b.effect.as_str())
212 .then_with(|| a.via.cmp(&b.via))
213 });
214 (sym.clone(), transitive)
215 })
216 .collect()
217}
218
219fn call_graph_components<'a>(
223 roots: &'a [String],
224 callees_of: &'a HashMap<String, Vec<String>>,
225) -> (Vec<Vec<&'a str>>, HashMap<&'a str, usize>) {
226 #[derive(Default)]
227 struct Search<'a> {
228 order: HashMap<&'a str, usize>,
229 low: HashMap<&'a str, usize>,
230 stack: Vec<&'a str>,
231 on_stack: HashSet<&'a str>,
232 work: Vec<(&'a str, usize)>,
234 }
235 impl<'a> Search<'a> {
236 fn visit(&mut self, sym: &'a str) {
237 let n = self.order.len();
238 self.order.insert(sym, n);
239 self.low.insert(sym, n);
240 self.stack.push(sym);
241 self.on_stack.insert(sym);
242 self.work.push((sym, 0));
243 }
244 fn lower(&mut self, sym: &'a str, to: usize) {
245 let low = self.low.get_mut(sym).expect("visited");
246 *low = (*low).min(to);
247 }
248 }
249
250 let mut search = Search::default();
251 let mut components: Vec<Vec<&str>> = Vec::new();
252 let mut component_of: HashMap<&str, usize> = HashMap::new();
253
254 for root in roots {
255 if search.order.contains_key(root.as_str()) {
256 continue;
257 }
258 search.visit(root.as_str());
259 while let Some(&(sym, next)) = search.work.last() {
260 let callees: &'a [String] = callees_of.get(sym).map_or(&[], Vec::as_slice);
261 if let Some(callee) = callees.get(next) {
262 search.work.last_mut().expect("non-empty").1 += 1;
263 let callee = callee.as_str();
264 if !search.order.contains_key(callee) {
265 search.visit(callee);
266 } else if search.on_stack.contains(callee) {
267 let reached = search.order[callee];
268 search.lower(sym, reached);
269 }
270 continue;
271 }
272 search.work.pop();
273 if let Some(&(caller, _)) = search.work.last() {
274 let reached = search.low[sym];
275 search.lower(caller, reached);
276 }
277 if search.low[sym] == search.order[sym] {
278 let id = components.len();
279 let mut members = Vec::new();
280 loop {
281 let member = search.stack.pop().expect("on the stack");
282 search.on_stack.remove(member);
283 component_of.insert(member, id);
284 members.push(member);
285 if member == sym {
286 break;
287 }
288 }
289 components.push(members);
290 }
291 }
292 }
293 (components, component_of)
294}
295
296pub(crate) fn transitive_eq(a: &[TransitiveEffect], b: &[TransitiveEffect]) -> bool {
300 if a.len() != b.len() {
301 return false;
302 }
303 let to_key = |t: &TransitiveEffect| {
304 let mut via = t.via.clone();
305 via.sort();
306 (t.effect.clone(), via)
307 };
308 let mut a_keys: Vec<_> = a.iter().map(to_key).collect();
309 let mut b_keys: Vec<_> = b.iter().map(to_key).collect();
310 a_keys.sort();
311 b_keys.sort();
312 a_keys == b_keys
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::schema::Effect;
319
320 fn graph(edges: &[(&str, &str)]) -> HashMap<String, Vec<String>> {
321 let mut g: HashMap<String, Vec<String>> = HashMap::new();
322 for (from, to) in edges {
323 g.entry(from.to_string()).or_default().push(to.to_string());
324 }
325 g
326 }
327
328 fn decls(declared: &[(&str, &[EffectCategory])]) -> HashMap<String, EffectDecl> {
329 declared
330 .iter()
331 .map(|(sym, cats)| {
332 let decl = EffectDecl {
333 symbol_id: sym.to_string(),
334 declared: cats.iter().cloned().map(Effect::new).collect(),
335 transitive: Vec::new(),
336 verification: None,
337 confidence: None,
338 runtime: None,
339 matched_policy: None,
340 };
341 (sym.to_string(), decl)
342 })
343 .collect()
344 }
345
346 fn summary(
348 out: Vec<(String, Vec<TransitiveEffect>)>,
349 ) -> BTreeMap<String, Vec<(String, Vec<String>)>> {
350 out.into_iter()
351 .map(|(sym, t)| {
352 let t = t
353 .into_iter()
354 .map(|t| (t.effect.as_str().to_string(), t.via))
355 .collect();
356 (sym, t)
357 })
358 .collect()
359 }
360
361 fn ids(order: &[&str]) -> Vec<String> {
362 order.iter().map(|s| s.to_string()).collect()
363 }
364
365 #[test]
369 fn every_member_of_a_cycle_reaches_what_the_cycle_reaches() {
370 let g = graph(&[("a", "b"), ("b", "a"), ("b", "t")]);
371 let d = decls(&[("a", &[]), ("b", &[]), ("t", &[EffectCategory::IoNetOut])]);
372 let expected = BTreeMap::from([
373 (
374 "a".to_string(),
375 vec![("io.net.out".to_string(), ids(&["b"]))],
376 ),
377 (
378 "b".to_string(),
379 vec![("io.net.out".to_string(), ids(&["a", "t"]))],
380 ),
381 ("t".to_string(), vec![]),
382 ]);
383 for order in [
384 ["a", "b", "t"],
385 ["a", "t", "b"],
386 ["b", "a", "t"],
387 ["b", "t", "a"],
388 ["t", "a", "b"],
389 ["t", "b", "a"],
390 ] {
391 assert_eq!(
392 summary(transitive_effects(&g, &d, &ids(&order))),
393 expected,
394 "visited in order {order:?}"
395 );
396 }
397 }
398
399 #[test]
400 fn recursion_adds_nothing_via_itself() {
401 let g = graph(&[("s", "s"), ("s", "t")]);
402 let d = decls(&[("s", &[]), ("t", &[EffectCategory::IoNetOut])]);
403 assert_eq!(
404 summary(transitive_effects(&g, &d, &ids(&["s"])))["s"],
405 vec![("io.net.out".to_string(), ids(&["t"]))]
406 );
407 }
408
409 #[test]
410 fn a_symbols_own_effects_are_not_transitive() {
411 let g = graph(&[("a", "b")]);
412 let d = decls(&[
413 ("a", &[EffectCategory::IoNetOut]),
414 ("b", &[EffectCategory::IoNetOut, EffectCategory::IoFsRead]),
415 ]);
416 assert_eq!(
417 summary(transitive_effects(&g, &d, &ids(&["a"])))["a"],
418 vec![("io.fs.read".to_string(), ids(&["b"]))]
419 );
420 }
421
422 #[test]
424 fn a_deep_call_chain_does_not_overflow_the_stack() {
425 const DEPTH: usize = 200_000;
426 let names: Vec<String> = (0..DEPTH).map(|i| format!("n{i}")).collect();
427 let mut g: HashMap<String, Vec<String>> = HashMap::new();
428 for pair in names.windows(2) {
429 g.insert(pair[0].clone(), vec![pair[1].clone()]);
430 }
431 let d = decls(&[(names[DEPTH - 1].as_str(), &[EffectCategory::IoNetOut])]);
432 let out = transitive_effects(&g, &d, &names[..1]);
433 assert_eq!(
434 summary(out)["n0"],
435 vec![("io.net.out".to_string(), ids(&["n1"]))]
436 );
437 }
438}