use crate::db::GraphDb;
use crate::repograph::facts::{
commits_of, evidence_line, evidence_lines, int_prop, label_of, list_prop, neighbors,
neighbors_both, owner_name, rank, score_of, str_prop, symbol_file,
};
use crate::repograph::map::SYNC_KEY;
use crate::repograph::render::sanitize;
use crate::Direction;
use core_storage::fs::Fs;
use core_storage::Value;
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
pub const MAX_SOURCE_LINES: usize = 80;
const MAX_CALLS: usize = 8;
const MAX_CALLER_FILES: usize = 12;
const MAX_SITES_PER_FILE: usize = 8;
const MAX_IMPORTS: usize = 8;
const MAX_PARTNERS: usize = 6;
const MAX_COMMITS: usize = 5;
const MAX_NOTES: usize = 3;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Target {
File {
path: String,
},
Symbol {
key: String,
},
Unknown {
target: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CallSites {
pub file: String,
pub symbols: Vec<String>,
pub lines: Vec<u32>,
pub sites: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ContextReport {
pub target: Target,
pub candidates: Vec<String>,
pub signature: Option<String>,
pub doc: Option<String>,
pub lines: Option<(u32, u32)>,
pub source: Option<String>,
pub file: String,
pub owner: Option<String>,
pub callers: Vec<CallSites>,
pub callers_not_shown: usize,
pub callees: Vec<(String, u32)>,
pub importers: Vec<String>,
pub imports: Vec<String>,
pub partners: Vec<(String, f64)>,
pub recent_commits: Vec<(String, i64, String)>,
pub notes: Vec<(String, String)>,
pub concepts: Vec<(String, String)>,
}
impl ContextReport {
fn empty(target: Target) -> Self {
Self {
target,
candidates: Vec::new(),
signature: None,
doc: None,
lines: None,
source: None,
file: String::new(),
owner: None,
callers: Vec::new(),
callers_not_shown: 0,
callees: Vec::new(),
importers: Vec::new(),
imports: Vec::new(),
partners: Vec::new(),
recent_commits: Vec::new(),
notes: Vec::new(),
concepts: Vec::new(),
}
}
}
#[must_use]
pub fn context<F: Fs>(db: &GraphDb<F>, repo: Option<&Path>, target: &str) -> ContextReport {
match resolve(db, target) {
Resolved::File(path) => {
let mut report = ContextReport::empty(Target::File {
path: sanitize(&path),
});
let symbols = neighbors(db, &path, "DEFINES", Direction::In);
(report.callers, report.callers_not_shown) = callers_of(db, &symbols, &path);
report.callees = callees_of(db, &symbols, &path);
report.source = read_source(db, repo, &path, None);
fill_file(db, &mut report, &path);
report.notes = notes_about(db, &[path]);
report
}
Resolved::Symbol(key) => {
let mut report = ContextReport::empty(Target::Symbol {
key: sanitize(&key),
});
report.signature = text_prop(db, &key, "signature");
report.doc = text_prop(db, &key, "doc");
report.lines = symbol_lines(db, &key);
(report.callers, report.callers_not_shown) =
callers_of(db, std::slice::from_ref(&key), "");
report.callees = callees_of(db, std::slice::from_ref(&key), "");
let file = symbol_file(db, &key).unwrap_or_default();
report.source = read_source(db, repo, &file, report.lines);
fill_file(db, &mut report, &file);
report.notes = notes_about(db, &[key, file]);
report
}
Resolved::Ambiguous(candidates) => {
let mut report = ContextReport::empty(Target::Unknown {
target: sanitize(target),
});
report.candidates = candidates;
report
}
Resolved::Unknown => ContextReport::empty(Target::Unknown {
target: sanitize(target),
}),
}
}
enum Resolved {
File(String),
Symbol(String),
Ambiguous(Vec<String>),
Unknown,
}
fn resolve<F: Fs>(db: &GraphDb<F>, target: &str) -> Resolved {
match label_of(db, target).as_deref() {
Some("File") => return Resolved::File(target.to_string()),
Some("Symbol") => return Resolved::Symbol(target.to_string()),
_ => {}
}
let mut named = named_symbols(db, target);
match named.len() {
0 => Resolved::Unknown,
1 => Resolved::Symbol(named.remove(0)),
_ => Resolved::Ambiguous(named),
}
}
fn named_symbols<F: Fs>(db: &GraphDb<F>, name: &str) -> Vec<String> {
let mut out: Vec<String> = db
.nodes_with_label("Symbol")
.iter()
.filter(|n| matches!(n.prop("name"), Some(Value::Str(s)) if s == name))
.map(|n| sanitize(n.key()))
.collect();
out.sort();
out
}
fn text_prop<F: Fs>(db: &GraphDb<F>, key: &str, field: &str) -> Option<String> {
str_prop(db, key, field)
.map(|s| sanitize(&s))
.filter(|s| !s.trim().is_empty())
}
fn symbol_lines<F: Fs>(db: &GraphDb<F>, key: &str) -> Option<(u32, u32)> {
let start = u32::try_from(int_prop(db, key, "line_start")?).ok()?;
let end = u32::try_from(int_prop(db, key, "line_end")?).ok()?;
Some((start, end.max(start)))
}
fn callers_of<F: Fs>(
db: &GraphDb<F>,
symbols: &[String],
exclude_file: &str,
) -> (Vec<CallSites>, usize) {
let mut by_file: BTreeMap<String, (BTreeSet<String>, BTreeSet<u32>)> = BTreeMap::new();
for symbol in symbols {
for caller in neighbors(db, symbol, "CALLS", Direction::In) {
let file = symbol_file(db, &caller).unwrap_or_default();
if !exclude_file.is_empty() && file == exclude_file {
continue;
}
let lines = evidence_lines(&list_prop(db, &caller, "call_lines"), symbol);
let slot = by_file.entry(sanitize(&file)).or_default();
slot.0.insert(sanitize(&caller));
slot.1
.extend(if lines.is_empty() { vec![0] } else { lines });
}
}
let total = by_file.len();
let mut out: Vec<CallSites> = by_file
.into_iter()
.map(|(file, (symbols, lines))| CallSites {
file,
symbols: symbols.into_iter().collect(),
sites: lines.len(),
lines: lines.into_iter().take(MAX_SITES_PER_FILE).collect(),
})
.collect();
out.sort_by(|a, b| b.sites.cmp(&a.sites).then(a.file.cmp(&b.file)));
out.truncate(MAX_CALLER_FILES);
let cut = total - out.len();
(out, cut)
}
fn callees_of<F: Fs>(
db: &GraphDb<F>,
symbols: &[String],
exclude_file: &str,
) -> Vec<(String, u32)> {
let mut out: Vec<(String, u32)> = Vec::new();
for symbol in symbols {
let lines = list_prop(db, symbol, "call_lines");
for callee in neighbors(db, symbol, "CALLS", Direction::Out) {
if !exclude_file.is_empty() && symbol_file(db, &callee).as_deref() == Some(exclude_file)
{
continue;
}
out.push((
sanitize(&callee),
evidence_line(&lines, &callee).unwrap_or(0),
));
}
}
out.sort();
out.dedup();
out.truncate(MAX_CALLS);
out
}
fn fill_file<F: Fs>(db: &GraphDb<F>, report: &mut ContextReport, file: &str) {
report.file = sanitize(file);
if file.is_empty() {
return;
}
report.owner = owner_name(db, file).map(|n| sanitize(&n));
report.importers = neighbors(db, file, "IMPORTS", Direction::In)
.iter()
.take(MAX_IMPORTS)
.map(|k| sanitize(k))
.collect();
report.imports = neighbors(db, file, "IMPORTS", Direction::Out)
.iter()
.take(MAX_IMPORTS)
.map(|k| sanitize(k))
.collect();
let mut partners: Vec<(String, f64)> = neighbors_both(db, file, "CO_CHANGED")
.into_iter()
.map(|other| {
let score = score_of(db, "CO_CHANGED", file, &other).unwrap_or(0.0);
(sanitize(&other), score)
})
.collect();
rank(&mut partners);
partners.truncate(MAX_PARTNERS);
report.partners = partners;
report.recent_commits = commits_of(db, file)
.into_iter()
.take(MAX_COMMITS)
.map(|c| (sanitize(&c.sha), c.ts, sanitize(&c.subject)))
.collect();
report.concepts = neighbors(db, file, "DESCRIBED_IN", Direction::In)
.iter()
.take(MAX_NOTES)
.map(|key| {
let name = str_prop(db, key, "name").unwrap_or_else(|| key.clone());
(sanitize(key), sanitize(&name))
})
.collect();
}
fn notes_about<F: Fs>(db: &GraphDb<F>, keys: &[String]) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
for key in keys.iter().filter(|k| !k.is_empty()) {
for note in neighbors(db, key, "ABOUT", Direction::In) {
let text = str_prop(db, ¬e, "text").unwrap_or_default();
out.push((sanitize(¬e), sanitize(&text)));
}
}
out.sort();
out.dedup();
out.truncate(MAX_NOTES);
out
}
fn inside_repo(root: &Path, file: &str) -> Option<std::path::PathBuf> {
let rel = Path::new(file);
if rel
.components()
.any(|c| !matches!(c, std::path::Component::Normal(_)))
{
return None;
}
let real_root = root.canonicalize().ok()?;
let real = real_root.join(rel).canonicalize().ok()?;
real.starts_with(&real_root).then_some(real)
}
fn read_source<F: Fs>(
db: &GraphDb<F>,
repo: Option<&Path>,
file: &str,
lines: Option<(u32, u32)>,
) -> Option<String> {
if file.is_empty() {
return None;
}
let root = match repo {
Some(p) => p.to_path_buf(),
None => std::path::PathBuf::from(str_prop(db, SYNC_KEY, "repo")?),
};
let text = std::fs::read_to_string(inside_repo(&root, file)?).ok()?;
let (first, last) = lines.unwrap_or((1, u32::MAX));
let skip = first.saturating_sub(1) as usize;
let take = (last.saturating_sub(first) as usize).saturating_add(1);
let excerpt: Vec<&str> = text
.lines()
.skip(skip)
.take(take.min(MAX_SOURCE_LINES))
.collect();
(!excerpt.is_empty()).then(|| excerpt.join("\n"))
}