Skip to main content

cha_parser/
lib.rs

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
25/// Trait for language-specific parsers.
26pub trait LanguageParser: Send + Sync {
27    fn language_name(&self) -> &str;
28    fn parse(&self, file: &SourceFile) -> Option<SourceModel>;
29}
30
31/// Detect language from file extension and parse.
32pub 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
47/// Sniff whether a `.h` file contains C++ constructs.
48fn 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}