Skip to main content

sealmap_model/
codebase.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::path::SourcePath;
6use crate::source::SourceFile;
7use crate::symbol::{Relation, RelationKind, Symbol, SymbolId, SymbolKind};
8
9/// The whole analysed codebase: files, symbols and relations.
10///
11/// All collections are ordered, so serialising a `Codebase` (for example with
12/// `serde_json`) is deterministic. Query methods return iterators in key
13/// order.
14#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub struct Codebase {
16    /// Schema version, see [`MODEL_SCHEMA_VERSION`](crate::MODEL_SCHEMA_VERSION).
17    pub schema: u32,
18    /// Human name of the codebase (workspace or package name).
19    pub name: String,
20    /// Files keyed by path.
21    pub files: BTreeMap<SourcePath, SourceFile>,
22    /// Symbols keyed by id.
23    pub symbols: BTreeMap<SymbolId, Symbol>,
24    /// All relations, ordered.
25    pub relations: BTreeSet<Relation>,
26}
27
28impl Codebase {
29    /// An empty codebase called `name`.
30    pub fn new(name: impl Into<String>) -> Self {
31        Self { schema: crate::MODEL_SCHEMA_VERSION, name: name.into(), ..Self::default() }
32    }
33
34    /// Add or replace a file.
35    pub fn add_file(&mut self, file: SourceFile) {
36        self.files.insert(file.path.clone(), file);
37    }
38
39    /// Add a symbol. If the id already exists the two are merged: the first
40    /// definition wins for scalar fields, list fields are concatenated. This
41    /// handles e.g. `#[cfg]`-gated duplicate definitions deterministically.
42    pub fn add_symbol(&mut self, symbol: Symbol) {
43        match self.symbols.get_mut(&symbol.id) {
44            Some(existing) => {
45                existing.members.extend(symbol.members);
46                for tag in symbol.tags {
47                    if !existing.tags.contains(&tag) {
48                        existing.tags.push(tag);
49                    }
50                }
51            }
52            None => {
53                self.symbols.insert(symbol.id.clone(), symbol);
54            }
55        }
56    }
57
58    /// Add a relation (duplicates are ignored). Self-loops are dropped.
59    pub fn add_relation(&mut self, relation: Relation) {
60        if relation.from != relation.to {
61            self.relations.insert(relation);
62        }
63    }
64
65    /// Look up a file.
66    pub fn file(&self, path: &SourcePath) -> Option<&SourceFile> {
67        self.files.get(path)
68    }
69
70    /// Look up a symbol.
71    pub fn symbol(&self, id: &SymbolId) -> Option<&Symbol> {
72        self.symbols.get(id)
73    }
74
75    /// `true` if `id` is defined inside the codebase.
76    pub fn is_internal(&self, id: &SymbolId) -> bool {
77        self.symbols.contains_key(id)
78    }
79
80    /// Symbols defined in `path`, in id order.
81    pub fn symbols_in_file<'a>(&'a self, path: &'a SourcePath) -> impl Iterator<Item = &'a Symbol> {
82        self.symbols.values().filter(move |s| &s.file == path)
83    }
84
85    /// Symbols of `kind`, in id order.
86    pub fn symbols_of_kind(&self, kind: SymbolKind) -> impl Iterator<Item = &Symbol> {
87        self.symbols.values().filter(move |s| s.kind == kind)
88    }
89
90    /// Direct children of `id` (methods of a type, items of a module).
91    pub fn children<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Symbol> {
92        self.symbols.values().filter(move |s| s.parent.as_ref() == Some(id))
93    }
94
95    /// Relations whose source is `id`.
96    ///
97    /// `Relation` orders by `from` first, so this is a range scan
98    /// (O(log n + k)), not a pass over every relation.
99    pub fn relations_from<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
100        let lower = Relation::new(id.clone(), SymbolId::new(""), RelationKind::Contains, crate::Confidence::Exact);
101        self.relations.range(lower..).take_while(move |r| &r.from == id)
102    }
103
104    /// Relations whose target is `id`.
105    pub fn relations_to<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
106        self.relations.iter().filter(move |r| &r.to == id)
107    }
108
109    /// Relations of one kind.
110    pub fn relations_of_kind(&self, kind: RelationKind) -> impl Iterator<Item = &Relation> {
111        self.relations.iter().filter(move |r| r.kind == kind)
112    }
113
114    /// The innermost *type* that owns `id` (the type for a method), or the
115    /// module for free items. This is the "participant" a symbol belongs to
116    /// in a sequence diagram.
117    ///
118    /// For external ids the parent path is returned as-is.
119    pub fn owner_of(&self, id: &SymbolId) -> Option<SymbolId> {
120        match self.symbols.get(id) {
121            Some(sym) if sym.kind == SymbolKind::Method || sym.kind == SymbolKind::Function => sym.parent.clone(),
122            Some(sym) => Some(sym.id.clone()),
123            None => id.parent(),
124        }
125    }
126
127    /// Summary counts.
128    pub fn stats(&self) -> CodebaseStats {
129        let mut stats = CodebaseStats {
130            files: self.files.len(),
131            symbols: self.symbols.len(),
132            relations: self.relations.len(),
133            ..CodebaseStats::default()
134        };
135        for sym in self.symbols.values() {
136            if let Some(flow) = &sym.flow {
137                stats.flows += 1;
138                stats.calls += flow.call_count();
139            }
140        }
141        stats
142    }
143}
144
145/// Summary counts for a [`Codebase`].
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
147pub struct CodebaseStats {
148    /// Number of files.
149    pub files: usize,
150    /// Number of symbols.
151    pub symbols: usize,
152    /// Number of relations.
153    pub relations: usize,
154    /// Number of callables with a non-empty flow.
155    pub flows: usize,
156    /// Total call sites across all flows.
157    pub calls: usize,
158}