use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::path::SourcePath;
use crate::source::SourceFile;
use crate::sym::SymbolId;
use crate::symbol::{Relation, RelationKind, Symbol, SymbolKind};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Codebase {
pub schema_version: u32,
pub name: String,
pub files: BTreeMap<SourcePath, SourceFile>,
pub symbols: BTreeMap<SymbolId, Symbol>,
pub relations: BTreeSet<Relation>,
}
impl Codebase {
pub fn new(name: impl Into<String>) -> Self {
Self { schema_version: crate::MODEL_SCHEMA_VERSION, name: name.into(), ..Self::default() }
}
pub fn from_json(text: &str) -> Result<Self, ModelJsonError> {
#[derive(Deserialize)]
struct Probe {
schema_version: Option<u32>,
schema: Option<u32>,
}
let probe: Probe = serde_json::from_str(text).map_err(ModelJsonError::Json)?;
let found = probe.schema_version.or(probe.schema);
if found != Some(crate::MODEL_SCHEMA_VERSION) {
return Err(ModelJsonError::Version { found, expected: crate::MODEL_SCHEMA_VERSION });
}
serde_json::from_str(text).map_err(ModelJsonError::Json)
}
pub fn add_file(&mut self, file: SourceFile) {
self.files.insert(file.path.clone(), file);
}
pub fn add_symbol(&mut self, symbol: Symbol) {
match self.symbols.get_mut(&symbol.id) {
Some(existing) => {
existing.sig_hash = existing.sig_hash.merge(symbol.sig_hash);
existing.body_hash = existing.body_hash.merge(symbol.body_hash);
existing.members.extend(symbol.members);
for tag in symbol.tags {
if !existing.tags.contains(&tag) {
existing.tags.push(tag);
}
}
}
None => {
self.symbols.insert(symbol.id.clone(), symbol);
}
}
}
pub fn add_relation(&mut self, relation: Relation) {
if relation.from != relation.to {
self.relations.insert(relation);
}
}
pub fn file(&self, path: &SourcePath) -> Option<&SourceFile> {
self.files.get(path)
}
pub fn symbol(&self, id: &SymbolId) -> Option<&Symbol> {
self.symbols.get(id)
}
pub fn is_internal(&self, id: &SymbolId) -> bool {
self.symbols.contains_key(id)
}
pub fn symbols_in_file<'a>(&'a self, path: &'a SourcePath) -> impl Iterator<Item = &'a Symbol> {
self.symbols.values().filter(move |s| &s.file == path)
}
pub fn symbols_of_kind(&self, kind: SymbolKind) -> impl Iterator<Item = &Symbol> {
self.symbols.values().filter(move |s| s.kind == kind)
}
pub fn children<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Symbol> {
self.symbols.values().filter(move |s| s.parent.as_ref() == Some(id))
}
pub fn relations_from<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
let lower = Relation::new(id.clone(), SymbolId::min_value(), RelationKind::Contains, crate::Confidence::Exact);
self.relations.range(lower..).take_while(move |r| &r.from == id)
}
pub fn relations_to<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
self.relations.iter().filter(move |r| &r.to == id)
}
pub fn relations_of_kind(&self, kind: RelationKind) -> impl Iterator<Item = &Relation> {
self.relations.iter().filter(move |r| r.kind == kind)
}
pub fn owner_of(&self, id: &SymbolId) -> Option<SymbolId> {
match self.symbols.get(id) {
Some(sym) if sym.kind == SymbolKind::Method || sym.kind == SymbolKind::Function => sym.parent.clone(),
Some(sym) => Some(sym.id.clone()),
None => id.parent(),
}
}
pub fn stats(&self) -> CodebaseStats {
let mut stats = CodebaseStats {
files: self.files.len(),
symbols: self.symbols.len(),
relations: self.relations.len(),
..CodebaseStats::default()
};
for sym in self.symbols.values() {
if let Some(flow) = &sym.flow {
stats.flows += 1;
stats.calls += flow.call_count();
}
}
stats
}
}
#[derive(Debug, thiserror::Error)]
pub enum ModelJsonError {
#[error("invalid model JSON: {0}")]
Json(serde_json::Error),
#[error("model schema version {found:?} is not supported (expected {expected})")]
Version {
found: Option<u32>,
expected: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct CodebaseStats {
pub files: usize,
pub symbols: usize,
pub relations: usize,
pub flows: usize,
pub calls: usize,
}