use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex, PoisonError};
use ahash::AHashMap;
use lru::LruCache;
use rmcp::ErrorData as McpError;
use super::MapCache;
use super::helpers_calls::for_each_call_in_file;
use super::helpers_graph::is_function_like;
use crate::index::IndexDb;
use crate::path::RelPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum EdgeKind {
Calls,
Imports,
Inherits,
Contains,
Annotates,
Cites,
#[cfg(feature = "documents")]
Documents,
}
impl EdgeKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
EdgeKind::Calls => "calls",
EdgeKind::Imports => "imports",
EdgeKind::Inherits => "inherits",
EdgeKind::Contains => "contains",
EdgeKind::Annotates => "annotates",
EdgeKind::Cites => "cites",
#[cfg(feature = "documents")]
EdgeKind::Documents => "documents",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum Provenance {
Extracted,
Inferred,
Ambiguous,
}
impl Provenance {
pub(crate) fn as_str(self) -> &'static str {
match self {
Provenance::Extracted => "extracted",
Provenance::Inferred => "inferred",
Provenance::Ambiguous => "ambiguous",
}
}
pub(crate) fn confidence(self) -> f32 {
match self {
Provenance::Extracted => 1.0,
Provenance::Inferred => 0.5,
Provenance::Ambiguous => 0.2,
}
}
pub(crate) fn rank(self) -> u8 {
match self {
Provenance::Extracted => 2,
Provenance::Inferred => 1,
Provenance::Ambiguous => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) enum NodeKey {
Symbol {
path: RelPath,
start_byte: u32,
},
File {
path: RelPath,
},
Name(String),
Rationale {
path: RelPath,
start_byte: u32,
},
Decision {
path: RelPath,
},
#[cfg(feature = "documents")]
DocChunk {
path: RelPath,
chunk_idx: u32,
},
}
impl NodeKey {
pub(crate) fn file(&self) -> Option<&RelPath> {
match self {
NodeKey::Symbol { path, .. }
| NodeKey::File { path }
| NodeKey::Rationale { path, .. }
| NodeKey::Decision { path } => Some(path),
#[cfg(feature = "documents")]
NodeKey::DocChunk { path, .. } => Some(path),
NodeKey::Name(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct CodeEdge {
pub(crate) from: NodeKey,
pub(crate) to: NodeKey,
pub(crate) kind: EdgeKind,
pub(crate) provenance: Provenance,
pub(crate) weight: u32,
}
#[cfg(feature = "documents")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DocMention {
Name(String),
Path(RelPath),
}
#[cfg(feature = "documents")]
#[derive(Debug, Clone)]
pub(crate) struct DocLink {
pub(crate) doc_path: RelPath,
pub(crate) chunk_idx: u32,
pub(crate) mention: DocMention,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct EdgeKindSet {
pub(crate) calls: bool,
pub(crate) imports: bool,
pub(crate) inherits: bool,
pub(crate) contains: bool,
pub(crate) annotates: bool,
pub(crate) cites: bool,
pub(crate) documents: bool,
}
impl EdgeKindSet {
#[cfg(test)]
pub(crate) fn all() -> Self {
Self {
calls: true,
imports: true,
inherits: true,
contains: true,
annotates: true,
cites: true,
documents: true,
}
}
fn none() -> Self {
Self {
calls: false,
imports: false,
inherits: false,
contains: false,
annotates: false,
cites: false,
documents: false,
}
}
pub(crate) fn contains_kind(&self, kind: EdgeKind) -> bool {
match kind {
EdgeKind::Calls => self.calls,
EdgeKind::Imports => self.imports,
EdgeKind::Inherits => self.inherits,
EdgeKind::Contains => self.contains,
EdgeKind::Annotates => self.annotates,
EdgeKind::Cites => self.cites,
#[cfg(feature = "documents")]
EdgeKind::Documents => self.documents,
}
}
pub(crate) fn from_edges_param(s: &str) -> Self {
match s {
"imports" => Self {
imports: true,
..Self::none()
},
"inherits" => Self {
inherits: true,
..Self::none()
},
"both" => Self {
calls: true,
imports: true,
..Self::none()
},
"all" => Self {
calls: true,
imports: true,
inherits: true,
..Self::none()
},
_ => Self {
calls: true,
..Self::none()
},
}
}
}
pub(crate) const CODEGRAPH_SCAN_CAP: usize = 4_000_000;
pub(crate) struct BuildOpts {
pub(crate) kinds: EdgeKindSet,
pub(crate) focus: Option<RelPath>,
pub(crate) scan_cap: usize,
}
pub(crate) struct CodeGraph {
pub(crate) edges: Vec<CodeEdge>,
pub(crate) truncated: bool,
}
pub(crate) type GraphKey = (u64, EdgeKindSet, Option<RelPath>, bool);
const GRAPH_MEMO_CAP: usize = 16;
pub(crate) type GraphMemo = LruCache<GraphKey, Arc<CodeGraph>>;
pub(crate) fn new_graph_memo() -> GraphMemo {
LruCache::new(NonZeroUsize::new(GRAPH_MEMO_CAP).expect("GRAPH_MEMO_CAP > 0"))
}
pub(crate) fn build_memoized(
memo: &Mutex<GraphMemo>,
idx: Option<&IndexDb>,
cache: &MapCache,
opts: &BuildOpts,
) -> Result<Arc<CodeGraph>, McpError> {
debug_assert_eq!(opts.scan_cap, CODEGRAPH_SCAN_CAP);
let key: GraphKey = (cache.fingerprint, opts.kinds, opts.focus.clone(), idx.is_some());
if let Some(hit) = memo.lock().unwrap_or_else(PoisonError::into_inner).get(&key).cloned() {
return Ok(hit);
}
let graph = Arc::new(build(idx, cache, opts)?);
memo.lock()
.unwrap_or_else(PoisonError::into_inner)
.put(key, Arc::clone(&graph));
Ok(graph)
}
fn module_leaf(module: &str) -> &str {
module.rsplit(['.', '/', ':']).find(|s| !s.is_empty()).unwrap_or(module)
}
fn trailing_identifier(raw: &str) -> Option<&str> {
let bytes = raw.as_bytes();
let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let mut end = bytes.len();
while end > 0 && !is_ident(bytes[end - 1]) {
end -= 1;
}
if end == 0 {
return None;
}
let mut start = end;
while start > 0 && is_ident(bytes[start - 1]) {
start -= 1;
}
Some(&raw[start..end])
}
fn import_leaf(imp: &crate::extract::Import) -> Option<&str> {
if let Some(m) = imp.module.as_deref() {
let leaf = module_leaf(m);
if !leaf.is_empty() {
return Some(leaf);
}
}
trailing_identifier(&imp.raw)
}
pub(crate) fn normalize_decision_id(prefix: &str, number: u32) -> String {
format!("{}-{number:04}", prefix.to_ascii_uppercase())
}
fn decision_id_of_path(path: &RelPath) -> Option<String> {
let s = path.as_str()?;
let (dir, base) = s.rsplit_once('/')?;
let in_adr = dir.split('/').any(|seg| seg.eq_ignore_ascii_case("adr"));
let in_rfc = dir.split('/').any(|seg| seg.eq_ignore_ascii_case("rfc"));
if !in_adr && !in_rfc {
return None;
}
let digits = base.as_bytes().iter().take_while(|b| b.is_ascii_digit()).count();
if digits == 0 {
return None;
}
let number: u32 = base[..digits].parse().ok()?;
Some(normalize_decision_id(if in_adr { "ADR" } else { "RFC" }, number))
}
fn attach_symbol(syms_by_start: &[(u32, u32)], marker: u32) -> Option<u32> {
let hi = syms_by_start.partition_point(|&(sb, _)| sb <= marker);
for &(sb, eb) in syms_by_start[..hi].iter().rev() {
if marker < eb {
return Some(sb);
}
}
syms_by_start[hi..].first().map(|&(sb, _)| sb)
}
type EdgeKey = (NodeKey, NodeKey, EdgeKind);
type DefsByName<'a> = AHashMap<&'a str, Vec<(&'a RelPath, u32, u32, crate::extract::SymbolKind)>>;
fn resolve_named_edge(
push: &mut impl FnMut(NodeKey, NodeKey, EdgeKind, Provenance, u32),
defs_by_name: &DefsByName<'_>,
from: NodeKey,
name: &str,
kind: EdgeKind,
) {
match defs_by_name.get(name).filter(|c| !c.is_empty()) {
None => push(from, NodeKey::Name(name.to_string()), kind, Provenance::Inferred, 1),
Some(cands) => {
let prov = if cands.len() > 1 {
Provenance::Ambiguous
} else {
Provenance::Inferred
};
for (dp, ds, _de, _k) in cands {
push(
from.clone(),
NodeKey::Symbol {
path: (*dp).clone(),
start_byte: *ds,
},
kind,
prov,
1,
);
}
}
}
}
pub(crate) fn build(idx: Option<&IndexDb>, cache: &MapCache, opts: &BuildOpts) -> Result<CodeGraph, McpError> {
let kinds = opts.kinds;
let in_focus = |p: &RelPath| {
opts.focus
.as_ref()
.is_none_or(|fx| p.as_bytes().starts_with(fx.as_bytes()))
};
let mut defs_by_name: DefsByName<'_> = AHashMap::new();
for (path, l1) in &cache.by_path {
for sym in &l1.symbols {
defs_by_name
.entry(sym.name.as_str())
.or_default()
.push((path, sym.start_byte, sym.end_byte, sym.kind));
}
}
let mut acc: AHashMap<EdgeKey, (u32, Provenance)> = AHashMap::new();
let mut push = |from: NodeKey, to: NodeKey, kind: EdgeKind, prov: Provenance, w: u32| {
acc.entry((from, to, kind))
.and_modify(|(weight, p)| {
*weight += w;
if prov.rank() > p.rank() {
*p = prov;
}
})
.or_insert((w, prov));
};
let want_calls = kinds.calls;
let mut scanned = 0usize;
let mut truncated = false;
for (path, l1) in &cache.by_path {
if !in_focus(path) {
continue;
}
if kinds.contains {
for sym in &l1.symbols {
push(
NodeKey::File { path: path.clone() },
NodeKey::Symbol {
path: path.clone(),
start_byte: sym.start_byte,
},
EdgeKind::Contains,
Provenance::Extracted,
1,
);
}
}
if kinds.imports {
for imp in &l1.imports {
let Some(leaf) = import_leaf(imp) else { continue };
resolve_named_edge(
&mut push,
&defs_by_name,
NodeKey::File { path: path.clone() },
leaf,
EdgeKind::Imports,
);
}
}
if kinds.inherits {
for imp in &l1.implementations {
let from = NodeKey::Symbol {
path: path.clone(),
start_byte: imp.start_byte,
};
resolve_named_edge(&mut push, &defs_by_name, from, &imp.trait_name, EdgeKind::Inherits);
}
}
if !want_calls {
continue;
}
let mut fns: Vec<(u32, u32)> = l1
.symbols
.iter()
.filter(|s| is_function_like(s.kind))
.map(|s| (s.start_byte, s.end_byte))
.collect();
fns.sort_unstable_by_key(|&(sb, _)| sb);
let enclosing = |call_byte: u32| -> NodeKey {
let hi = fns.partition_point(|&(sb, _)| sb <= call_byte);
let mut best: Option<u32> = None;
for &(sb, eb) in fns[..hi].iter().rev() {
if call_byte < eb {
best = Some(sb);
break;
}
}
match best {
Some(sb) => NodeKey::Symbol {
path: path.clone(),
start_byte: sb,
},
None => NodeKey::File { path: path.clone() },
}
};
let mut cap_hit = false;
for_each_call_in_file(idx, cache, path, |callee, call_byte| {
scanned += 1;
if scanned > opts.scan_cap {
cap_hit = true;
return false;
}
let cands: Vec<(&RelPath, u32, u32)> = match defs_by_name.get(callee) {
Some(c) => c
.iter()
.filter(|(_, _, _, k)| is_function_like(*k))
.map(|(p, start, end, _)| (*p, *start, *end))
.collect(),
None => Vec::new(),
};
if cands.is_empty() {
return true; }
let base = if cands.len() > 1 {
Provenance::Ambiguous
} else {
Provenance::Inferred
};
let from = enclosing(call_byte);
let proven = idx
.and_then(|index| index.definition_of(path, call_byte))
.and_then(|(def_path, def_byte)| {
cands
.iter()
.filter(|(candidate_path, start, end)| {
**candidate_path == def_path && def_byte >= *start && def_byte < *end
})
.max_by_key(|(_, start, _)| *start)
.copied()
});
if let Some((dp, ds, _)) = proven {
push(
from.clone(),
NodeKey::Symbol {
path: dp.clone(),
start_byte: ds,
},
EdgeKind::Calls,
Provenance::Extracted,
1,
);
}
if proven.is_none() {
for &(dp, ds, _) in &cands {
push(
from.clone(),
NodeKey::Symbol {
path: dp.clone(),
start_byte: ds,
},
EdgeKind::Calls,
base,
1,
);
}
}
true
})?;
if cap_hit {
truncated = true;
break;
}
}
if kinds.annotates || kinds.cites {
let mut decisions_by_id: AHashMap<String, Vec<&RelPath>> = AHashMap::new();
if kinds.cites {
for path in cache.by_path.keys() {
if let Some(id) = decision_id_of_path(path) {
decisions_by_id.entry(id).or_default().push(path);
}
}
}
for (path, l1) in &cache.by_path {
if !in_focus(path) || l1.rationale.is_empty() {
continue;
}
let mut syms: Vec<(u32, u32)> = l1.symbols.iter().map(|s| (s.start_byte, s.end_byte)).collect();
syms.sort_unstable_by_key(|&(sb, _)| sb);
for rec in &l1.rationale {
let from = NodeKey::Rationale {
path: path.clone(),
start_byte: rec.start_byte,
};
if kinds.annotates {
let target = match attach_symbol(&syms, rec.start_byte) {
Some(sb) => NodeKey::Symbol {
path: path.clone(),
start_byte: sb,
},
None => NodeKey::File { path: path.clone() },
};
push(from.clone(), target, EdgeKind::Annotates, Provenance::Inferred, 1);
}
if kinds.cites {
for citation in &rec.citations {
match decisions_by_id.get(citation.as_str()).filter(|c| !c.is_empty()) {
Some(paths) => {
let prov = if paths.len() > 1 {
Provenance::Ambiguous
} else {
Provenance::Extracted
};
for dp in paths {
push(
from.clone(),
NodeKey::Decision { path: (*dp).clone() },
EdgeKind::Cites,
prov,
1,
);
}
}
None => push(
from.clone(),
NodeKey::Name(citation.clone()),
EdgeKind::Cites,
Provenance::Inferred,
1,
),
}
}
}
}
}
}
#[cfg(feature = "documents")]
if kinds.documents {
for link in cache.doc_links.iter() {
if !in_focus(&link.doc_path) {
continue;
}
let from = NodeKey::DocChunk {
path: link.doc_path.clone(),
chunk_idx: link.chunk_idx,
};
match &link.mention {
DocMention::Name(name) => resolve_named_edge(&mut push, &defs_by_name, from, name, EdgeKind::Documents),
DocMention::Path(target) => {
if cache.by_path.contains_key(target) {
push(
from,
NodeKey::File { path: target.clone() },
EdgeKind::Documents,
Provenance::Extracted,
1,
);
} else {
let name = target.as_str().unwrap_or_default().to_string();
push(from, NodeKey::Name(name), EdgeKind::Documents, Provenance::Inferred, 1);
}
}
}
}
}
let mut edges: Vec<CodeEdge> = acc
.into_iter()
.map(|((from, to, kind), (weight, provenance))| CodeEdge {
from,
to,
kind,
provenance,
weight,
})
.collect();
edges.sort_by(|a, b| {
a.kind
.as_str()
.cmp(b.kind.as_str())
.then_with(|| a.from.cmp(&b.from))
.then_with(|| a.to.cmp(&b.to))
});
Ok(CodeGraph { edges, truncated })
}
#[cfg(test)]
#[path = "codegraph_tests.rs"]
mod tests;