1mod c_imports;
2mod c_lang;
3mod cpp;
4mod golang;
5mod golang_imports;
6mod python;
7mod python_imports;
8mod rust_imports;
9mod rust_lang;
10mod switch_arms;
11mod type_aliases;
12mod type_ref;
13mod typescript;
14mod typescript_imports;
15
16pub use c_lang::{CParser, CppParser};
17pub use cha_core::{ClassInfo, CommentInfo, FunctionInfo, ImportInfo, SourceModel};
18pub use golang::GolangParser;
19pub use python::PythonParser;
20pub use rust_lang::RustParser;
21pub use typescript::TypeScriptParser;
22
23use cha_core::SourceFile;
24
25pub trait LanguageParser: Send + Sync {
27 fn language_name(&self) -> &str;
28 fn parse(&self, file: &SourceFile) -> Option<SourceModel>;
29}
30
31pub fn parse_file(file: &SourceFile) -> Option<SourceModel> {
33 let ext = file.path.extension()?.to_str()?;
34 let parser: Box<dyn LanguageParser> = match ext {
35 "ts" | "tsx" => Box::new(TypeScriptParser),
36 "rs" => Box::new(RustParser),
37 "py" => Box::new(PythonParser),
38 "go" => Box::new(GolangParser),
39 "h" if looks_like_cpp(&file.content) => Box::new(CppParser),
40 "c" | "h" => Box::new(CParser),
41 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Box::new(CppParser),
42 _ => return None,
43 };
44 parser.parse(file)
45}
46
47fn looks_like_cpp(content: &str) -> bool {
49 content.lines().any(|line| {
50 let t = line.trim();
51 t.starts_with("class ")
52 || t.starts_with("namespace ")
53 || t.starts_with("template")
54 || t.starts_with("using ")
55 || t.contains("public:")
56 || t.contains("private:")
57 || t.contains("protected:")
58 })
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use proptest::prelude::*;
65 use std::path::PathBuf;
66
67 proptest! {
68 #[test]
69 fn parse_rust_never_panics(content in ".*") {
70 let file = SourceFile::new(PathBuf::from("test.rs"), content);
71 let _ = parse_file(&file);
72 }
73
74 #[test]
75 fn parse_ts_never_panics(content in ".*") {
76 let file = SourceFile::new(PathBuf::from("test.ts"), content);
77 let _ = parse_file(&file);
78 }
79
80 #[test]
81 fn parse_unknown_ext_returns_none(content in ".*") {
82 let file = SourceFile::new(PathBuf::from("test.txt"), content);
83 prop_assert!(parse_file(&file).is_none());
84 }
85
86 #[test]
87 fn parse_model_invariants(content in ".{0,500}") {
88 let file = SourceFile::new(PathBuf::from("test.rs"), content.clone());
89 if let Some(model) = parse_file(&file) {
90 prop_assert_eq!(model.language, "rust");
91 prop_assert!(model.total_lines > 0 || content.is_empty());
92 for f in &model.functions {
93 prop_assert!(f.start_line <= f.end_line);
94 prop_assert!(f.line_count > 0);
95 prop_assert!(!f.name.is_empty());
96 }
97 for c in &model.classes {
98 prop_assert!(c.start_line <= c.end_line);
99 prop_assert!(!c.name.is_empty());
100 }
101 }
102 }
103 }
104}