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