Skip to main content

cha_parser/
lib.rs

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
16/// Trait for language-specific parsers.
17pub trait LanguageParser: Send + Sync {
18    fn language_name(&self) -> &str;
19    fn parse(&self, file: &SourceFile) -> Option<SourceModel>;
20}
21
22/// Detect language from file extension and parse.
23pub 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
38/// Sniff whether a `.h` file contains C++ constructs.
39fn 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}