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