context_creator/core/semantic/
graph_builder.rs

1//! Graph construction module for semantic analysis
2//!
3//! This module is responsible for building dependency graphs from file information.
4//! It follows the Single Responsibility Principle by focusing solely on graph construction.
5
6use crate::core::semantic::dependency_types::{
7    DependencyEdgeType, DependencyNode as RichNode, FileAnalysisResult,
8};
9use crate::core::walker::FileInfo;
10use anyhow::Result;
11use petgraph::graph::{DiGraph, NodeIndex};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15/// Builder for constructing dependency graphs
16pub struct GraphBuilder {
17    // Future: Could add configuration options here
18}
19
20impl GraphBuilder {
21    /// Create a new GraphBuilder
22    pub fn new() -> Self {
23        Self {}
24    }
25
26    /// Build a dependency graph from file information
27    pub fn build(
28        &self,
29        files: &[FileInfo],
30    ) -> Result<(
31        DiGraph<RichNode, DependencyEdgeType>,
32        HashMap<PathBuf, NodeIndex>,
33    )> {
34        let mut graph = DiGraph::new();
35        let mut node_map = HashMap::new();
36
37        // Create nodes for each file
38        for (index, file) in files.iter().enumerate() {
39            let rich_node = RichNode {
40                file_index: index,
41                path: file.path.clone(),
42                language: Self::detect_language(&file.path),
43                content_hash: None, // Will be filled during analysis
44                file_size: file.size,
45                depth: 0,
46            };
47
48            let node_idx = graph.add_node(rich_node);
49            // Only store the last occurrence if there are duplicates
50            node_map.insert(file.path.clone(), node_idx);
51        }
52
53        Ok((graph, node_map))
54    }
55
56    /// Add a dependency edge to the graph
57    pub fn add_edge(
58        &self,
59        graph: &mut DiGraph<RichNode, DependencyEdgeType>,
60        from: NodeIndex,
61        to: NodeIndex,
62        edge_type: DependencyEdgeType,
63    ) {
64        // Avoid self-loops
65        if from != to {
66            graph.add_edge(from, to, edge_type);
67        }
68    }
69
70    /// Build edges from file import information
71    pub fn build_edges_from_imports(
72        &self,
73        graph: &mut DiGraph<RichNode, DependencyEdgeType>,
74        files: &[FileInfo],
75        node_map: &HashMap<PathBuf, NodeIndex>,
76    ) {
77        for file in files {
78            if let Some(&from_idx) = node_map.get(&file.path) {
79                for import_path in &file.imports {
80                    if let Some(&to_idx) = node_map.get(import_path) {
81                        let edge_type = DependencyEdgeType::Import {
82                            symbols: Vec::new(), // Basic import without symbol information
83                        };
84                        self.add_edge(graph, from_idx, to_idx, edge_type);
85                    }
86                }
87            }
88        }
89    }
90
91    /// Build edges from parallel analysis results
92    pub fn build_edges_from_analysis(
93        &self,
94        graph: &mut DiGraph<RichNode, DependencyEdgeType>,
95        analysis_results: &[FileAnalysisResult],
96        path_to_index: &HashMap<PathBuf, usize>,
97        node_map: &HashMap<PathBuf, NodeIndex>,
98    ) {
99        for result in analysis_results {
100            let file_index = result.file_index;
101
102            // Find the source node
103            let source_path = path_to_index
104                .iter()
105                .find(|(_, &idx)| idx == file_index)
106                .map(|(path, _)| path.clone());
107
108            if let Some(source_path) = source_path {
109                if let Some(&from_idx) = node_map.get(&source_path) {
110                    // Update node with content hash
111                    if let Some(hash) = result.content_hash {
112                        graph[from_idx].content_hash = Some(hash);
113                    }
114
115                    // Add import edges
116                    for (import_path, edge_type) in &result.imports {
117                        // Try to find the target in our node map
118                        for (path, &to_idx) in node_map {
119                            if path
120                                .to_string_lossy()
121                                .contains(&import_path.to_string_lossy().to_string())
122                            {
123                                self.add_edge(graph, from_idx, to_idx, edge_type.clone());
124                                break;
125                            }
126                        }
127                    }
128                }
129            }
130        }
131    }
132
133    /// Detect programming language from file extension
134    fn detect_language(path: &Path) -> Option<String> {
135        path.extension()
136            .and_then(|ext| ext.to_str())
137            .map(|ext| match ext {
138                "rs" => "rust",
139                "py" => "python",
140                "js" | "mjs" => "javascript",
141                "ts" | "tsx" => "typescript",
142                "jsx" => "javascript",
143                "go" => "go",
144                "java" => "java",
145                "cpp" | "cc" | "cxx" => "cpp",
146                "c" => "c",
147                "rb" => "ruby",
148                "php" => "php",
149                "swift" => "swift",
150                "kt" => "kotlin",
151                "scala" => "scala",
152                "r" => "r",
153                "sh" | "bash" => "shell",
154                "yaml" | "yml" => "yaml",
155                "json" => "json",
156                "xml" => "xml",
157                "html" | "htm" => "html",
158                "css" | "scss" | "sass" => "css",
159                _ => ext,
160            })
161            .map(String::from)
162    }
163}
164
165impl Default for GraphBuilder {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171#[cfg(test)]
172#[path = "graph_builder_tests.rs"]
173mod tests;