Skip to main content

cha_parser/
lib.rs

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