1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::{
8 error::GitCortexError,
9 schema::{CodeSmell, DesignPattern, EdgeConfidence, EdgeKind, NodeKind, SolidHint, Visibility},
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct NodeId(Uuid);
17
18impl NodeId {
19 pub fn new() -> Self {
20 Self(Uuid::new_v4())
21 }
22
23 pub fn as_str(&self) -> String {
24 self.0.to_string()
25 }
26}
27
28impl Default for NodeId {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl std::fmt::Display for NodeId {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 self.0.fmt(f)
37 }
38}
39
40impl TryFrom<&str> for NodeId {
41 type Error = GitCortexError;
42
43 fn try_from(s: &str) -> Result<Self, Self::Error> {
44 Uuid::parse_str(s)
45 .map(NodeId)
46 .map_err(|e| GitCortexError::Store(format!("invalid NodeId '{s}': {e}")))
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct Span {
54 pub start_line: u32,
55 pub end_line: u32,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
63pub struct LldLabels {
64 pub solid_hints: Vec<SolidHint>,
65 pub patterns: Vec<DesignPattern>,
66 pub smells: Vec<CodeSmell>,
67 pub complexity: Option<u32>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
78pub struct DefinitionText {
79 pub signature: String,
82 pub body: String,
84 pub doc_comment: Option<String>,
87 pub start_byte: u32,
89 pub end_byte: u32,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
94pub struct NodeMetadata {
95 pub loc: u32,
97 pub visibility: Visibility,
98 pub is_async: bool,
99 pub is_unsafe: bool,
100 pub is_static: bool,
102 pub is_abstract: bool,
104 pub is_final: bool,
106 pub is_property: bool,
108 pub is_generator: bool,
110 pub is_const: bool,
112 pub generic_bounds: Vec<String>,
115 pub annotations: Vec<String>,
121 pub lld: LldLabels,
123 pub definition: DefinitionText,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct Node {
132 pub id: NodeId,
133 pub kind: NodeKind,
134 pub name: String,
136 pub qualified_name: String,
138 pub file: PathBuf,
140 pub span: Span,
141 pub metadata: NodeMetadata,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct Edge {
147 pub src: NodeId,
148 pub dst: NodeId,
149 pub kind: EdgeKind,
150 #[serde(default)]
154 pub line: Option<u32>,
155 #[serde(default)]
157 pub confidence: EdgeConfidence,
158}
159
160impl Edge {
161 pub fn new(src: NodeId, dst: NodeId, kind: EdgeKind) -> Self {
164 Self {
165 src,
166 dst,
167 kind,
168 line: None,
169 confidence: EdgeConfidence::Extracted,
170 }
171 }
172
173 pub fn call(src: NodeId, dst: NodeId, line: u32) -> Self {
175 Self {
176 src,
177 dst,
178 kind: EdgeKind::Calls,
179 line: Some(line),
180 confidence: EdgeConfidence::Extracted,
181 }
182 }
183
184 pub fn with_confidence(mut self, confidence: EdgeConfidence) -> Self {
187 self.confidence = confidence;
188 self
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
197pub struct GraphDiff {
198 pub added_nodes: Vec<Node>,
199 pub removed_node_ids: Vec<NodeId>,
201 pub removed_files: Vec<PathBuf>,
206 pub added_edges: Vec<Edge>,
207 pub removed_edges: Vec<(NodeId, NodeId, EdgeKind)>,
208 pub deferred_calls: Vec<(NodeId, String, u32)>,
213 pub deferred_uses: Vec<(NodeId, String)>,
215 pub deferred_implements: Vec<(NodeId, String)>,
217 pub deferred_inherits: Vec<(NodeId, String)>,
219 pub deferred_throws: Vec<(NodeId, String)>,
221 pub deferred_annotated: Vec<(NodeId, String)>,
223 pub deferred_doc_refs: Vec<(NodeId, String)>,
226}
227
228impl GraphDiff {
229 pub fn is_empty(&self) -> bool {
230 self.added_nodes.is_empty()
231 && self.removed_node_ids.is_empty()
232 && self.removed_files.is_empty()
233 && self.added_edges.is_empty()
234 && self.removed_edges.is_empty()
235 && self.deferred_calls.is_empty()
236 && self.deferred_uses.is_empty()
237 && self.deferred_implements.is_empty()
238 && self.deferred_inherits.is_empty()
239 && self.deferred_throws.is_empty()
240 && self.deferred_annotated.is_empty()
241 && self.deferred_doc_refs.is_empty()
242 }
243
244 pub fn merge(&mut self, other: GraphDiff) {
248 self.added_nodes.extend(other.added_nodes);
249 self.removed_node_ids.extend(other.removed_node_ids);
250 self.removed_files.extend(other.removed_files);
251 self.added_edges.extend(other.added_edges);
252 self.removed_edges.extend(other.removed_edges);
253 self.deferred_calls.extend(other.deferred_calls);
254 self.deferred_uses.extend(other.deferred_uses);
255 self.deferred_implements.extend(other.deferred_implements);
256 self.deferred_inherits.extend(other.deferred_inherits);
257 self.deferred_throws.extend(other.deferred_throws);
258 self.deferred_annotated.extend(other.deferred_annotated);
259 self.deferred_doc_refs.extend(other.deferred_doc_refs);
260 }
261}
262
263pub fn in_degree_by_calls(edges: &[Edge]) -> HashMap<String, u32> {
269 let mut in_degree: HashMap<String, u32> = HashMap::new();
270 for e in edges {
271 if matches!(e.kind, EdgeKind::Calls) {
272 *in_degree.entry(e.dst.as_str()).or_insert(0) += 1;
273 }
274 }
275 in_degree
276}
277
278pub fn find_import_cycles(edges: &[Edge]) -> Result<Vec<Vec<String>>, GitCortexError> {
282 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
283 for e in edges {
284 if matches!(e.kind, EdgeKind::Imports) {
285 adj.entry(e.src.as_str()).or_default().push(e.dst.as_str());
286 }
287 }
288
289 let nodes: Vec<String> = adj.keys().cloned().collect();
290 let mut index_counter = 0usize;
291 let mut stack: Vec<String> = Vec::new();
292 let mut on_stack: HashMap<String, bool> = HashMap::new();
293 let mut index: HashMap<String, usize> = HashMap::new();
294 let mut lowlink: HashMap<String, usize> = HashMap::new();
295 let mut result: Vec<Vec<String>> = Vec::new();
296
297 #[allow(clippy::too_many_arguments)]
298 fn strongconnect(
299 v: &str,
300 adj: &HashMap<String, Vec<String>>,
301 counter: &mut usize,
302 stack: &mut Vec<String>,
303 on_stack: &mut HashMap<String, bool>,
304 index: &mut HashMap<String, usize>,
305 lowlink: &mut HashMap<String, usize>,
306 result: &mut Vec<Vec<String>>,
307 ) -> Result<(), GitCortexError> {
308 index.insert(v.to_owned(), *counter);
309 lowlink.insert(v.to_owned(), *counter);
310 *counter += 1;
311 stack.push(v.to_owned());
312 on_stack.insert(v.to_owned(), true);
313
314 if let Some(neighbours) = adj.get(v) {
315 for w in neighbours.iter() {
316 if !index.contains_key(w.as_str()) {
317 strongconnect(w, adj, counter, stack, on_stack, index, lowlink, result)?;
318 let ll_w = lowlink[w.as_str()];
319 let ll_v = lowlink[v];
320 lowlink.insert(v.to_owned(), ll_v.min(ll_w));
321 } else if *on_stack.get(w.as_str()).unwrap_or(&false) {
322 let idx_w = index[w.as_str()];
323 let ll_v = lowlink[v];
324 lowlink.insert(v.to_owned(), ll_v.min(idx_w));
325 }
326 }
327 }
328
329 if lowlink[v] == index[v] {
330 let mut scc: Vec<String> = Vec::new();
331 loop {
332 let w = stack.pop().ok_or_else(|| {
333 GitCortexError::Store("SCC stack underflow: Tarjan invariant violated".into())
334 })?;
335 on_stack.insert(w.clone(), false);
336 scc.push(w.clone());
337 if w == v {
338 break;
339 }
340 }
341 if scc.len() > 1 {
342 result.push(scc);
343 }
344 }
345 Ok(())
346 }
347
348 for v in &nodes {
349 if !index.contains_key(v.as_str()) {
350 strongconnect(
351 v,
352 &adj,
353 &mut index_counter,
354 &mut stack,
355 &mut on_stack,
356 &mut index,
357 &mut lowlink,
358 &mut result,
359 )?;
360 }
361 }
362
363 Ok(result)
364}
365
366#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn node_id_is_unique() {
374 let a = NodeId::new();
375 let b = NodeId::new();
376 assert_ne!(a, b);
377 }
378
379 #[test]
380 fn graph_diff_merge() {
381 let node = Node {
382 id: NodeId::new(),
383 kind: NodeKind::Function,
384 name: "foo".into(),
385 qualified_name: "crate::foo".into(),
386 file: PathBuf::from("src/lib.rs"),
387 span: Span {
388 start_line: 1,
389 end_line: 3,
390 },
391 metadata: NodeMetadata::default(),
392 };
393 let mut base = GraphDiff::default();
394 let other = GraphDiff {
395 added_nodes: vec![node],
396 ..Default::default()
397 };
398 base.merge(other);
399 assert_eq!(base.added_nodes.len(), 1);
400 }
401
402 #[test]
403 fn graph_diff_is_empty_on_default() {
404 assert!(GraphDiff::default().is_empty());
405 }
406
407 fn import_edge(src: &NodeId, dst: &NodeId) -> Edge {
408 Edge::new(src.clone(), dst.clone(), EdgeKind::Imports)
409 }
410
411 #[test]
412 fn cycles_empty_when_imports_are_acyclic() {
413 let (a, b, c) = (NodeId::new(), NodeId::new(), NodeId::new());
414 let edges = vec![import_edge(&a, &b), import_edge(&b, &c)];
416 assert!(find_import_cycles(&edges).unwrap().is_empty());
417 }
418
419 #[test]
420 fn cycles_detects_two_node_cycle() {
421 let (a, b) = (NodeId::new(), NodeId::new());
422 let edges = vec![import_edge(&a, &b), import_edge(&b, &a)];
423 let cycles = find_import_cycles(&edges).unwrap();
424 assert_eq!(cycles.len(), 1);
425 let members: std::collections::HashSet<&String> = cycles[0].iter().collect();
426 assert_eq!(members.len(), 2);
427 assert!(members.contains(&a.as_str()));
428 assert!(members.contains(&b.as_str()));
429 }
430
431 #[test]
432 fn cycles_ignores_non_import_edges() {
433 let (a, b) = (NodeId::new(), NodeId::new());
434 let edges = vec![
436 Edge::new(a.clone(), b.clone(), EdgeKind::Calls),
437 Edge::new(b.clone(), a.clone(), EdgeKind::Calls),
438 ];
439 assert!(find_import_cycles(&edges).unwrap().is_empty());
440 }
441
442 #[test]
443 fn cycles_detects_three_node_cycle() {
444 let (a, b, c) = (NodeId::new(), NodeId::new(), NodeId::new());
445 let edges = vec![
446 import_edge(&a, &b),
447 import_edge(&b, &c),
448 import_edge(&c, &a),
449 ];
450 let cycles = find_import_cycles(&edges).unwrap();
451 assert_eq!(cycles.len(), 1);
452 assert_eq!(cycles[0].len(), 3);
453 }
454}