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#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
17pub struct Codebase {
18 pub schema_version: u32,
20 pub name: String,
22 pub files: BTreeMap<SourcePath, SourceFile>,
24 pub symbols: BTreeMap<SymbolId, Symbol>,
26 pub relations: BTreeSet<Relation>,
28}
29
30impl Codebase {
31 pub fn new(name: impl Into<String>) -> Self {
33 Self { schema_version: crate::MODEL_SCHEMA_VERSION, name: name.into(), ..Self::default() }
34 }
35
36 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 pub fn add_file(&mut self, file: SourceFile) {
64 self.files.insert(file.path.clone(), file);
65 }
66
67 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 pub fn add_relation(&mut self, relation: Relation) {
119 if relation.from != relation.to {
120 self.relations.insert(relation);
121 }
122 }
123
124 pub fn file(&self, path: &SourcePath) -> Option<&SourceFile> {
126 self.files.get(path)
127 }
128
129 pub fn symbol(&self, id: &SymbolId) -> Option<&Symbol> {
131 self.symbols.get(id)
132 }
133
134 pub fn is_internal(&self, id: &SymbolId) -> bool {
136 self.symbols.contains_key(id)
137 }
138
139 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 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 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 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 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 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 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 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#[derive(Debug, thiserror::Error)]
206pub enum ModelJsonError {
207 #[error("invalid model JSON: {0}")]
209 Json(serde_json::Error),
210 #[error("model schema version {found:?} is not supported (expected {expected})")]
212 Version {
213 found: Option<u32>,
215 expected: u32,
217 },
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
222pub struct CodebaseStats {
223 pub files: usize,
225 pub symbols: usize,
227 pub relations: usize,
229 pub flows: usize,
231 pub calls: usize,
233}
234
235const 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
242fn 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
261fn 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
273fn 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}