use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
error::GitCortexError,
schema::{CodeSmell, DesignPattern, EdgeConfidence, EdgeKind, NodeKind, SolidHint, Visibility},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(Uuid);
impl NodeId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn as_str(&self) -> String {
self.0.to_string()
}
}
impl Default for NodeId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl TryFrom<&str> for NodeId {
type Error = GitCortexError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Uuid::parse_str(s)
.map(NodeId)
.map_err(|e| GitCortexError::Store(format!("invalid NodeId '{s}': {e}")))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
pub start_line: u32,
pub end_line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct LldLabels {
pub solid_hints: Vec<SolidHint>,
pub patterns: Vec<DesignPattern>,
pub smells: Vec<CodeSmell>,
pub complexity: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DefinitionText {
pub signature: String,
pub body: String,
pub doc_comment: Option<String>,
pub start_byte: u32,
pub end_byte: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct NodeMetadata {
pub loc: u32,
pub visibility: Visibility,
pub is_async: bool,
pub is_unsafe: bool,
pub is_static: bool,
pub is_abstract: bool,
pub is_final: bool,
pub is_property: bool,
pub is_generator: bool,
pub is_const: bool,
pub generic_bounds: Vec<String>,
pub annotations: Vec<String>,
pub lld: LldLabels,
pub definition: DefinitionText,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Node {
pub id: NodeId,
pub kind: NodeKind,
pub name: String,
pub qualified_name: String,
pub file: PathBuf,
pub span: Span,
pub metadata: NodeMetadata,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edge {
pub src: NodeId,
pub dst: NodeId,
pub kind: EdgeKind,
#[serde(default)]
pub line: Option<u32>,
#[serde(default)]
pub confidence: EdgeConfidence,
}
impl Edge {
pub fn new(src: NodeId, dst: NodeId, kind: EdgeKind) -> Self {
Self {
src,
dst,
kind,
line: None,
confidence: EdgeConfidence::Extracted,
}
}
pub fn call(src: NodeId, dst: NodeId, line: u32) -> Self {
Self {
src,
dst,
kind: EdgeKind::Calls,
line: Some(line),
confidence: EdgeConfidence::Extracted,
}
}
pub fn with_confidence(mut self, confidence: EdgeConfidence) -> Self {
self.confidence = confidence;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct GraphDiff {
pub added_nodes: Vec<Node>,
pub removed_node_ids: Vec<NodeId>,
pub removed_files: Vec<PathBuf>,
pub added_edges: Vec<Edge>,
pub removed_edges: Vec<(NodeId, NodeId, EdgeKind)>,
pub deferred_calls: Vec<(NodeId, String, u32)>,
pub deferred_uses: Vec<(NodeId, String)>,
pub deferred_implements: Vec<(NodeId, String)>,
pub deferred_inherits: Vec<(NodeId, String)>,
pub deferred_throws: Vec<(NodeId, String)>,
pub deferred_annotated: Vec<(NodeId, String)>,
pub deferred_doc_refs: Vec<(NodeId, String)>,
}
impl GraphDiff {
pub fn is_empty(&self) -> bool {
self.added_nodes.is_empty()
&& self.removed_node_ids.is_empty()
&& self.removed_files.is_empty()
&& self.added_edges.is_empty()
&& self.removed_edges.is_empty()
&& self.deferred_calls.is_empty()
&& self.deferred_uses.is_empty()
&& self.deferred_implements.is_empty()
&& self.deferred_inherits.is_empty()
&& self.deferred_throws.is_empty()
&& self.deferred_annotated.is_empty()
&& self.deferred_doc_refs.is_empty()
}
pub fn merge(&mut self, other: GraphDiff) {
self.added_nodes.extend(other.added_nodes);
self.removed_node_ids.extend(other.removed_node_ids);
self.removed_files.extend(other.removed_files);
self.added_edges.extend(other.added_edges);
self.removed_edges.extend(other.removed_edges);
self.deferred_calls.extend(other.deferred_calls);
self.deferred_uses.extend(other.deferred_uses);
self.deferred_implements.extend(other.deferred_implements);
self.deferred_inherits.extend(other.deferred_inherits);
self.deferred_throws.extend(other.deferred_throws);
self.deferred_annotated.extend(other.deferred_annotated);
self.deferred_doc_refs.extend(other.deferred_doc_refs);
}
}
pub fn in_degree_by_calls(edges: &[Edge]) -> HashMap<String, u32> {
let mut in_degree: HashMap<String, u32> = HashMap::new();
for e in edges {
if matches!(e.kind, EdgeKind::Calls) {
*in_degree.entry(e.dst.as_str()).or_insert(0) += 1;
}
}
in_degree
}
pub fn find_import_cycles(edges: &[Edge]) -> Result<Vec<Vec<String>>, GitCortexError> {
let mut adj: HashMap<String, Vec<String>> = HashMap::new();
for e in edges {
if matches!(e.kind, EdgeKind::Imports) {
adj.entry(e.src.as_str()).or_default().push(e.dst.as_str());
}
}
let nodes: Vec<String> = adj.keys().cloned().collect();
let mut index_counter = 0usize;
let mut stack: Vec<String> = Vec::new();
let mut on_stack: HashMap<String, bool> = HashMap::new();
let mut index: HashMap<String, usize> = HashMap::new();
let mut lowlink: HashMap<String, usize> = HashMap::new();
let mut result: Vec<Vec<String>> = Vec::new();
#[allow(clippy::too_many_arguments)]
fn strongconnect(
v: &str,
adj: &HashMap<String, Vec<String>>,
counter: &mut usize,
stack: &mut Vec<String>,
on_stack: &mut HashMap<String, bool>,
index: &mut HashMap<String, usize>,
lowlink: &mut HashMap<String, usize>,
result: &mut Vec<Vec<String>>,
) -> Result<(), GitCortexError> {
index.insert(v.to_owned(), *counter);
lowlink.insert(v.to_owned(), *counter);
*counter += 1;
stack.push(v.to_owned());
on_stack.insert(v.to_owned(), true);
if let Some(neighbours) = adj.get(v) {
for w in neighbours.iter() {
if !index.contains_key(w.as_str()) {
strongconnect(w, adj, counter, stack, on_stack, index, lowlink, result)?;
let ll_w = lowlink[w.as_str()];
let ll_v = lowlink[v];
lowlink.insert(v.to_owned(), ll_v.min(ll_w));
} else if *on_stack.get(w.as_str()).unwrap_or(&false) {
let idx_w = index[w.as_str()];
let ll_v = lowlink[v];
lowlink.insert(v.to_owned(), ll_v.min(idx_w));
}
}
}
if lowlink[v] == index[v] {
let mut scc: Vec<String> = Vec::new();
loop {
let w = stack.pop().ok_or_else(|| {
GitCortexError::Store("SCC stack underflow: Tarjan invariant violated".into())
})?;
on_stack.insert(w.clone(), false);
scc.push(w.clone());
if w == v {
break;
}
}
if scc.len() > 1 {
result.push(scc);
}
}
Ok(())
}
for v in &nodes {
if !index.contains_key(v.as_str()) {
strongconnect(
v,
&adj,
&mut index_counter,
&mut stack,
&mut on_stack,
&mut index,
&mut lowlink,
&mut result,
)?;
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_id_is_unique() {
let a = NodeId::new();
let b = NodeId::new();
assert_ne!(a, b);
}
#[test]
fn graph_diff_merge() {
let node = Node {
id: NodeId::new(),
kind: NodeKind::Function,
name: "foo".into(),
qualified_name: "crate::foo".into(),
file: PathBuf::from("src/lib.rs"),
span: Span {
start_line: 1,
end_line: 3,
},
metadata: NodeMetadata::default(),
};
let mut base = GraphDiff::default();
let other = GraphDiff {
added_nodes: vec![node],
..Default::default()
};
base.merge(other);
assert_eq!(base.added_nodes.len(), 1);
}
#[test]
fn graph_diff_is_empty_on_default() {
assert!(GraphDiff::default().is_empty());
}
fn import_edge(src: &NodeId, dst: &NodeId) -> Edge {
Edge::new(src.clone(), dst.clone(), EdgeKind::Imports)
}
#[test]
fn cycles_empty_when_imports_are_acyclic() {
let (a, b, c) = (NodeId::new(), NodeId::new(), NodeId::new());
let edges = vec![import_edge(&a, &b), import_edge(&b, &c)];
assert!(find_import_cycles(&edges).unwrap().is_empty());
}
#[test]
fn cycles_detects_two_node_cycle() {
let (a, b) = (NodeId::new(), NodeId::new());
let edges = vec![import_edge(&a, &b), import_edge(&b, &a)];
let cycles = find_import_cycles(&edges).unwrap();
assert_eq!(cycles.len(), 1);
let members: std::collections::HashSet<&String> = cycles[0].iter().collect();
assert_eq!(members.len(), 2);
assert!(members.contains(&a.as_str()));
assert!(members.contains(&b.as_str()));
}
#[test]
fn cycles_ignores_non_import_edges() {
let (a, b) = (NodeId::new(), NodeId::new());
let edges = vec![
Edge::new(a.clone(), b.clone(), EdgeKind::Calls),
Edge::new(b.clone(), a.clone(), EdgeKind::Calls),
];
assert!(find_import_cycles(&edges).unwrap().is_empty());
}
#[test]
fn cycles_detects_three_node_cycle() {
let (a, b, c) = (NodeId::new(), NodeId::new(), NodeId::new());
let edges = vec![
import_edge(&a, &b),
import_edge(&b, &c),
import_edge(&c, &a),
];
let cycles = find_import_cycles(&edges).unwrap();
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].len(), 3);
}
}