1pub mod frontend;
2pub mod helpers;
3pub mod python;
4pub mod rust_lang;
5pub mod svelte;
6pub mod tags_frontend;
7pub mod tags_registry;
8pub mod typescript;
9
10pub mod dynamic_loader;
11pub mod grammar_compiler;
12
13use std::path::Path;
14
15use anyhow::Result;
16use graphy_core::{Language, ParseOutput};
17
18use crate::frontend::LanguageFrontend;
19use crate::python::PythonFrontend;
20use crate::rust_lang::RustFrontend;
21use crate::svelte::SvelteFrontend;
22use crate::tags_frontend::TagsFrontend;
23use crate::typescript::TypeScriptFrontend;
24
25pub fn parse_file(path: &Path, source: &str) -> Result<ParseOutput> {
27 let ext = path
28 .extension()
29 .and_then(|e| e.to_str())
30 .unwrap_or("");
31
32 let lang = Language::from_extension(ext)
33 .ok_or_else(|| anyhow::anyhow!("Unsupported file extension: {ext}"))?;
34
35 match lang {
36 Language::Python => PythonFrontend::new().parse(path, source),
38 Language::TypeScript | Language::JavaScript => TypeScriptFrontend::new().parse(path, source),
39 Language::Rust => RustFrontend::new().parse(path, source),
40 Language::Svelte => SvelteFrontend::new().parse(path, source),
41 other => {
43 if let Some(config) = tags_registry::tags_config_for_language(other) {
44 TagsFrontend::new(config).parse(path, source)
45 } else {
46 let hint = dynamic_loader::grammar_info_for_language(other)
47 .map(|info| format!("Install with: graphy lang add {}", info.name))
48 .unwrap_or_else(|| "No grammar available for this language".to_string());
49 Err(anyhow::anyhow!("Language {other:?} grammar not installed. {hint}"))
50 }
51 }
52 }
53}
54
55pub fn parse_files(files: &[(std::path::PathBuf, String)]) -> Vec<(std::path::PathBuf, Result<ParseOutput>)> {
57 use rayon::prelude::*;
58
59 files
60 .par_iter()
61 .map(|(path, source)| {
62 let result = parse_file(path, source);
63 (path.clone(), result)
64 })
65 .collect()
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use std::path::PathBuf;
72
73 #[test]
74 fn parse_file_python() {
75 let path = PathBuf::from("test.py");
76 let source = "def hello():\n pass\n";
77 let result = parse_file(&path, source);
78 assert!(result.is_ok());
79 }
80
81 #[test]
82 fn parse_file_typescript() {
83 let path = PathBuf::from("test.ts");
84 let source = "function greet(): void {}\n";
85 let result = parse_file(&path, source);
86 assert!(result.is_ok());
87 }
88
89 #[test]
90 fn parse_file_javascript() {
91 let path = PathBuf::from("test.js");
92 let source = "function add(a, b) { return a + b; }\n";
93 let result = parse_file(&path, source);
94 assert!(result.is_ok());
95 }
96
97 #[test]
98 fn parse_file_rust() {
99 let path = PathBuf::from("test.rs");
100 let source = "fn main() {}\n";
101 let result = parse_file(&path, source);
102 assert!(result.is_ok());
103 }
104
105 #[test]
106 fn parse_file_svelte() {
107 let path = PathBuf::from("Component.svelte");
108 let source = "<script>\nlet x = 1;\n</script>\n<p>Hello</p>";
109 let result = parse_file(&path, source);
110 assert!(result.is_ok());
111 }
112
113 #[test]
114 fn parse_file_unsupported_extension() {
115 let path = PathBuf::from("test.xyz");
116 let source = "irrelevant";
117 let result = parse_file(&path, source);
118 assert!(result.is_err());
119 let err_msg = result.unwrap_err().to_string();
120 assert!(err_msg.contains("Unsupported") || err_msg.contains("xyz"));
121 }
122
123 #[test]
124 fn parse_file_no_extension() {
125 let path = PathBuf::from("Makefile");
126 let source = "all: build\n";
127 let result = parse_file(&path, source);
128 assert!(result.is_err());
129 }
130
131 #[test]
132 fn parse_files_parallel() {
133 let files = vec![
134 (PathBuf::from("a.py"), "def a(): pass\n".to_string()),
135 (PathBuf::from("b.ts"), "function b() {}\n".to_string()),
136 (PathBuf::from("c.rs"), "fn c() {}\n".to_string()),
137 ];
138 let results = parse_files(&files);
139 assert_eq!(results.len(), 3);
140 assert!(results.iter().all(|(_, r)| r.is_ok()));
141 }
142
143 #[test]
144 fn parse_files_empty() {
145 let files: Vec<(PathBuf, String)> = vec![];
146 let results = parse_files(&files);
147 assert!(results.is_empty());
148 }
149
150 #[test]
151 fn parse_files_mixed_success_failure() {
152 let files = vec![
153 (PathBuf::from("good.py"), "def ok(): pass\n".to_string()),
154 (PathBuf::from("bad.xyz"), "???".to_string()),
155 ];
156 let results = parse_files(&files);
157 assert_eq!(results.len(), 2);
158 assert!(results[0].1.is_ok());
159 assert!(results[1].1.is_err());
160 }
161
162 #[test]
163 fn parse_file_jsx_extension() {
164 let path = PathBuf::from("App.jsx");
165 let source = "function App() { return <div/>; }\n";
166 let result = parse_file(&path, source);
167 assert!(result.is_ok());
168 }
169
170 #[test]
171 fn parse_file_tsx_extension() {
172 let path = PathBuf::from("App.tsx");
173 let source = "function App(): JSX.Element { return <div/>; }\n";
174 let result = parse_file(&path, source);
175 assert!(result.is_ok());
176 }
177}