codegraph_typescript/
parser_impl.rs

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