Skip to main content

graphy_parser/
lib.rs

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
25/// Parse a single file, auto-detecting the language from its extension.
26pub 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        // Built-in: custom frontends for deep analysis
37        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        // Dynamic: loaded from ~/.config/graphy/grammars/
42        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
55/// Parse multiple files in parallel using rayon.
56pub 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}