use std::collections::BTreeMap;
use std::path::Path;
use anyhow::Result;
use sinter_core::{Edge, Evidence, Relation};
use sinter_resolve::qualified_of;
use crate::lookup::{ensure_snapshot, open_store, unique_symbol};
use crate::render::{ellipsize, line_of, location, node_json, site_json, site_location};
const EXEMPLARS: usize = 8;
fn names(edges: &[&Edge], end: fn(&Edge) -> &str) -> String {
let shown: Vec<&str> = edges
.iter()
.take(EXEMPLARS)
.map(|e| {
let q = qualified_of(end(e));
q.rsplit("::").next().unwrap_or(q)
})
.collect();
let mut out = shown.join(", ");
if edges.len() > EXEMPLARS {
out.push_str(&format!(", … (+{})", edges.len() - EXEMPLARS));
}
out
}
fn evidence_tally(edges: &[&Edge]) -> String {
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for e in edges {
*counts.entry(e.evidence.as_str()).or_default() += 1;
}
counts
.iter()
.map(|(k, v)| format!("{k} {v}"))
.collect::<Vec<_>>()
.join(" · ")
}
pub fn run(repo: &Path, symbol: &str, json: bool, if_snapshot: Option<&str>) -> Result<bool> {
let repo = repo.canonicalize()?;
let store = open_store(&repo)?;
let snapshot = ensure_snapshot(&store, if_snapshot)?;
let node = unique_symbol(&store, symbol)?;
let scope = store.file_scope(&node.file)?;
if json {
let edge_json = |e: &Edge, other: &str| {
serde_json::json!({
"symbol": qualified_of(other),
"relation": e.relation.as_str(),
"evidence": e.evidence.as_str(),
"site": site_json(&repo, e),
})
};
let out: Vec<serde_json::Value> = store
.out_edges(&node.id)?
.iter()
.map(|e| edge_json(e, e.dst.as_str()))
.collect();
let inn: Vec<serde_json::Value> = store
.in_edges(&node.id)?
.iter()
.map(|e| edge_json(e, e.src.as_str()))
.collect();
let mut symbol_json = node_json(&node);
symbol_json["scope"] = serde_json::json!(scope.as_str());
crate::agent_protocol::write_json(&serde_json::json!({
"symbol": symbol_json,
"snapshot": snapshot,
"outgoing": out,
"incoming": inn,
}))?;
return Ok(true);
}
let line = line_of(&repo, &node.file, node.span.start);
println!(
"{} {} {} ({}..{}) [{scope}]",
node.kind.as_str(),
qualified_of(node.id.as_str()),
location(&repo, &node.file, line),
node.span.start,
node.span.end,
);
if let Some(doc) = &node.doc {
for l in doc.lines().take(3) {
println!(" /// {l}");
}
}
if !node.signature.is_empty() {
println!(" {}", ellipsize(&node.signature, 110));
}
println!();
let out = store.out_edges(&node.id)?;
let inn = store.in_edges(&node.id)?;
let group =
|rel: Relation| -> Vec<&Edge> { out.iter().filter(|e| e.relation == rel).collect() };
let contains = group(Relation::Contains);
if !contains.is_empty() {
println!(
"contains ({}) {}",
contains.len(),
names(&contains, |e| e.dst.as_str())
);
}
let extends = group(Relation::Extends);
if !extends.is_empty() {
println!(
"extends {} [{}]",
names(&extends, |e| e.dst.as_str()),
evidence_tally(&extends)
);
}
let imports = group(Relation::Imports);
if !imports.is_empty() {
println!(
"imports ({}) {} [{}]",
imports.len(),
names(&imports, |e| e.dst.as_str()),
evidence_tally(&imports)
);
}
let implements = group(Relation::Implements);
if !implements.is_empty() {
println!(
"implements {} [{}]",
names(&implements, |e| e.dst.as_str()),
evidence_tally(&implements)
);
}
let implementors: Vec<&Edge> = inn
.iter()
.filter(|e| e.relation == Relation::Implements)
.collect();
if !implementors.is_empty() {
println!(
"implemented by ({}) {} [{}]",
implementors.len(),
names(&implementors, |e| e.src.as_str()),
evidence_tally(&implementors)
);
}
let dependents: Vec<&Edge> = inn
.iter()
.filter(|e| !matches!(e.relation, Relation::Contains | Relation::Implements))
.collect();
if !dependents.is_empty() {
let mut per_file: BTreeMap<&str, (usize, Option<u64>)> = BTreeMap::new();
for e in &dependents {
let file = e
.src
.as_str()
.split_once('#')
.map_or(e.src.as_str(), |(f, _)| f);
let entry = per_file.entry(file).or_default();
entry.0 += 1;
if let Some(span) = e.site {
entry.1 = Some(entry.1.map_or(span.start, |s| s.min(span.start)));
}
}
println!(
"used by ({} files, {} edges)",
per_file.len(),
dependents.len()
);
let mut rows: Vec<(&str, (usize, Option<u64>))> = per_file.into_iter().collect();
rows.sort_by(|a, b| b.1.0.cmp(&a.1.0).then_with(|| a.0.cmp(b.0)));
for (file, (count, site)) in rows.iter().take(EXEMPLARS) {
let line = site.and_then(|byte| line_of(&repo, file, byte));
println!(" {} {count} edges", location(&repo, file, line));
}
if rows.len() > EXEMPLARS {
println!(" … (+{} files)", rows.len() - EXEMPLARS);
}
}
let dispatches: Vec<&Edge> = out
.iter()
.filter(|e| e.relation == Relation::Calls && e.evidence == Evidence::Dynamic)
.collect();
if !dispatches.is_empty() {
let shown: Vec<&str> = dispatches
.iter()
.take(EXEMPLARS)
.map(|e| qualified_of(e.dst.as_str()))
.collect();
let mut listed = shown.join(", ");
if dispatches.len() > EXEMPLARS {
listed.push_str(&format!(", … (+{})", dispatches.len() - EXEMPLARS));
}
println!("dispatches to ({}) {}", dispatches.len(), listed);
}
for (label, rel) in [("calls", Relation::Calls), ("uses", Relation::Uses)] {
let edges: Vec<&Edge> = out
.iter()
.filter(|e| e.relation == rel && e.evidence != Evidence::Dynamic)
.collect();
if edges.is_empty() {
continue;
}
let shown: Vec<String> = edges
.iter()
.take(EXEMPLARS)
.map(|e| {
let q = qualified_of(e.dst.as_str());
let name = q.rsplit("::").next().unwrap_or(q);
match site_location(&repo, e) {
Some(site) => format!("{name} ({site})"),
None => name.to_string(),
}
})
.collect();
let mut listed = shown.join(", ");
if edges.len() > EXEMPLARS {
listed.push_str(&format!(", … (+{})", edges.len() - EXEMPLARS));
}
println!(
"{:<16} {} [{}]",
format!("{label} ({})", edges.len()),
listed,
evidence_tally(&edges)
);
}
let unresolved = store.references_in(&node.file)?.len();
println!("unresolved refs in this file: {unresolved}");
println!();
println!(
"Next: sinter affected {} --max-depth 3",
qualified_of(node.id.as_str())
);
Ok(true)
}