use std::collections::BTreeSet;
use std::path::Path;
use anyhow::{Context, Result, bail};
use sinter_core::{Confidence, Evidence, Node};
use sinter_resolve::qualified_of;
use sinter_store::{EdgeFilter, Store};
use crate::pipeline;
#[derive(Debug)]
pub struct NoMatch(pub String);
impl std::fmt::Display for NoMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for NoMatch {}
pub fn open_store(repo: &Path) -> Result<Store> {
let repo = pipeline::discover_root(repo);
let repo = repo
.canonicalize()
.with_context(|| format!("repo path {}", repo.display()))?;
let path = pipeline::db_path(&repo);
if !path.exists() {
bail!("no graph at {} — run `sinter build` first", path.display());
}
pipeline::build(&repo, None)?;
open_current(&repo)
}
pub(crate) fn open_current(repo: &Path) -> Result<Store> {
let repo = pipeline::discover_root(repo);
let repo = repo
.canonicalize()
.with_context(|| format!("repo path {}", repo.display()))?;
let path = pipeline::db_path(&repo);
if !path.exists() {
bail!("no graph at {} — run `sinter build` first", path.display());
}
let store = Store::open(&path)?;
if store.node_count()? == 0 {
bail!(
"graph at {} is empty — was `sinter build` run in the right directory?",
path.display()
);
}
Ok(store)
}
pub enum Found {
Exact(Vec<Node>),
Suggestions(Vec<Node>),
}
pub fn find_symbol(store: &Store, symbol: &str) -> Result<Found> {
if symbol.contains('#') {
if let Some(node) = store.node(&sinter_core::NodeId::new(symbol))? {
return Ok(Found::Exact(vec![node]));
}
return Ok(Found::Suggestions(Vec::new()));
}
let (symbol, file) = match symbol.rsplit_once('@') {
Some((s, f)) if !s.is_empty() && !f.is_empty() => (s, Some(f)),
_ => (symbol, None),
};
let name = symbol.rsplit("::").next().unwrap_or(symbol);
let mut matches: Vec<Node> = store
.nodes_named(name)?
.into_iter()
.filter(|n| {
let q = qualified_of(n.id.as_str());
(q == symbol || q.ends_with(&format!("::{symbol}")))
&& file.is_none_or(|f| n.file == f || n.file.ends_with(&format!("/{f}")))
})
.collect();
matches.sort_by(|a, b| a.id.cmp(&b.id));
if matches.is_empty() {
Ok(Found::Suggestions(store.search(symbol, 10)?))
} else {
Ok(Found::Exact(matches))
}
}
pub fn unique_symbol(store: &Store, symbol: &str) -> Result<Node> {
match find_symbol(store, symbol)? {
Found::Exact(mut nodes) if nodes.len() == 1 => Ok(nodes.remove(0)),
Found::Exact(nodes) => {
let list: Vec<String> = nodes
.iter()
.map(|n| {
format!(
" {}@{} ({}, id {})",
qualified_of(n.id.as_str()),
n.file,
n.kind.as_str(),
n.id.as_str()
)
})
.collect();
bail!(
"`{symbol}` is ambiguous — rerun with one of these (`name@file`; a \
file-path suffix is enough) or the node id:\n{}",
list.join("\n")
)
}
Found::Suggestions(nodes) if nodes.is_empty() => Err(NoMatch(format!(
"no symbol matches `{symbol}` — try `sinter ask \"{symbol}\"` for concept search"
))
.into()),
Found::Suggestions(nodes) => {
let list: Vec<String> = nodes
.iter()
.map(|n| format!(" {}", qualified_of(n.id.as_str())))
.collect();
Err(NoMatch(format!(
"no exact match for `{symbol}`; close names:\n{}",
list.join("\n")
))
.into())
}
}
}
pub struct ExternalSite {
pub file: String,
pub enclosing: Option<String>,
pub refs: usize,
}
pub fn external_sites(store: &Store, symbol: &str) -> Result<Vec<ExternalSite>> {
let tail = symbol.rsplit([':', '/', '.']).next().unwrap_or(symbol);
if tail.is_empty() {
return Ok(Vec::new());
}
let matches = |written: &str| {
written == symbol
|| (written.ends_with(symbol)
&& written[..written.len() - symbol.len()]
.chars()
.next_back()
.is_some_and(|c| !c.is_alphanumeric() && c != '_'))
};
let files = store.ref_files(&BTreeSet::from([tail.to_string()]))?;
let mut sites: std::collections::BTreeMap<(String, Option<String>), usize> =
std::collections::BTreeMap::new();
for file in files {
for r in store.references_in(&file)? {
let written = r.path.as_deref().unwrap_or(&r.name);
if matches(written) || matches(&r.name) {
let enclosing = r.enclosing.map(|id| qualified_of(id.as_str()).to_string());
*sites.entry((r.file, enclosing)).or_default() += 1;
}
}
}
Ok(sites
.into_iter()
.map(|((file, enclosing), refs)| ExternalSite {
file,
enclosing,
refs,
})
.collect())
}
pub fn edge_filter(evidence: &[String], certain: bool) -> Result<EdgeFilter> {
let evidence = if evidence.is_empty() {
None
} else {
let mut set = BTreeSet::new();
for e in evidence {
set.insert(match e.as_str() {
"structural" => Evidence::Structural,
"scope" => Evidence::Scope,
"import" => Evidence::Import,
"scip" => Evidence::Scip,
"dynamic" => Evidence::Dynamic,
other => bail!("unknown evidence kind `{other}`"),
});
}
Some(set)
};
Ok(EdgeFilter {
evidence,
min_confidence: certain.then_some(Confidence::Certain),
relations: None,
})
}
pub fn relation_set(relations: &[String]) -> Result<Option<BTreeSet<sinter_core::Relation>>> {
if relations.is_empty() {
return Ok(None);
}
let mut set = BTreeSet::new();
for r in relations {
set.insert(match r.as_str() {
"calls" => sinter_core::Relation::Calls,
"uses" => sinter_core::Relation::Uses,
"imports" => sinter_core::Relation::Imports,
"implements" => sinter_core::Relation::Implements,
"extends" => sinter_core::Relation::Extends,
other => {
bail!("unknown relation `{other}` (calls, uses, imports, implements, extends)")
}
});
}
Ok(Some(set))
}
pub fn print_node(node: &Node) {
println!(
"{} {} {}:{}..{}",
node.kind.as_str(),
qualified_of(node.id.as_str()),
node.file,
node.span.start,
node.span.end
);
if !node.signature.is_empty() {
println!(" {}", node.signature);
}
if let Some(doc) = &node.doc {
for line in doc.lines().take(3) {
println!(" /// {line}");
}
}
}