1use 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
15pub struct TypeScriptParser {
17 config: ParserConfig,
18 metrics: Mutex<ParserMetrics>,
19}
20
21impl TypeScriptParser {
22 pub fn new() -> Self {
24 Self {
25 config: ParserConfig::default(),
26 metrics: Mutex::new(ParserMetrics::default()),
27 }
28 }
29
30 pub fn with_config(config: ParserConfig) -> Self {
32 Self {
33 config,
34 metrics: Mutex::new(ParserMetrics::default()),
35 }
36 }
37
38 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 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 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 let source =
100 fs::read_to_string(path).map_err(|e| ParserError::IoError(path.to_path_buf(), e))?;
101
102 let result = self.parse_source(&source, path, graph);
104
105 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 let ir = extractor::extract(source, file_path, &self.config)?;
126
127 let mut file_info = self.ir_to_graph(&ir, graph, file_path)?;
129
130 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 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 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 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}