use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use petgraph::stable_graph::NodeIndex;
use petgraph::visit::EdgeRef;
use crate::ingest::parser::{Entity, FileInsight, ImportStmt};
use crate::model::{EdgeKind, KnowledgeGraph, ModuleCluster, NodeKind};
#[derive(Debug, Clone)]
pub struct Chunk {
pub module_path: Vec<String>,
pub entities: Vec<Entity>,
pub imports: Vec<ImportStmt>,
pub dependencies: Vec<String>,
pub file_paths: Vec<PathBuf>,
pub entity_sources: Vec<PathBuf>,
}
impl Chunk {
pub fn is_empty(&self) -> bool {
self.entities.is_empty() && self.imports.is_empty()
}
pub fn entity_count(&self) -> usize {
self.entities.len()
}
}
pub fn build_node_to_file_map(graph: &KnowledgeGraph) -> HashMap<NodeIndex, PathBuf> {
let mut map = HashMap::new();
for n in graph.graph.node_indices() {
if let Some(w) = graph.graph.node_weight(n)
&& w.kind == NodeKind::File
&& let Some(ref fp) = w.file_path
{
map.insert(n, PathBuf::from(fp));
}
}
map
}
pub fn chunk_by_module(
insights: &[FileInsight],
modules: &[ModuleCluster],
graph: &KnowledgeGraph,
) -> Vec<Chunk> {
let node_to_file = build_node_to_file_map(graph);
let mut chunks = Vec::with_capacity(modules.len());
let module_node_ids: std::collections::HashMap<&str, HashSet<NodeIndex>> = modules
.iter()
.map(|m| (m.name.as_str(), m.node_ids.iter().copied().collect()))
.collect();
for module in modules {
let module_file_paths: HashSet<&PathBuf> = module
.node_ids
.iter()
.filter_map(|nid| node_to_file.get(nid))
.collect();
let mut entities = Vec::new();
let mut imports = Vec::new();
let mut file_paths = Vec::new();
let mut entity_sources = Vec::new();
for insight in insights {
if module_file_paths.contains(&insight.path) {
for _ in &insight.entities {
entity_sources.push(insight.path.clone());
}
entities.extend(insight.entities.clone());
imports.extend(insight.imports.clone());
if !file_paths.contains(&insight.path) {
file_paths.push(insight.path.clone());
}
}
}
let mut paired: Vec<(Entity, PathBuf)> = entities
.into_iter()
.zip(entity_sources)
.collect();
paired.sort_by(|a, b| a.0.name.cmp(&b.0.name));
let before = paired.len();
paired.dedup_by(|a, b| a.0.name == b.0.name);
if paired.len() < before {
tracing::warn!(
"模块 {} 去重 {} 个同名实体(保留排序后首个定义)",
module.name,
before - paired.len()
);
}
let entities: Vec<Entity> = paired.iter().map(|(e, _)| e.clone()).collect();
let entity_sources: Vec<PathBuf> = paired.iter().map(|(_, f)| f.clone()).collect();
let module_path: Vec<String> = module.name.split("::").map(|s| s.to_string()).collect();
let mut deps: Vec<String> = Vec::new();
for (&other_name, other_set) in &module_node_ids {
if other_name == module.name {
continue;
}
let has_dep = module.node_ids.iter().any(|nid| {
graph.graph.edges(*nid).any(|e| {
let kind = &graph.graph[e.id()].kind;
kind == &EdgeKind::Imports && other_set.contains(&e.target())
})
});
if has_dep {
deps.push(other_name.to_string());
}
}
deps.sort();
chunks.push(Chunk {
module_path,
entities,
imports,
dependencies: deps,
file_paths,
entity_sources,
});
}
chunks
}
pub fn chunk_by_file(insight: &FileInsight) -> Chunk {
let module_path: Vec<String> = insight
.path
.parent()
.and_then(|p| {
p.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.map(|c| c.as_os_str().to_string_lossy().to_string())
.reduce(|a, b| format!("{}::{}", a, b))
})
.map(|s| s.split("::").map(|p| p.to_string()).collect())
.unwrap_or_default();
Chunk {
module_path,
entities: insight.entities.clone(),
imports: insight.imports.clone(),
dependencies: Vec::new(),
file_paths: vec![insight.path.clone()],
entity_sources: insight.entities.iter().map(|_| insight.path.clone()).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ingest::parser::Entity;
use crate::model::KnowledgeGraph;
fn make_entity(name: &str, kind: &str) -> Entity {
Entity {
name: name.to_string(),
kind: kind.to_string(),
line_start: 1,
line_end: 10,
doc_comment: None,
signature: None, visibility: None,
}
}
fn make_insight(file_name: &str, entities: Vec<Entity>) -> FileInsight {
FileInsight {
path: PathBuf::from(file_name),
language: "rust".into(),
entities,
imports: Vec::new(),
doc_comments: Vec::new(),
source: String::new(),
}
}
#[test]
fn test_chunk_by_file() {
let entity = make_entity("MyStruct", "struct");
let insight = make_insight("src/lib.rs", vec![entity]);
let chunk = chunk_by_file(&insight);
assert_eq!(chunk.entity_count(), 1);
assert!(!chunk.module_path.is_empty());
}
#[test]
fn test_empty_chunk() {
let entities = vec![make_entity("Foo", "fn")];
let insight = make_insight("src/main.rs", entities);
let chunk = chunk_by_file(&insight);
assert!(!chunk.is_empty());
let empty_chunk = Chunk {
module_path: vec![],
entities: vec![],
imports: vec![],
dependencies: vec![],
file_paths: vec![],
entity_sources: vec![],
};
assert!(empty_chunk.is_empty());
}
#[test]
fn test_chunk_by_module_empty_modules() {
let graph = KnowledgeGraph::default();
let insight = make_insight("src/lib.rs", vec![make_entity("Foo", "fn")]);
let chunks = chunk_by_module(&[insight], &[], &graph);
assert!(chunks.is_empty());
}
#[test]
fn test_build_node_to_file_map() {
let mut g = petgraph::stable_graph::StableDiGraph::<
crate::model::CodeNode,
crate::model::CodeEdge,
>::new();
let file_id = g.add_node(crate::model::CodeNode {
id: petgraph::stable_graph::NodeIndex::new(0),
kind: crate::model::NodeKind::File,
name: "lib.rs".into(),
file_path: Some("src/lib.rs".into()),
line_range: None,
doc_comment: None,
signature: None, visibility: None,
module_path: vec!["src".into()],
});
let fn_id = g.add_node(crate::model::CodeNode {
id: petgraph::stable_graph::NodeIndex::new(1),
kind: crate::model::NodeKind::Function,
name: "foo".into(),
file_path: Some("src/lib.rs".into()),
line_range: None,
doc_comment: None,
signature: None, visibility: None,
module_path: vec!["src".into(), "lib".into()],
});
let kg = KnowledgeGraph {
graph: g,
modules: vec![],
features: Vec::new(),
};
let map = build_node_to_file_map(&kg);
assert_eq!(map.len(), 1); assert!(map.contains_key(&file_id));
assert!(!map.contains_key(&fn_id));
assert_eq!(map[&file_id], std::path::PathBuf::from("src/lib.rs"));
}
#[test]
fn test_chunk_by_module_groups_entities_by_module() {
let mut graph = KnowledgeGraph::default();
let file_a = graph.graph.add_node(crate::model::CodeNode {
id: petgraph::stable_graph::NodeIndex::new(0),
kind: NodeKind::File,
name: "file_a.rs".into(),
file_path: Some("src/a/file_a.rs".into()),
line_range: None,
doc_comment: None,
signature: None, visibility: None,
module_path: vec!["src".into(), "a".into()],
});
let e1 = graph.graph.add_node(crate::model::CodeNode {
id: petgraph::stable_graph::NodeIndex::new(1),
kind: NodeKind::Function,
name: "e1".into(),
file_path: Some("src/a/file_a.rs".into()),
line_range: Some((1, 5)),
doc_comment: None,
signature: Some("fn e1()".into()), visibility: None,
module_path: vec!["src".into(), "a".into()],
});
let file_b = graph.graph.add_node(crate::model::CodeNode {
id: petgraph::stable_graph::NodeIndex::new(2),
kind: NodeKind::File,
name: "file_b.rs".into(),
file_path: Some("src/b/file_b.rs".into()),
line_range: None,
doc_comment: None,
signature: None, visibility: None,
module_path: vec!["src".into(), "b".into()],
});
let e2 = graph.graph.add_node(crate::model::CodeNode {
id: petgraph::stable_graph::NodeIndex::new(3),
kind: NodeKind::Function,
name: "e2".into(),
file_path: Some("src/b/file_b.rs".into()),
line_range: Some((1, 5)),
doc_comment: None,
signature: Some("fn e2()".into()), visibility: None,
module_path: vec!["src".into(), "b".into()],
});
graph.graph.add_edge(file_a, e1, crate::model::CodeEdge {
id: petgraph::stable_graph::EdgeIndex::new(0),
kind: EdgeKind::Contains,
source: file_a,
target: e1,
weight: 1.0,
location: None,
});
graph.graph.add_edge(file_b, e2, crate::model::CodeEdge {
id: petgraph::stable_graph::EdgeIndex::new(1),
kind: EdgeKind::Contains,
source: file_b,
target: e2,
weight: 1.0,
location: None,
});
graph.graph.add_edge(file_a, file_b, crate::model::CodeEdge {
id: petgraph::stable_graph::EdgeIndex::new(2),
kind: EdgeKind::Imports,
source: file_a,
target: file_b,
weight: 1.0,
location: None,
});
let modules = vec![
ModuleCluster { name: "src::a".into(), node_ids: vec![file_a, e1], cohesion: 0.9, coupling: 0.1, description: None },
ModuleCluster { name: "src::b".into(), node_ids: vec![file_b, e2], cohesion: 0.9, coupling: 0.1, description: None },
];
let insights = vec![
make_insight("src/a/file_a.rs", vec![make_entity("e1", "fn")]),
make_insight("src/b/file_b.rs", vec![make_entity("e2", "fn")]),
];
let chunks = chunk_by_module(&insights, &modules, &graph);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].module_path, vec!["src".to_string(), "a".to_string()]);
assert_eq!(chunks[1].module_path, vec!["src".to_string(), "b".to_string()]);
assert_eq!(chunks[0].entities.len(), 1);
assert_eq!(chunks[0].entities[0].name, "e1");
assert_eq!(chunks[1].entities[0].name, "e2");
assert_eq!(chunks[0].dependencies, vec!["src::b".to_string()]);
assert!(chunks[1].dependencies.is_empty());
assert_eq!(chunks[0].entity_sources, vec![std::path::PathBuf::from("src/a/file_a.rs")]);
assert_eq!(chunks[1].entity_sources, vec![std::path::PathBuf::from("src/b/file_b.rs")]);
}
}