use std::collections::{HashMap, HashSet};
use anyhow::Result;
use sinter_core::{CorpusScope, Node, NodeId, SymbolKind};
use sinter_store::{EdgeFilter, Reached, Store};
pub const HUB_FAN_IN: usize = 100;
pub struct Rules<'a> {
pub keep_tests: bool,
pub drop_file_rows: bool,
pub hub_fan_in: Option<usize>,
pub is_test: &'a dyn Fn(&Node) -> bool,
}
#[derive(Default)]
pub struct Pruned {
pub rows: Vec<Reached>,
pub tests: usize,
pub hubs: Vec<(String, usize)>,
}
pub fn prune(reached: Vec<Reached>, parent: fn(&Reached) -> &NodeId, rules: &Rules) -> Pruned {
let mut fan_in: HashMap<String, usize> = HashMap::new();
for row in &reached {
*fan_in.entry(parent(row).as_str().to_string()).or_default() += 1;
}
let is_hub = |id: &str| {
rules
.hub_fan_in
.is_some_and(|max| fan_in.get(id) > Some(&max))
};
let mut cut: HashSet<String> = HashSet::new();
let mut out = Pruned::default();
for row in reached {
if cut.contains(parent(&row).as_str()) {
cut.insert(row.node.id.as_str().to_string());
continue;
}
let test = (rules.is_test)(&row.node);
out.tests += usize::from(test);
let noise_file_row = rules.drop_file_rows
&& row.node.kind == SymbolKind::File
&& matches!(
row.via.relation,
sinter_core::Relation::Imports | sinter_core::Relation::Uses
);
if (test && !rules.keep_tests) || noise_file_row {
cut.insert(row.node.id.as_str().to_string());
continue;
}
if is_hub(row.node.id.as_str()) {
let id = row.node.id.as_str();
out.hubs
.push((sinter_resolve::qualified_of(id).to_string(), fan_in[id]));
cut.insert(id.to_string());
}
out.rows.push(row);
}
out.rows.sort_by_key(|row| (rules.is_test)(&row.node));
out
}
pub fn is_test_scope(scope: CorpusScope) -> bool {
scope == CorpusScope::Test
}
pub fn filter_excluded(
store: &Store,
id: &NodeId,
filter: &EdgeFilter,
max_depth: usize,
forward: bool,
) -> Result<bool> {
if max_depth == 0 {
return Ok(true);
}
if filter.relations.is_none() && filter.evidence.is_none() && filter.min_confidence.is_none() {
return Ok(false);
}
let open = EdgeFilter {
scopes: filter.scopes.clone(),
..EdgeFilter::default()
};
let reached = if forward {
store.dependencies(id, &open, 1)?
} else {
store.dependents(id, &open, 1)?
};
Ok(!reached.is_empty())
}
#[cfg(test)]
mod tests {
use sinter_core::{Confidence, Edge, Evidence, Node, NodeId, Relation, Span, SymbolKind};
use sinter_store::Reached;
use super::{Pruned, Rules, prune};
fn node(id: &str, kind: SymbolKind) -> Node {
Node {
id: NodeId::new(id),
kind,
name: id.rsplit('#').next().unwrap_or(id).to_string(),
file: id.split('#').next().unwrap_or(id).to_string(),
span: Span { start: 0, end: 1 },
signature: String::new(),
doc: None,
}
}
fn dependent(id: &str, from: &str, depth: usize, kind: SymbolKind) -> Reached {
Reached {
node: node(id, kind),
depth,
via: Edge {
src: NodeId::new(id),
dst: NodeId::new(from),
relation: Relation::Calls,
evidence: Evidence::Scope,
confidence: Confidence::Inferred,
site: None,
extra_sites: Vec::new(),
sites_total: 0,
},
}
}
fn parent(r: &Reached) -> &NodeId {
&r.via.dst
}
fn ids(pruned: &Pruned) -> Vec<&str> {
pruned.rows.iter().map(|r| r.node.id.as_str()).collect()
}
#[test]
fn a_hidden_test_row_takes_its_subtree_and_is_counted() {
let rows = vec![
dependent("a.rs#x", "seed", 1, SymbolKind::Function),
dependent("tests/t.rs#test_x", "seed", 1, SymbolKind::Function),
dependent(
"tests/t.rs#helper",
"tests/t.rs#test_x",
2,
SymbolKind::Function,
),
dependent("a.rs#y", "a.rs#x", 2, SymbolKind::Function),
];
let is_test = |n: &Node| n.file.starts_with("tests/");
let pruned = prune(
rows,
parent,
&Rules {
keep_tests: false,
drop_file_rows: false,
hub_fan_in: None,
is_test: &is_test,
},
);
assert_eq!(ids(&pruned), vec!["a.rs#x", "a.rs#y"]);
assert_eq!(pruned.tests, 1);
}
#[test]
fn kept_test_rows_rank_after_production_rows() {
let rows = vec![
dependent("tests/t.rs#test_x", "seed", 1, SymbolKind::Function),
dependent("a.rs#x", "seed", 1, SymbolKind::Function),
];
let is_test = |n: &Node| n.file.starts_with("tests/");
let pruned = prune(
rows,
parent,
&Rules {
keep_tests: true,
drop_file_rows: false,
hub_fan_in: None,
is_test: &is_test,
},
);
assert_eq!(ids(&pruned), vec!["a.rs#x", "tests/t.rs#test_x"]);
assert_eq!(pruned.tests, 1);
}
#[test]
fn file_rows_leave_under_a_relations_filter() {
let mut import_row = dependent("a.rs", "seed", 1, SymbolKind::File);
import_row.via.relation = Relation::Imports;
let rows = vec![
import_row,
dependent("b.rs#imports_a", "a.rs", 2, SymbolKind::Function),
dependent("a.rs#x", "seed", 1, SymbolKind::Function),
];
let never = |_: &Node| false;
let pruned = prune(
rows,
parent,
&Rules {
keep_tests: true,
drop_file_rows: true,
hub_fan_in: None,
is_test: &never,
},
);
assert_eq!(ids(&pruned), vec!["a.rs#x"]);
}
#[test]
fn a_file_that_calls_the_seed_is_a_caller_not_noise() {
let rows = vec![dependent("t.test.ts", "seed", 1, SymbolKind::File)];
let never = |_: &Node| false;
let pruned = prune(
rows,
parent,
&Rules {
keep_tests: true,
drop_file_rows: true,
hub_fan_in: None,
is_test: &never,
},
);
assert_eq!(ids(&pruned), vec!["t.test.ts"]);
}
#[test]
fn the_answer_stops_at_a_hub_and_names_it() {
let mut rows = vec![dependent("hub.rs#hub", "seed", 1, SymbolKind::Function)];
for i in 0..3 {
rows.push(dependent(
&format!("c.rs#c{i}"),
"hub.rs#hub",
2,
SymbolKind::Function,
));
}
rows.push(dependent("d.rs#d", "c.rs#c0", 3, SymbolKind::Function));
let never = |_: &Node| false;
let pruned = prune(
rows,
parent,
&Rules {
keep_tests: true,
drop_file_rows: false,
hub_fan_in: Some(2),
is_test: &never,
},
);
assert_eq!(ids(&pruned), vec!["hub.rs#hub"]);
assert_eq!(pruned.hubs, vec![("hub".to_string(), 3)]);
}
}