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