impl DagBuilder {
fn process_relationships(&mut self, file: &FileContext) {
let file_module_id = self.normalize_path(&file.path);
let node = NodeInfo {
id: file_module_id.clone(),
label: self.extract_module_name(&file.path),
node_type: NodeType::Module,
file_path: file.path.clone(),
line_number: 0,
complexity: file
.complexity_metrics
.as_ref()
.map_or(1, |m| u32::from(m.total_complexity.cognitive)),
metadata: FxHashMap::default(),
};
self.add_node(self.enrich_node(node));
for item in &file.items {
self.process_single_relationship(item, &file_module_id);
}
}
fn process_single_relationship(&mut self, item: &AstItem, file_module_id: &str) {
match item {
AstItem::Use { path, line: _ } => {
self.process_use_import(path, file_module_id);
}
AstItem::Import {
module,
items,
alias: _,
line: _,
} => {
self.process_language_import(module, items, file_module_id);
}
AstItem::Impl {
type_name,
trait_name,
..
} => {
self.process_impl_relationship(type_name, trait_name);
}
_ => {}
}
}
fn process_use_import(&mut self, path: &str, file_module_id: &str) {
if let Some(target_id) = self.resolve_import_path(path) {
self.add_edge(Edge {
from: file_module_id.to_string(),
to: target_id,
edge_type: EdgeType::Imports,
weight: 1,
});
}
}
fn process_language_import(
&mut self,
module: &str,
items: &[String],
file_module_id: &str,
) {
if let Some(target_id) = self.resolve_import_path(module) {
self.add_edge(Edge {
from: file_module_id.to_string(),
to: target_id.clone(),
edge_type: EdgeType::Imports,
weight: 1,
});
}
for item in items {
let full_path = format!("{module}.{item}");
if let Some(target_id) = self.resolve_import_path(&full_path) {
self.add_edge(Edge {
from: file_module_id.to_string(),
to: target_id,
edge_type: EdgeType::Imports,
weight: 1,
});
}
}
}
fn process_impl_relationship(
&mut self,
type_name: &str,
trait_name: &Option<String>,
) {
if let (Some(trait_name), Some(struct_id)) =
(trait_name.as_ref(), self.type_map.get(type_name))
{
if let Some(trait_id) = self.type_map.get(trait_name) {
self.add_edge(Edge {
from: struct_id.clone(),
to: trait_id.clone(),
edge_type: EdgeType::Inherits,
weight: 1,
});
}
}
}
fn add_node(&mut self, node: NodeInfo) {
self.graph.add_node(node);
}
fn add_edge(&mut self, edge: Edge) {
self.graph.add_edge(edge);
}
fn enrich_node(&self, mut node: NodeInfo) -> NodeInfo {
let semantic_name = self.namer.get_semantic_name(&node.id, &node);
if semantic_name != node.id && !semantic_name.is_empty() {
node.label = semantic_name;
}
node.metadata
.insert("file_path".to_string(), node.file_path.clone());
node.metadata.insert(
"module_path".to_string(),
self.path_to_module(&node.file_path),
);
node.metadata
.insert("display_name".to_string(), node.label.clone());
node.metadata
.insert("node_type".to_string(), format!("{:?}", node.node_type));
node.metadata
.insert("line_number".to_string(), node.line_number.to_string());
node.metadata
.insert("complexity".to_string(), node.complexity.to_string());
let language = detect_language_from_path(&node.file_path);
node.metadata
.insert("language".to_string(), language.to_string());
node
}
fn normalize_path(&self, path: &str) -> String {
path.trim_start_matches("./")
.trim_start_matches('/')
.trim_end_matches(".rs")
.trim_end_matches(".ts")
.trim_end_matches(".py")
.trim_end_matches(".js")
.trim_end_matches(".tsx")
.trim_end_matches(".jsx")
.replace(['/', '.', '-'], "_")
}
fn path_to_module(&self, path: &str) -> String {
let ext = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let language = SemanticNamer::detect_language(ext);
let clean_path = path
.trim_start_matches("./")
.trim_start_matches('/')
.trim_start_matches("src/")
.trim_start_matches("lib/")
.trim_start_matches("app/");
let without_ext = std::path::Path::new(clean_path)
.with_extension("")
.to_string_lossy()
.into_owned();
let separator = match language {
"rust" => "::",
"python" => ".",
"typescript" | "javascript" => ".",
"go" => "/",
"java" => ".",
_ => "::",
};
without_ext.replace(['/', '\\'], separator)
}
fn extract_module_name(&self, path: &str) -> String {
std::path::Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(path)
.to_string()
}
fn resolve_import_path(&self, import_path: &str) -> Option<String> {
if let Some(type_id) = self.type_map.get(import_path) {
return Some(type_id.clone());
}
if let Some(func_id) = self.function_map.get(import_path) {
return Some(func_id.clone());
}
let parts: Vec<&str> = import_path.split("::").collect();
if let Some(last_part) = parts.last() {
if let Some(type_id) = self.type_map.get(*last_part) {
return Some(type_id.clone());
}
if let Some(func_id) = self.function_map.get(*last_part) {
return Some(func_id.clone());
}
}
Some(import_path.replace("::", "_"))
}
}
fn detect_language_from_path(file_path: &str) -> &'static str {
let ext = std::path::Path::new(file_path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"rs" => "rust",
"ts" | "tsx" => "typescript",
"js" | "jsx" => "javascript",
"py" => "python",
"go" => "go",
"java" => "java",
_ => "unknown",
}
}