codegraph_rust/
parser_impl.rs

1//! Implementation of the CodeParser trait for Rust
2//!
3//! This module provides the RustParser struct that implements the
4//! codegraph-parser-api::CodeParser trait for parsing Rust source files.
5
6use codegraph::CodeGraph;
7use codegraph_parser_api::{
8    CodeIR, CodeParser, FileInfo, ParserConfig, ParserError, ParserMetrics, ProjectInfo,
9};
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13use std::time::{Duration, Instant};
14
15use crate::extractor;
16use crate::mapper;
17
18/// Rust language parser implementing the CodeParser trait
19pub struct RustParser {
20    config: ParserConfig,
21    metrics: Mutex<ParserMetrics>,
22}
23
24impl RustParser {
25    /// Create a new Rust parser with default configuration
26    pub fn new() -> Self {
27        Self {
28            config: ParserConfig::default(),
29            metrics: Mutex::new(ParserMetrics::default()),
30        }
31    }
32
33    /// Create a new Rust parser with custom configuration
34    pub fn with_config(config: ParserConfig) -> Self {
35        Self {
36            config,
37            metrics: Mutex::new(ParserMetrics::default()),
38        }
39    }
40
41    /// Update metrics after parsing a file
42    fn update_metrics(
43        &self,
44        success: bool,
45        duration: Duration,
46        entities: usize,
47        relationships: usize,
48    ) {
49        let mut metrics = self.metrics.lock().unwrap();
50        metrics.files_attempted += 1;
51        if success {
52            metrics.files_succeeded += 1;
53        } else {
54            metrics.files_failed += 1;
55        }
56        metrics.total_parse_time += duration;
57        metrics.total_entities += entities;
58        metrics.total_relationships += relationships;
59    }
60
61    /// Convert CodeIR to graph nodes and return FileInfo
62    fn ir_to_graph(
63        &self,
64        ir: &CodeIR,
65        graph: &mut CodeGraph,
66        file_path: &Path,
67    ) -> Result<FileInfo, ParserError> {
68        mapper::ir_to_graph(ir, graph, file_path)
69    }
70}
71
72impl Default for RustParser {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl CodeParser for RustParser {
79    fn language(&self) -> &str {
80        "rust"
81    }
82
83    fn file_extensions(&self) -> &[&str] {
84        &[".rs"]
85    }
86
87    fn parse_file(&self, path: &Path, graph: &mut CodeGraph) -> Result<FileInfo, ParserError> {
88        let start = Instant::now();
89
90        // Check file size
91        let metadata =
92            fs::metadata(path).map_err(|e| ParserError::IoError(path.to_path_buf(), e))?;
93
94        if metadata.len() as usize > self.config.max_file_size {
95            return Err(ParserError::FileTooLarge(
96                path.to_path_buf(),
97                metadata.len() as usize,
98            ));
99        }
100
101        // Read file
102        let source =
103            fs::read_to_string(path).map_err(|e| ParserError::IoError(path.to_path_buf(), e))?;
104
105        // Parse source
106        let result = self.parse_source(&source, path, graph);
107
108        // Update metrics
109        let duration = start.elapsed();
110        if let Ok(ref info) = result {
111            self.update_metrics(true, duration, info.entity_count(), 0);
112        } else {
113            self.update_metrics(false, duration, 0, 0);
114        }
115
116        result
117    }
118
119    fn parse_source(
120        &self,
121        source: &str,
122        file_path: &Path,
123        graph: &mut CodeGraph,
124    ) -> Result<FileInfo, ParserError> {
125        let start = Instant::now();
126
127        // Extract entities and relationships into IR
128        let ir = extractor::extract(source, file_path, &self.config)?;
129
130        // Insert IR into graph
131        let mut file_info = self.ir_to_graph(&ir, graph, file_path)?;
132
133        // Add timing and size information
134        file_info.parse_time = start.elapsed();
135        file_info.line_count = source.lines().count();
136        file_info.byte_count = source.len();
137
138        Ok(file_info)
139    }
140
141    fn config(&self) -> &ParserConfig {
142        &self.config
143    }
144
145    fn metrics(&self) -> ParserMetrics {
146        self.metrics.lock().unwrap().clone()
147    }
148
149    fn reset_metrics(&mut self) {
150        *self.metrics.lock().unwrap() = ParserMetrics::default();
151    }
152
153    fn parse_files(
154        &self,
155        paths: &[PathBuf],
156        graph: &mut CodeGraph,
157    ) -> Result<ProjectInfo, ParserError> {
158        if self.config.parallel {
159            self.parse_files_parallel(paths, graph)
160        } else {
161            self.parse_files_sequential(paths, graph)
162        }
163    }
164}
165
166impl RustParser {
167    /// Parse files sequentially
168    fn parse_files_sequential(
169        &self,
170        paths: &[PathBuf],
171        graph: &mut CodeGraph,
172    ) -> Result<ProjectInfo, ParserError> {
173        let mut files = Vec::new();
174        let mut failed_files = Vec::new();
175        let mut total_functions = 0;
176        let mut total_classes = 0;
177        let mut total_parse_time = Duration::ZERO;
178
179        for path in paths {
180            match self.parse_file(path, graph) {
181                Ok(info) => {
182                    total_functions += info.functions.len();
183                    total_classes += info.classes.len();
184                    total_parse_time += info.parse_time;
185                    files.push(info);
186                }
187                Err(e) => {
188                    failed_files.push((path.clone(), e.to_string()));
189                }
190            }
191        }
192
193        Ok(ProjectInfo {
194            files,
195            failed_files,
196            total_functions,
197            total_classes,
198            total_parse_time,
199        })
200    }
201
202    /// Parse files in parallel using rayon
203    fn parse_files_parallel(
204        &self,
205        paths: &[PathBuf],
206        graph: &mut CodeGraph,
207    ) -> Result<ProjectInfo, ParserError> {
208        use rayon::prelude::*;
209
210        let graph_mutex = Mutex::new(graph);
211
212        // Configure thread pool if parallel_workers is specified
213        let pool = if let Some(num_threads) = self.config.parallel_workers {
214            rayon::ThreadPoolBuilder::new()
215                .num_threads(num_threads)
216                .build()
217                .map_err(|e| {
218                    ParserError::GraphError(format!("Failed to create thread pool: {e}"))
219                })?
220        } else {
221            rayon::ThreadPoolBuilder::new().build().map_err(|e| {
222                ParserError::GraphError(format!("Failed to create thread pool: {e}"))
223            })?
224        };
225
226        let results: Vec<_> = pool.install(|| {
227            paths
228                .par_iter()
229                .map(|path| {
230                    let mut graph = graph_mutex.lock().unwrap();
231                    match self.parse_file(path, &mut graph) {
232                        Ok(info) => Ok(info),
233                        Err(e) => Err((path.clone(), e.to_string())),
234                    }
235                })
236                .collect()
237        });
238
239        let mut files = Vec::new();
240        let mut failed_files = Vec::new();
241        let mut total_functions = 0;
242        let mut total_classes = 0;
243        let mut total_parse_time = Duration::ZERO;
244
245        for result in results {
246            match result {
247                Ok(info) => {
248                    total_functions += info.functions.len();
249                    total_classes += info.classes.len();
250                    total_parse_time += info.parse_time;
251                    files.push(info);
252                }
253                Err((path, error)) => {
254                    failed_files.push((path, error));
255                }
256            }
257        }
258
259        Ok(ProjectInfo {
260            files,
261            failed_files,
262            total_functions,
263            total_classes,
264            total_parse_time,
265        })
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_language() {
275        let parser = RustParser::new();
276        assert_eq!(parser.language(), "rust");
277    }
278
279    #[test]
280    fn test_file_extensions() {
281        let parser = RustParser::new();
282        assert_eq!(parser.file_extensions(), &[".rs"]);
283    }
284
285    #[test]
286    fn test_can_parse() {
287        let parser = RustParser::new();
288        assert!(parser.can_parse(Path::new("test.rs")));
289        assert!(!parser.can_parse(Path::new("test.py")));
290        assert!(!parser.can_parse(Path::new("test.txt")));
291    }
292}