use std::collections::HashSet;
use petgraph::visit::{EdgeRef, IntoEdgeReferences};
use crate::model::*;
use super::community::{community_name, detect_communities};
pub struct ModuleDetector<'a> {
graph: &'a KnowledgeGraph,
}
impl<'a> ModuleDetector<'a> {
pub fn new(graph: &'a KnowledgeGraph) -> Self {
Self { graph }
}
pub fn detect(&self) -> Vec<ModuleCluster> {
let communities = detect_communities(self.graph);
let mut clusters: Vec<ModuleCluster> = Vec::with_capacity(communities.len());
let mut used_names: HashSet<String> = HashSet::new();
for (idx, community) in communities.iter().enumerate() {
let mut file_paths: Vec<String> = community
.iter()
.filter_map(|nid| {
self.graph
.graph
.node_weight(*nid)
.and_then(|n| n.file_path.clone())
})
.collect();
file_paths.sort();
let mut name = community_name(&file_paths, idx);
if used_names.contains(&name) {
if let Some(stem) = file_stem(&file_paths) {
let alt = format!("{name}::{stem}");
if !used_names.contains(&alt) {
name = alt;
} else {
name = format!("module_{idx}");
}
} else {
name = format!("module_{idx}");
}
}
used_names.insert(name.clone());
let cohesion = self.calculate_cohesion(community);
let coupling = self.calculate_coupling(community);
let file_set: HashSet<NodeId> = community.iter().copied().collect();
let mut expanded: HashSet<NodeId> = file_set.clone();
for edge in self.graph.graph.edge_references() {
let kind = self.graph.graph.edge_weight(edge.id()).map(|e| e.kind.clone());
if kind == Some(EdgeKind::Contains) && file_set.contains(&edge.source()) {
expanded.insert(edge.target());
}
}
let mut unique: Vec<NodeId> = expanded.into_iter().collect();
unique.sort();
clusters.push(ModuleCluster {
name,
node_ids: unique,
cohesion,
coupling,
description: None,
});
}
clusters
}
fn calculate_cohesion(&self, node_ids: &[NodeId]) -> f64 {
let (internal, external) = self.count_edges(node_ids);
let total = internal + external;
if total == 0.0 {
return 0.0;
}
internal / total
}
fn calculate_coupling(&self, node_ids: &[NodeId]) -> f64 {
let (internal, external) = self.count_edges(node_ids);
let total = internal + external;
if total == 0.0 {
return 0.0;
}
external / total
}
fn count_edges(&self, node_ids: &[NodeId]) -> (f64, f64) {
let file_set: HashSet<NodeId> = node_ids.iter().copied().collect();
let mut set: HashSet<NodeId> = file_set.clone();
for edge in self.graph.graph.edge_references() {
let kind = self.graph.graph.edge_weight(edge.id()).map(|e| e.kind.clone());
if kind == Some(EdgeKind::Contains) && file_set.contains(&edge.source()) {
set.insert(edge.target());
}
}
let mut internal = 0.0;
let mut external = 0.0;
for edge in self.graph.graph.edge_references() {
let kind = self.graph.graph.edge_weight(edge.id()).map(|e| e.kind.clone());
if kind == Some(EdgeKind::Contains) {
continue;
}
let s = edge.source();
let t = edge.target();
let in_s = set.contains(&s);
let in_t = set.contains(&t);
if in_s && in_t {
internal += 1.0;
} else if in_s || in_t {
external += 1.0;
}
}
(internal, external)
}
}
fn file_stem(files: &[String]) -> Option<String> {
files
.first()
.and_then(|p| std::path::Path::new(p).file_stem())
.map(|s| s.to_string_lossy().into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_small_graph() -> KnowledgeGraph {
let mut kg = KnowledgeGraph::default();
let g = &mut kg.graph;
let p = g.add_node(CodeNode {
id: NodeId::new(0), kind: NodeKind::Project, name: "p".into(),
file_path: None, line_range: None, doc_comment: None,
signature: None, module_path: vec![], visibility: None,
});
let m = g.add_node(CodeNode {
id: NodeId::new(1), kind: NodeKind::Module, name: "m".into(),
file_path: None, line_range: None, doc_comment: None,
signature: None, module_path: vec!["src".into()], visibility: None,
});
let f1 = g.add_node(CodeNode {
id: NodeId::new(2), kind: NodeKind::File, name: "a.rs".into(),
file_path: Some("src/a.rs".into()), line_range: None, doc_comment: None,
signature: None, module_path: vec!["src".into()], visibility: None,
});
let f2 = g.add_node(CodeNode {
id: NodeId::new(3), kind: NodeKind::File, name: "b.rs".into(),
file_path: Some("src/b.rs".into()), line_range: None, doc_comment: None,
signature: None, module_path: vec!["src".into()], visibility: None,
});
let e1 = g.add_node(CodeNode {
id: NodeId::new(4), kind: NodeKind::Function, name: "foo".into(),
file_path: Some("src/a.rs".into()), line_range: None, doc_comment: None,
signature: None, module_path: vec!["src".into(), "a".into()], visibility: None,
});
let e2 = g.add_node(CodeNode {
id: NodeId::new(5), kind: NodeKind::Function, name: "bar".into(),
file_path: Some("src/b.rs".into()), line_range: None, doc_comment: None,
signature: None, module_path: vec!["src".into(), "b".into()], visibility: None,
});
for (src, tgt) in &[(p, m), (m, f1), (m, f2), (f1, e1), (f2, e2)] {
g.add_edge(*src, *tgt, CodeEdge {
id: EdgeId::new(g.edge_count()), kind: EdgeKind::Contains,
source: *src, target: *tgt, weight: 1.0, location: None,
});
}
g.add_edge(e1, e2, CodeEdge {
id: EdgeId::new(g.edge_count()), kind: EdgeKind::Calls,
source: e1, target: e2, weight: 0.7, location: None,
});
kg
}
#[test]
fn test_cohesion() {
let kg = make_small_graph();
let detector = ModuleDetector::new(&kg);
let ids = vec![NodeId::new(2), NodeId::new(3)];
let c = detector.calculate_cohesion(&ids);
let expected = 1.0;
assert!((c - expected).abs() < 1e-6);
}
#[test]
fn test_coupling() {
let mut kg = make_small_graph();
kg.graph.add_edge(
NodeId::new(4), NodeId::new(5),
CodeEdge {
id: EdgeId::new(kg.graph.edge_count()), kind: EdgeKind::Calls,
source: NodeId::new(4), target: NodeId::new(5), weight: 0.5, location: None,
},
);
let detector = ModuleDetector::new(&kg);
let ids = vec![NodeId::new(2)]; let coupling = detector.calculate_coupling(&ids);
dbg!(coupling);
assert!((coupling - 1.0).abs() < 1e-6);
}
#[test]
fn test_detect() {
let kg = make_small_graph();
let detector = ModuleDetector::new(&kg);
let clusters = detector.detect();
assert_eq!(clusters.len(), 1, "应检出 src 模块,实际: {:?}", clusters.iter().map(|c| &c.name).collect::<Vec<_>>());
assert_eq!(clusters[0].name, "src");
assert_eq!(clusters[0].node_ids.len(), 4, "模块应包含 2 文件 + 2 实体节点");
let kinds: Vec<_> = clusters[0]
.node_ids
.iter()
.map(|nid| kg.graph.node_weight(*nid).unwrap().kind.clone())
.collect();
assert_eq!(
kinds.iter().filter(|k| **k == NodeKind::File).count(),
2,
"应含 2 个文件节点: {:?}",
kinds
);
assert_eq!(
kinds.iter().filter(|k| **k == NodeKind::Function).count(),
2,
"应含 2 个实体节点: {:?}",
kinds
);
}
#[test]
fn test_detect_multiple_directories() {
let mut kg = KnowledgeGraph::default();
let g = &mut kg.graph;
let add_file = |g: &mut petgraph::stable_graph::StableDiGraph<CodeNode, CodeEdge>, id: usize, path: &str, segs: Vec<&str>| -> (NodeId, NodeId) {
let nid = g.add_node(CodeNode {
id: NodeId::new(id), kind: NodeKind::File, name: path.into(),
file_path: Some(path.into()), line_range: None, doc_comment: None,
signature: None, module_path: segs.iter().map(|s| s.to_string()).collect(), visibility: None,
});
let eid = g.add_node(CodeNode {
id: NodeId::new(id + 100), kind: NodeKind::Function, name: format!("f{id}"),
file_path: Some(path.into()), line_range: None, doc_comment: None,
signature: None, module_path: segs.iter().map(|s| s.to_string()).collect(), visibility: None,
});
g.add_edge(nid, eid, CodeEdge {
id: EdgeId::new(g.edge_count()), kind: EdgeKind::Contains,
source: nid, target: eid, weight: 1.0, location: None,
});
(nid, eid)
};
let (_tcp, etcp) = add_file(g, 0, "src/net/tcp.rs", vec!["src", "net"]);
let (_udp, eudp) = add_file(g, 1, "src/net/udp.rs", vec!["src", "net"]);
let _server = add_file(g, 2, "src/http/server.rs", vec!["src", "http"]);
let _client = add_file(g, 3, "src/http/client.rs", vec!["src", "http"]);
g.add_edge(etcp, eudp, CodeEdge {
id: EdgeId::new(g.edge_count()), kind: EdgeKind::Calls,
source: etcp, target: eudp, weight: 0.7, location: None,
});
let detector = ModuleDetector::new(&kg);
let clusters = detector.detect();
let names: Vec<&str> = clusters.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"src::net"),
"应检出 src::net 社区,实际: {names:?}"
);
assert!(
names.contains(&"src::http"),
"应检出 src::http 单文件社区,实际: {names:?}"
);
let unique: std::collections::HashSet<&str> = names.iter().copied().collect();
assert_eq!(unique.len(), names.len(), "模块名必须唯一: {names:?}");
let net = clusters.iter().find(|c| c.name == "src::net").unwrap();
assert_eq!(net.node_ids.len(), 4, "src::net 应含 2 文件 + 2 实体");
}
}