Skip to main content

sealmap_model/
codebase.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::flow::{Arm, Call, Flow, Step};
6use crate::path::SourcePath;
7use crate::source::SourceFile;
8use crate::sym::SymbolId;
9use crate::symbol::{Relation, RelationKind, Symbol, SymbolKind};
10
11/// The whole analysed codebase: files, symbols and relations.
12///
13/// All collections are ordered, so serialising a `Codebase` (for example with
14/// `serde_json`) is deterministic. Query methods return iterators in key
15/// order.
16#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
17pub struct Codebase {
18    /// Schema version, see [`MODEL_SCHEMA_VERSION`](crate::MODEL_SCHEMA_VERSION).
19    pub schema_version: u32,
20    /// Human name of the codebase (workspace or package name).
21    pub name: String,
22    /// Files keyed by path.
23    pub files: BTreeMap<SourcePath, SourceFile>,
24    /// Symbols keyed by id.
25    pub symbols: BTreeMap<SymbolId, Symbol>,
26    /// All relations, ordered.
27    pub relations: BTreeSet<Relation>,
28}
29
30impl Codebase {
31    /// An empty codebase called `name`.
32    pub fn new(name: impl Into<String>) -> Self {
33        Self { schema_version: crate::MODEL_SCHEMA_VERSION, name: name.into(), ..Self::default() }
34    }
35
36    /// Read a model serialised as JSON, refusing any schema version other
37    /// than [`MODEL_SCHEMA_VERSION`](crate::MODEL_SCHEMA_VERSION).
38    ///
39    /// ```
40    /// use sealmap_model::{Codebase, ModelJsonError};
41    ///
42    /// let json = serde_json::to_string(&Codebase::new("demo")).unwrap();
43    /// assert_eq!(Codebase::from_json(&json).unwrap().name, "demo");
44    ///
45    /// let v1 = r#"{"schema":1,"name":"old","files":{},"symbols":{},"relations":[]}"#;
46    /// assert!(matches!(Codebase::from_json(v1), Err(ModelJsonError::Version { found: Some(1), .. })));
47    /// ```
48    pub fn from_json(text: &str) -> Result<Self, ModelJsonError> {
49        #[derive(Deserialize)]
50        struct Probe {
51            schema_version: Option<u32>,
52            schema: Option<u32>,
53        }
54        let probe: Probe = serde_json::from_str(text).map_err(ModelJsonError::Json)?;
55        let found = probe.schema_version.or(probe.schema);
56        if found != Some(crate::MODEL_SCHEMA_VERSION) {
57            return Err(ModelJsonError::Version { found, expected: crate::MODEL_SCHEMA_VERSION });
58        }
59        serde_json::from_str(text).map_err(ModelJsonError::Json)
60    }
61
62    /// Add or replace a file.
63    pub fn add_file(&mut self, file: SourceFile) {
64        self.files.insert(file.path.clone(), file);
65    }
66
67    /// Add a symbol. If the id already exists the two are merged: the first
68    /// definition wins for scalar fields, list fields are concatenated, and
69    /// the fingerprints are folded with [`Fingerprint::merge`](crate::Fingerprint::merge)
70    /// so an edit to either definition shows. This handles e.g.
71    /// `#[cfg]`-gated duplicate definitions deterministically.
72    ///
73    /// Flows are kept, not dropped: twins whose flows differ become the arms
74    /// of one [`Step::Branch`], one arm per definition, each labelled
75    /// `cfg twin at <file>:<line>` (an arm is empty when its definition makes
76    /// no calls). Flows that differ only in line numbers stay one flat flow.
77    ///
78    /// ```
79    /// use sealmap_model::*;
80    ///
81    /// let id = |s: &str| SymbolId::parse(s).unwrap();
82    /// let file = SourcePath::new("src/lib.rs").unwrap();
83    /// let twin = |line, callee: &str| {
84    ///     let mut s = Symbol::new(id("sym:cargo app . plat()."), "plat", SymbolKind::Function, file.clone());
85    ///     s.span.start_line = line;
86    ///     s.flow = Some(Flow::new(vec![Step::Call(Call::new(id(callee), "f", Confidence::Exact))]));
87    ///     s
88    /// };
89    /// let mut cb = Codebase::new("app");
90    /// cb.add_symbol(twin(2, "sym:cargo app . unix_impl()."));
91    /// cb.add_symbol(twin(5, "sym:cargo app . windows_impl()."));
92    /// let flow = cb.symbol(&id("sym:cargo app . plat().")).unwrap().flow.as_ref().unwrap();
93    /// let names: Vec<_> = flow.calls().map(|c| c.target.name()).collect();
94    /// assert_eq!(names, ["unix_impl", "windows_impl"]);
95    /// let Step::Branch { arms } = &flow.steps[0] else { unreachable!() };
96    /// assert_eq!(arms[1].label, "cfg twin at src/lib.rs:5");
97    /// ```
98    pub fn add_symbol(&mut self, symbol: Symbol) {
99        match self.symbols.get_mut(&symbol.id) {
100            Some(existing) => {
101                existing.sig_hash = existing.sig_hash.merge(symbol.sig_hash);
102                existing.body_hash = existing.body_hash.merge(symbol.body_hash);
103                merge_twin_flow(existing, symbol.flow, &symbol.file, symbol.span.start_line);
104                existing.members.extend(symbol.members);
105                for tag in symbol.tags {
106                    if !existing.tags.contains(&tag) {
107                        existing.tags.push(tag);
108                    }
109                }
110            }
111            None => {
112                self.symbols.insert(symbol.id.clone(), symbol);
113            }
114        }
115    }
116
117    /// Add a relation (duplicates are ignored). Self-loops are dropped.
118    pub fn add_relation(&mut self, relation: Relation) {
119        if relation.from != relation.to {
120            self.relations.insert(relation);
121        }
122    }
123
124    /// Look up a file.
125    pub fn file(&self, path: &SourcePath) -> Option<&SourceFile> {
126        self.files.get(path)
127    }
128
129    /// Look up a symbol.
130    pub fn symbol(&self, id: &SymbolId) -> Option<&Symbol> {
131        self.symbols.get(id)
132    }
133
134    /// `true` if `id` is defined inside the codebase.
135    pub fn is_internal(&self, id: &SymbolId) -> bool {
136        self.symbols.contains_key(id)
137    }
138
139    /// Symbols defined in `path`, in id order.
140    pub fn symbols_in_file<'a>(&'a self, path: &'a SourcePath) -> impl Iterator<Item = &'a Symbol> {
141        self.symbols.values().filter(move |s| &s.file == path)
142    }
143
144    /// Symbols of `kind`, in id order.
145    pub fn symbols_of_kind(&self, kind: SymbolKind) -> impl Iterator<Item = &Symbol> {
146        self.symbols.values().filter(move |s| s.kind == kind)
147    }
148
149    /// Direct children of `id` (methods of a type, items of a module).
150    pub fn children<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Symbol> {
151        self.symbols.values().filter(move |s| s.parent.as_ref() == Some(id))
152    }
153
154    /// Relations whose source is `id`.
155    ///
156    /// `Relation` orders by `from` first, so this is a range scan
157    /// (O(log n + k)), not a pass over every relation.
158    pub fn relations_from<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
159        let lower = Relation::new(id.clone(), SymbolId::min_value(), RelationKind::Contains, crate::Confidence::Exact);
160        self.relations.range(lower..).take_while(move |r| &r.from == id)
161    }
162
163    /// Relations whose target is `id`.
164    pub fn relations_to<'a>(&'a self, id: &'a SymbolId) -> impl Iterator<Item = &'a Relation> {
165        self.relations.iter().filter(move |r| &r.to == id)
166    }
167
168    /// Relations of one kind.
169    pub fn relations_of_kind(&self, kind: RelationKind) -> impl Iterator<Item = &Relation> {
170        self.relations.iter().filter(move |r| r.kind == kind)
171    }
172
173    /// The innermost *type* that owns `id` (the type for a method), or the
174    /// module for free items. This is the "participant" a symbol belongs to
175    /// in a sequence diagram.
176    ///
177    /// For external ids the parent path is returned as-is.
178    pub fn owner_of(&self, id: &SymbolId) -> Option<SymbolId> {
179        match self.symbols.get(id) {
180            Some(sym) if sym.kind == SymbolKind::Method || sym.kind == SymbolKind::Function => sym.parent.clone(),
181            Some(sym) => Some(sym.id.clone()),
182            None => id.parent(),
183        }
184    }
185
186    /// Summary counts.
187    pub fn stats(&self) -> CodebaseStats {
188        let mut stats = CodebaseStats {
189            files: self.files.len(),
190            symbols: self.symbols.len(),
191            relations: self.relations.len(),
192            ..CodebaseStats::default()
193        };
194        for sym in self.symbols.values() {
195            if let Some(flow) = &sym.flow {
196                stats.flows += 1;
197                stats.calls += flow.call_count();
198            }
199        }
200        stats
201    }
202}
203
204/// Why [`Codebase::from_json`] refused its input.
205#[derive(Debug, thiserror::Error)]
206pub enum ModelJsonError {
207    /// Not JSON, or not the shape of a [`Codebase`].
208    #[error("invalid model JSON: {0}")]
209    Json(serde_json::Error),
210    /// A schema version this build does not read.
211    #[error("model schema version {found:?} is not supported (expected {expected})")]
212    Version {
213        /// The version found (`schema_version`, or v1's `schema`), if any.
214        found: Option<u32>,
215        /// The version this build reads.
216        expected: u32,
217    },
218}
219
220/// Summary counts for a [`Codebase`].
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
222pub struct CodebaseStats {
223    /// Number of files.
224    pub files: usize,
225    /// Number of symbols.
226    pub symbols: usize,
227    /// Number of relations.
228    pub relations: usize,
229    /// Number of callables with a non-empty flow.
230    pub flows: usize,
231    /// Total call sites across all flows.
232    pub calls: usize,
233}
234
235/// Label prefix of the arms [`Codebase::add_symbol`] makes for `#[cfg]` twins.
236const TWIN_ARM: &str = "cfg twin at ";
237
238fn twin_arm(file: &SourcePath, line: u32, flow: Option<Flow>) -> Arm {
239    Arm { label: format!("{TWIN_ARM}{file}:{line}"), steps: flow.map(|f| f.steps).unwrap_or_default() }
240}
241
242/// Fold a twin's flow into `existing`. A flow that is already the twin
243/// branch of earlier merges gains one arm; identical flows stay as they are.
244fn merge_twin_flow(existing: &mut Symbol, flow: Option<Flow>, file: &SourcePath, line: u32) {
245    let same = match (&existing.flow, &flow) {
246        (Some(a), Some(b)) => same_steps(&a.steps, &b.steps),
247        (a, b) => a.is_none() && b.is_none(),
248    };
249    if same {
250        return;
251    }
252    let mut arms = match existing.flow.take().map(twin_arms) {
253        Some(Ok(arms)) => arms,
254        Some(Err(first)) => vec![twin_arm(&existing.file, existing.span.start_line, Some(first))],
255        None => vec![twin_arm(&existing.file, existing.span.start_line, None)],
256    };
257    arms.push(twin_arm(file, line, flow));
258    existing.flow = Some(Flow::new(vec![Step::Branch { arms }]));
259}
260
261/// The arms of a flow that is already a twin branch, or the flow back.
262fn twin_arms(mut flow: Flow) -> Result<Vec<Arm>, Flow> {
263    let twin =
264        matches!(flow.steps.as_slice(), [Step::Branch { arms }] if arms.iter().all(|a| a.label.starts_with(TWIN_ARM)));
265    if twin {
266        if let Some(Step::Branch { arms }) = flow.steps.pop() {
267            return Ok(arms);
268        }
269    }
270    Err(flow)
271}
272
273/// Two step lists that differ at most in their line numbers.
274fn same_steps(a: &[Step], b: &[Step]) -> bool {
275    let arms = |x: &[Arm], y: &[Arm]| {
276        x.len() == y.len() && x.iter().zip(y).all(|(p, q)| p.label == q.label && same_steps(&p.steps, &q.steps))
277    };
278    a.len() == b.len()
279        && a.iter().zip(b).all(|pair| match pair {
280            (Step::Call(x), Step::Call(y)) => Call { line: y.line, ..x.clone() } == *y,
281            (Step::Return(x), Step::Return(y)) => x.label == y.label,
282            (Step::Branch { arms: x }, Step::Branch { arms: y })
283            | (Step::Parallel { arms: x }, Step::Parallel { arms: y }) => arms(x, y),
284            (Step::Loop { label: l, body: x }, Step::Loop { label: m, body: y })
285            | (Step::Optional { label: l, body: x }, Step::Optional { label: m, body: y }) => {
286                l == m && same_steps(x, y)
287            }
288            _ => false,
289        })
290}