|
| 1 | +use camino::Utf8Path; |
| 2 | +use serde::Deserialize; |
| 3 | +use std::fs; |
| 4 | + |
| 5 | +/// How `.h` header files should be parsed when both C and C++ extractors are available. |
| 6 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 7 | +pub enum HeaderLanguage { |
| 8 | + /// Detect from project layout and file content. |
| 9 | + #[default] |
| 10 | + Auto, |
| 11 | + C, |
| 12 | + Cpp, |
| 13 | +} |
| 14 | + |
| 15 | +#[derive(Debug, Default, Deserialize)] |
| 16 | +struct ConfigFile { |
| 17 | + #[serde(default)] |
| 18 | + languages: LanguagesSection, |
| 19 | +} |
| 20 | + |
| 21 | +#[derive(Debug, Default, Deserialize)] |
| 22 | +struct LanguagesSection { |
| 23 | + /// `"auto"`, `"c"`, or `"cpp"`. |
| 24 | + #[serde(default)] |
| 25 | + headers: Option<String>, |
| 26 | +} |
| 27 | + |
| 28 | +/// Project-level extraction settings (`.codegraph/config.toml`). |
| 29 | +#[derive(Debug, Clone, Default)] |
| 30 | +pub struct ExtractConfig { |
| 31 | + pub header_language: HeaderLanguage, |
| 32 | +} |
| 33 | + |
| 34 | +impl ExtractConfig { |
| 35 | + pub fn load(root: &Utf8Path) -> Self { |
| 36 | + let path = root.join(".codegraph").join("config.toml"); |
| 37 | + Self::load_from(&path) |
| 38 | + } |
| 39 | + |
| 40 | + pub fn load_from(path: &Utf8Path) -> Self { |
| 41 | + let Ok(text) = fs::read_to_string(path.as_std_path()) else { |
| 42 | + return Self::default(); |
| 43 | + }; |
| 44 | + let Ok(file) = toml::from_str::<ConfigFile>(&text) else { |
| 45 | + return Self::default(); |
| 46 | + }; |
| 47 | + Self { |
| 48 | + header_language: parse_header_language(file.languages.headers.as_deref()), |
| 49 | + } |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +fn parse_header_language(raw: Option<&str>) -> HeaderLanguage { |
| 54 | + match raw.unwrap_or("auto").trim().to_ascii_lowercase().as_str() { |
| 55 | + "c" => HeaderLanguage::C, |
| 56 | + "cpp" | "c++" | "cxx" => HeaderLanguage::Cpp, |
| 57 | + _ => HeaderLanguage::Auto, |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +/// Default `config.toml` written on `codegraph init`. |
| 62 | +pub const DEFAULT_CONFIG_TOML: &str = r#"# CodeGraph project configuration |
| 63 | +# See https://github.com/Cleboost/codegraph-rs |
| 64 | +
|
| 65 | +[languages] |
| 66 | +# How to parse .h header files: "auto", "c", or "cpp". |
| 67 | +# "auto" detects C++ projects from .cpp/.hpp files and C++ syntax in headers. |
| 68 | +headers = "auto" |
| 69 | +"#; |
| 70 | + |
| 71 | +/// Quick project scan: returns a hint when the tree is clearly C-only or C++-only. |
| 72 | +pub fn detect_project_header_hint(root: &Utf8Path) -> Option<HeaderLanguage> { |
| 73 | + let mut c_files = 0u32; |
| 74 | + let mut cpp_files = 0u32; |
| 75 | + |
| 76 | + let walker = ignore::WalkBuilder::new(root) |
| 77 | + .hidden(true) |
| 78 | + .git_ignore(true) |
| 79 | + .git_exclude(true) |
| 80 | + .parents(true) |
| 81 | + .add_custom_ignore_filename(".codegraphignore") |
| 82 | + .build(); |
| 83 | + |
| 84 | + for entry in walker.flatten() { |
| 85 | + if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) { |
| 86 | + continue; |
| 87 | + } |
| 88 | + let Some(ext) = entry.path().extension().and_then(|s| s.to_str()) else { |
| 89 | + continue; |
| 90 | + }; |
| 91 | + match ext { |
| 92 | + "c" => c_files += 1, |
| 93 | + "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => cpp_files += 1, |
| 94 | + _ => {} |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + if cpp_files > 0 && c_files == 0 { |
| 99 | + Some(HeaderLanguage::Cpp) |
| 100 | + } else if c_files > 0 && cpp_files == 0 { |
| 101 | + Some(HeaderLanguage::C) |
| 102 | + } else { |
| 103 | + None |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +/// Heuristic: does this header look like C++ from its source text? |
| 108 | +pub fn is_cpp_header(source: &str) -> bool { |
| 109 | + let sample = &source[..source.len().min(8192)]; |
| 110 | + const MARKERS: &[&str] = &[ |
| 111 | + "namespace ", |
| 112 | + "class ", |
| 113 | + "template ", |
| 114 | + "typename ", |
| 115 | + "constexpr ", |
| 116 | + "noexcept", |
| 117 | + "public:", |
| 118 | + "private:", |
| 119 | + "protected:", |
| 120 | + "operator ", |
| 121 | + "std::", |
| 122 | + "extern \"C\"", |
| 123 | + "using ", |
| 124 | + "::", |
| 125 | + ]; |
| 126 | + MARKERS.iter().any(|m| sample.contains(m)) |
| 127 | +} |
| 128 | + |
| 129 | +#[cfg(test)] |
| 130 | +mod tests { |
| 131 | + use super::*; |
| 132 | + |
| 133 | + #[test] |
| 134 | + fn parse_config_headers() { |
| 135 | + let cfg = toml::from_str::<ConfigFile>( |
| 136 | + r#" |
| 137 | +[languages] |
| 138 | +headers = "cpp" |
| 139 | +"#, |
| 140 | + ) |
| 141 | + .unwrap(); |
| 142 | + assert_eq!( |
| 143 | + parse_header_language(cfg.languages.headers.as_deref()), |
| 144 | + HeaderLanguage::Cpp |
| 145 | + ); |
| 146 | + } |
| 147 | + |
| 148 | + #[test] |
| 149 | + fn sniff_cpp_header() { |
| 150 | + assert!(is_cpp_header( |
| 151 | + "#pragma once\nnamespace tnl { class String {}; }\n" |
| 152 | + )); |
| 153 | + assert!(!is_cpp_header( |
| 154 | + "#ifndef FOO_H\n#define FOO_H\nstruct foo { int x; };\n#endif\n" |
| 155 | + )); |
| 156 | + } |
| 157 | +} |
0 commit comments