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::sym::SymbolId;
8use crate::symbol::{Relation, RelationKind, Symbol, SymbolKind};
9
10/// The whole analysed codebase: files, symbols and relations.
11///
12/// All collections are ordered, so serialising a `Codebase` (for example with
13/// `serde_json`) is deterministic. Query methods return iterators in key
14/// order.
15#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
16pub struct Codebase {
17    /// Schema version, see [`MODEL_SCHEMA_VERSION`](crate::MODEL_SCHEMA_VERSION).
18    pub schema_version: u32,
19    /// Human name of the codebase (workspace or package name).
20    pub name: String,
21    /// Files keyed by path.
22    pub files: BTreeMap<SourcePath, SourceFile>,
23    /// Symbols keyed by id.
24    pub symbols: BTreeMap<SymbolId, Symbol>,
25    /// All relations, ordered.
26    pub relations: BTreeSet<Relation>,
27}
28
29impl Codebase {
30    /// An empty codebase called `name`.
31    pub fn new(name: impl Into<String>) -> Self {
32        Self { schema_version: crate::MODEL_SCHEMA_VERSION, name: name.into(), ..Self::default() }
33    }
34
35    /// Read a model serialised as JSON, refusing any schema version other
36    /// than [`MODEL_SCHEMA_VERSION`](crate::MODEL_SCHEMA_VERSION).
37    ///
38    /// ```
39    /// use sealmap_model::{Codebase, ModelJsonError};
40    ///
41    /// let json = serde_json::to_string(&Codebase::new("demo")).unwrap();
42    /// assert_eq!(Codebase::from_json(&json).unwrap().name, "demo");
43    ///
44    /// let v1 = r#"{"schema":1,"name":"old","files":{},"symbols":{},"relations":[]}"#;
45    /// assert!(matches!(Codebase::from_json(v1), Err(ModelJsonError::Version { found: Some(1), .. })));
46    /// ```
47    pub fn from_json(text: &str) -> Result<Self, ModelJsonError> {
48        #[derive(Deserialize)]
49        struct Probe {
50            schema_version: Option<u32>,
51            schema: Option<u32>,
52        }
53        let probe: Probe = serde_json::from_str(text).map_err(ModelJsonError::Json)?;
54        let found = probe.schema_version.or(probe.schema);
55        if found != Some(crate::MODEL_SCHEMA_VERSION) {
56            return Err(ModelJsonError::Version { found, expected: crate::MODEL_SCHEMA_VERSION });
57        }
58        serde_json::from_str(text).map_err(ModelJsonError::Json)
59    }
60
61    /// Add or replace a file.
62    pub fn add_file(&mut self, file: SourceFile) {
63        self.files.insert(file.path.clone(), file);
64    }
65
66    /// Add a symbol. If the id already exists the two are merged: the first
67    /// definition wins for scalar fields, list fields are concatenated, and
68    /// the fingerprints are folded with [`Fingerprint::merge`](crate::Fingerprint::merge)
69    /// so an edit to either definition shows. This handles e.g.
70    /// `#[cfg]`-gated duplicate definitions deterministically.
71    pub fn add_symbol(&mut self, symbol: Symbol) {
72        match self.symbols.get_mut(&symbol.id) {
73            Some(existing) => {
74                existing.sig_hash = existing.sig_hash.merge(symbol.sig_hash);
75                existing.body_hash = existing.body_hash.merge(symbol.body_hash);
76                existing.members.extend(symbol.members);
77                for tag in symbol.tags {
78                    if !existing.tags.contains(&tag) {
79                        existing.tags.push(tag);
80                    }
81                }
82            }
83            None => {
84                self.symbols.insert(symbol.id.clone(), symbol);
85            }
86        }
87    }
88
89    /// Add a relation (duplicates are ignored). Self-loops are dropped.
90    pub fn add_relation(&mut self, relation: Relation) {
91        if relation.from != relation.to {
92            self.relations.insert(relation);
93        }
94    }
95
96    /// Look up a file.
97    pub fn file(&self, path: &SourcePath) -> Option<&SourceFile> {
98        self.files.get(path)
99    }
100
101    /// Look up a symbol.
102    pub fn symbol(&self, id: &SymbolId) -> Option<&Symbol> {
103        self.symbols.get(id)
104    }
105
106    /// `true` if `id` is defined inside the codebase.
107    pub fn is_internal(&self, id: &SymbolId) -> bool {
108        self.symbols.contains_key(id)
109    }
110
111    /// Symbols defined in `path`, in id order.
112    pub fn symbols_in_file<'a>(&'a self, path: &'a SourcePath) -> impl Iterator<Item = &'a Symbol> {
113        self.symbols.values().filter(move |s| &s.file == path)
114    }
115
116    /// Symbols of `kind`, in id order.
117    pub fn symbols_of_kind(&self, kind: SymbolKind) -> impl Iterator<Item = &Symbol> {
118        self.symbols.values().filter(move |s| s.kind == kind)
119    }
120
121    /// Direct children of `id` (methods of a type, items of a module).
122    pub fn children<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Symbol> {
123        self.symbols.values().filter(move |s| s.parent.as_ref() == Some(id))
124    }
125
126    /// Relations whose source is `id`.
127    ///
128    /// `Relation` orders by `from` first, so this is a range scan
129    /// (O(log n + k)), not a pass over every relation.
130    pub fn relations_from<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
131        let lower = Relation::new(id.clone(), SymbolId::min_value(), RelationKind::Contains, crate::Confidence::Exact);
132        self.relations.range(lower..).take_while(move |r| &r.from == id)
133    }
134
135    /// Relations whose target is `id`.
136    pub fn relations_to<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
137        self.relations.iter().filter(move |r| &r.to == id)
138    }
139
140    /// Relations of one kind.
141    pub fn relations_of_kind(&self, kind: RelationKind) -> impl Iterator<Item = &Relation> {
142        self.relations.iter().filter(move |r| r.kind == kind)
143    }
144
145    /// The innermost *type* that owns `id` (the type for a method), or the
146    /// module for free items. This is the "participant" a symbol belongs to
147    /// in a sequence diagram.
148    ///
149    /// For external ids the parent path is returned as-is.
150    pub fn owner_of(&self, id: &SymbolId) -> Option<SymbolId> {
151        match self.symbols.get(id) {
152            Some(sym) if sym.kind == SymbolKind::Method || sym.kind == SymbolKind::Function => sym.parent.clone(),
153            Some(sym) => Some(sym.id.clone()),
154            None => id.parent(),
155        }
156    }
157
158    /// Summary counts.
159    pub fn stats(&self) -> CodebaseStats {
160        let mut stats = CodebaseStats {
161            files: self.files.len(),
162            symbols: self.symbols.len(),
163            relations: self.relations.len(),
164            ..CodebaseStats::default()
165        };
166        for sym in self.symbols.values() {
167            if let Some(flow) = &sym.flow {
168                stats.flows += 1;
169                stats.calls += flow.call_count();
170            }
171        }
172        stats
173    }
174}
175
176/// Why [`Codebase::from_json`] refused its input.
177#[derive(Debug, thiserror::Error)]
178pub enum ModelJsonError {
179    /// Not JSON, or not the shape of a [`Codebase`].
180    #[error("invalid model JSON: {0}")]
181    Json(serde_json::Error),
182    /// A schema version this build does not read.
183    #[error("model schema version {found:?} is not supported (expected {expected})")]
184    Version {
185        /// The version found (`schema_version`, or v1's `schema`), if any.
186        found: Option<u32>,
187        /// The version this build reads.
188        expected: u32,
189    },
190}
191
192/// Summary counts for a [`Codebase`].
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
194pub struct CodebaseStats {
195    /// Number of files.
196    pub files: usize,
197    /// Number of symbols.
198    pub symbols: usize,
199    /// Number of relations.
200    pub relations: usize,
201    /// Number of callables with a non-empty flow.
202    pub flows: usize,
203    /// Total call sites across all flows.
204    pub calls: usize,
205}