Skip to main content

the_code_graph_parser/
registry.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use domain::model::Language;
5
6use crate::{
7    GoParser, JavaScriptParser, LanguageParser, PythonParser, RustParser, TypeScriptParser,
8};
9
10/// Registry of language parsers with extension-based dispatch.
11pub struct ParserRegistry {
12    parsers: Vec<Box<dyn LanguageParser>>,
13    extension_map: HashMap<String, usize>,
14}
15
16impl ParserRegistry {
17    /// Create registry with all supported language parsers.
18    pub fn new() -> Self {
19        let mut registry = Self {
20            parsers: Vec::new(),
21            extension_map: HashMap::new(),
22        };
23        registry.register(Box::new(TypeScriptParser::new()));
24        registry.register(Box::new(JavaScriptParser::new()));
25        registry.register(Box::new(RustParser::new()));
26        registry.register(Box::new(PythonParser::new()));
27        registry.register(Box::new(GoParser::new()));
28        registry
29    }
30
31    fn register(&mut self, parser: Box<dyn LanguageParser>) {
32        let idx = self.parsers.len();
33        for ext in parser.file_extensions() {
34            self.extension_map.insert(ext.to_string(), idx);
35        }
36        self.parsers.push(parser);
37    }
38
39    /// Get the parser for a file based on its extension.
40    pub fn parser_for_file(&self, path: &Path) -> Option<&dyn LanguageParser> {
41        let ext = path.extension()?.to_str()?;
42        let idx = self.extension_map.get(ext)?;
43        Some(self.parsers[*idx].as_ref())
44    }
45
46    /// Get the parser for a specific Language enum value.
47    pub fn parser_for_language(&self, lang: Language) -> Option<&dyn LanguageParser> {
48        self.parsers
49            .iter()
50            .find(|p| p.language() == lang)
51            .map(|p| p.as_ref())
52    }
53
54    /// List all supported file extensions.
55    pub fn supported_extensions(&self) -> Vec<&str> {
56        self.extension_map.keys().map(|s| s.as_str()).collect()
57    }
58}
59
60impl Default for ParserRegistry {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66// Compile-time assertion: ParserRegistry is Send + Sync
67const _: fn() = || {
68    fn assert_send_sync<T: Send + Sync>() {}
69    assert_send_sync::<ParserRegistry>();
70};
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::path::Path;
76
77    #[test]
78    fn parser_for_ts_file() {
79        let registry = ParserRegistry::new();
80        let parser = registry.parser_for_file(Path::new("foo.ts"));
81        assert!(parser.is_some());
82        assert_eq!(parser.unwrap().language(), Language::TypeScript);
83    }
84
85    #[test]
86    fn parser_for_tsx_file() {
87        let registry = ParserRegistry::new();
88        let parser = registry.parser_for_file(Path::new("foo.tsx"));
89        assert!(parser.is_some());
90        assert_eq!(parser.unwrap().language(), Language::TypeScript);
91    }
92
93    #[test]
94    fn parser_for_js_file() {
95        let registry = ParserRegistry::new();
96        let parser = registry.parser_for_file(Path::new("foo.js"));
97        assert!(parser.is_some());
98        assert_eq!(parser.unwrap().language(), Language::JavaScript);
99    }
100
101    #[test]
102    fn parser_for_jsx_file() {
103        let registry = ParserRegistry::new();
104        let parser = registry.parser_for_file(Path::new("foo.jsx"));
105        assert!(parser.is_some());
106        assert_eq!(parser.unwrap().language(), Language::JavaScript);
107    }
108
109    #[test]
110    fn parser_for_rs_file() {
111        let registry = ParserRegistry::new();
112        let parser = registry.parser_for_file(Path::new("foo.rs"));
113        assert!(parser.is_some());
114        assert_eq!(parser.unwrap().language(), Language::Rust);
115    }
116
117    #[test]
118    fn parser_for_py_file() {
119        let registry = ParserRegistry::new();
120        let parser = registry.parser_for_file(Path::new("foo.py"));
121        assert!(parser.is_some());
122        assert_eq!(parser.unwrap().language(), Language::Python);
123    }
124
125    #[test]
126    fn parser_for_go_file() {
127        let registry = ParserRegistry::new();
128        let parser = registry.parser_for_file(Path::new("foo.go"));
129        assert!(parser.is_some());
130        assert_eq!(parser.unwrap().language(), Language::Go);
131    }
132
133    #[test]
134    fn parser_for_txt_returns_none() {
135        let registry = ParserRegistry::new();
136        assert!(registry.parser_for_file(Path::new("foo.txt")).is_none());
137    }
138
139    #[test]
140    fn parser_for_language_typescript() {
141        let registry = ParserRegistry::new();
142        let parser = registry.parser_for_language(Language::TypeScript);
143        assert!(parser.is_some());
144    }
145
146    #[test]
147    fn parser_for_language_rust() {
148        let registry = ParserRegistry::new();
149        let parser = registry.parser_for_language(Language::Rust);
150        assert!(parser.is_some());
151    }
152
153    #[test]
154    fn supported_extensions_contains_all_five_languages() {
155        let registry = ParserRegistry::new();
156        let exts = registry.supported_extensions();
157        // TypeScript + JavaScript
158        assert!(exts.contains(&"ts"));
159        assert!(exts.contains(&"tsx"));
160        assert!(exts.contains(&"js"));
161        assert!(exts.contains(&"jsx"));
162        // Rust
163        assert!(exts.contains(&"rs"));
164        // Python
165        assert!(exts.contains(&"py"));
166        // Go
167        assert!(exts.contains(&"go"));
168    }
169
170    #[test]
171    fn registry_is_thread_safe() {
172        use std::sync::Arc;
173        use std::thread;
174
175        let registry = Arc::new(ParserRegistry::new());
176        let r1 = Arc::clone(&registry);
177        let r2 = Arc::clone(&registry);
178
179        let t1 = thread::spawn(move || {
180            let parser = r1.parser_for_file(Path::new("foo.rs"));
181            assert!(parser.is_some());
182            assert_eq!(parser.unwrap().language(), Language::Rust);
183        });
184
185        let t2 = thread::spawn(move || {
186            let parser = r2.parser_for_file(Path::new("foo.py"));
187            assert!(parser.is_some());
188            assert_eq!(parser.unwrap().language(), Language::Python);
189        });
190
191        t1.join().expect("thread 1 panicked");
192        t2.join().expect("thread 2 panicked");
193    }
194
195    #[test]
196    fn ac51_parse_from_multiple_threads_all_languages() {
197        use std::sync::Arc;
198        use std::thread;
199
200        let registry = Arc::new(ParserRegistry::new());
201
202        let sources: Vec<(&str, &str)> = vec![
203            ("test.rs", "fn hello() {} struct Foo {}"),
204            ("test.py", "def hello(): pass\nclass Foo: pass"),
205            ("test.go", "package main\nfunc Hello() {}"),
206        ];
207
208        let handles: Vec<_> = sources
209            .into_iter()
210            .map(|(filename, source)| {
211                let reg = Arc::clone(&registry);
212                let filename = filename.to_string();
213                let source = source.to_string();
214                thread::spawn(move || {
215                    let parser = reg
216                        .parser_for_file(Path::new(&filename))
217                        .expect("parser should exist");
218                    let result = parser.parse(source.as_bytes(), Path::new(&filename));
219                    assert!(result.is_ok(), "parse failed for {filename}");
220                    let pr = result.unwrap();
221                    assert!(!pr.symbols.is_empty(), "expected symbols from {filename}");
222                })
223            })
224            .collect();
225
226        for h in handles {
227            h.join().expect("thread panicked");
228        }
229    }
230}