1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::edge_weights::{C_FAMILY_SEMANTIC, SEMANTIC_DISCOVERY};
8use crate::config::extensions::C_FAMILY_EXTENSIONS;
9use crate::config::weights::EDGE_WEIGHTS;
10use crate::types::{Fragment, FragmentId};
11
12use super::super::EdgeDict;
13use super::super::base::{self, EdgeBuilder, add_edge};
14
15fn is_c_family(path: &Path) -> bool {
16 let ext = base::file_ext(path);
17 C_FAMILY_EXTENSIONS.contains(ext.as_str())
18}
19
20static HEADER_EXTENSIONS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
21 [".h", ".hpp", ".hh", ".hxx", ".h++"]
22 .iter()
23 .copied()
24 .collect()
25});
26
27static IMPL_EXTENSIONS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
28 [".c", ".cpp", ".cc", ".cxx", ".c++", ".m", ".mm"]
29 .iter()
30 .copied()
31 .collect()
32});
33
34static INCLUDE_RE: Lazy<Regex> =
35 Lazy::new(|| Regex::new(r#"(?m)^\s*#\s*(?:include|import)\s*[<"]([^>"]+)[>"]"#).unwrap());
36static FUNC_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(\w+)\s*\(").unwrap());
37static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
38static FUNC_DEF_RE: Lazy<Regex> =
39 Lazy::new(|| Regex::new(r"(?m)^\s*(?:[\w*&]+\s+)+(\w+)\s*\(").unwrap());
40static TYPE_DEF_RE: Lazy<Regex> =
41 Lazy::new(|| Regex::new(r"(?m)^\s*(?:class|struct|enum|union|typedef)\s+([A-Z]\w*)").unwrap());
42static INHERITANCE_RE: Lazy<Regex> = Lazy::new(|| {
43 Regex::new(r"(?:class|struct)\s+(\w+)\s*:\s*(?:public|protected|private)?\s*(\w+)").unwrap()
44});
45
46static C_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
47 [
48 "if",
49 "for",
50 "while",
51 "switch",
52 "case",
53 "return",
54 "sizeof",
55 "typeof",
56 "alignof",
57 "static_assert",
58 "do",
59 "else",
60 "goto",
61 "break",
62 "continue",
63 "default",
64 "register",
65 "volatile",
66 "extern",
67 "typedef",
68 "auto",
69 "inline",
70 "restrict",
71 "noexcept",
72 "decltype",
73 "nullptr",
74 "throw",
75 "try",
76 "catch",
77 "delete",
78 "new",
79 "template",
80 "namespace",
81 "using",
82 "operator",
83 ]
84 .iter()
85 .copied()
86 .collect()
87});
88
89static C_COMMON_MACROS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
90 [
91 "NULL", "TRUE", "FALSE", "BOOL", "DWORD", "HANDLE", "VOID", "HRESULT", "LPCTSTR", "LPCSTR",
92 "LPWSTR", "INT", "UINT", "LONG", "ULONG", "WORD", "BYTE", "CHAR", "SHORT", "EOF",
93 "SIZE_MAX", "INT_MAX", "INT_MIN",
94 ]
95 .iter()
96 .copied()
97 .collect()
98});
99
100fn extract_includes(content: &str) -> FxHashSet<String> {
101 let mut includes = FxHashSet::default();
102 for cap in INCLUDE_RE.captures_iter(content) {
103 let header = cap[1].to_string();
104 if header.contains('/') {
105 includes.insert(header.split('/').next_back().unwrap().to_string());
106 }
107 includes.insert(header);
108 }
109 includes
110}
111
112fn extract_definitions(content: &str) -> (FxHashSet<String>, FxHashSet<String>) {
113 let functions: FxHashSet<String> = FUNC_DEF_RE
114 .captures_iter(content)
115 .map(|c| c[1].to_string())
116 .filter(|n| {
117 !C_KEYWORDS.contains(n.as_str()) && n.len() > SEMANTIC_DISCOVERY.min_identifier_length
118 })
119 .collect();
120 let types: FxHashSet<String> = TYPE_DEF_RE
121 .captures_iter(content)
122 .map(|c| c[1].to_string())
123 .collect();
124 (functions, types)
125}
126
127fn extract_references(
128 content: &str,
129 own_defs: &FxHashSet<String>,
130) -> (FxHashSet<String>, FxHashSet<String>) {
131 let calls: FxHashSet<String> = FUNC_CALL_RE
132 .captures_iter(content)
133 .map(|c| c[1].to_string())
134 .filter(|n| {
135 !C_KEYWORDS.contains(n.as_str())
136 && !own_defs.contains(n)
137 && !n.starts_with('_')
138 && n.len() > SEMANTIC_DISCOVERY.min_identifier_length
139 })
140 .collect();
141 let type_refs: FxHashSet<String> = TYPE_REF_RE
142 .captures_iter(content)
143 .map(|c| c[1].to_string())
144 .filter(|n| {
145 !C_COMMON_MACROS.contains(n.as_str())
146 && !own_defs.contains(n)
147 && n.len() > SEMANTIC_DISCOVERY.min_identifier_length
148 })
149 .collect();
150 (calls, type_refs)
151}
152
153pub struct CFamilyEdgeBuilder;
154
155impl EdgeBuilder for CFamilyEdgeBuilder {
156 fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
157 let c_frags: Vec<&Fragment> = fragments
158 .iter()
159 .filter(|f| is_c_family(Path::new(f.path())))
160 .collect();
161 if c_frags.is_empty() {
162 return FxHashMap::default();
163 }
164
165 let include_weight = EDGE_WEIGHTS["c_include"].forward;
166 let call_weight = EDGE_WEIGHTS["c_call"].forward;
167 let type_weight = EDGE_WEIGHTS["c_type"].forward;
168 let inheritance_weight = EDGE_WEIGHTS["c_inheritance"].forward;
169 let reverse_factor = EDGE_WEIGHTS["c_include"].reverse_factor;
170 let base_weight = C_FAMILY_SEMANTIC.base_weight;
171
172 let mut header_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
173 let mut func_defs_map: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
174 let mut type_defs_map: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
175 let mut frag_own_defs: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
176
177 for f in &c_frags {
178 let path = Path::new(f.path());
179 let name = path
180 .file_name()
181 .map(|n| n.to_string_lossy().to_string())
182 .unwrap_or_default();
183 let stem = path
184 .file_stem()
185 .map(|s| s.to_string_lossy().to_string())
186 .unwrap_or_default();
187
188 header_to_frags.entry(name).or_default().push(f.id.clone());
189 if !stem.is_empty() {
190 header_to_frags
191 .entry(format!("{}.h", stem))
192 .or_default()
193 .push(f.id.clone());
194 header_to_frags
195 .entry(format!("{}.hpp", stem))
196 .or_default()
197 .push(f.id.clone());
198 }
199
200 let (functions, types) = extract_definitions(&f.content);
201 let mut own_defs = FxHashSet::default();
202 for func in &functions {
203 func_defs_map
204 .entry(func.clone())
205 .or_default()
206 .push(f.id.clone());
207 own_defs.insert(func.clone());
208 }
209 for t in &types {
210 type_defs_map
211 .entry(t.clone())
212 .or_default()
213 .push(f.id.clone());
214 own_defs.insert(t.clone());
215 }
216 frag_own_defs.insert(f.id.clone(), own_defs);
217 }
218
219 let mut edges: EdgeDict = FxHashMap::default();
220
221 for f in &c_frags {
222 for inc in extract_includes(&f.content) {
223 let inc_name = if inc.contains('/') {
224 inc.split('/').next_back().unwrap().to_string()
225 } else {
226 inc.clone()
227 };
228 for target_id in header_to_frags.get(&inc_name).unwrap_or(&vec![]) {
229 if target_id != &f.id {
230 add_edge(&mut edges, &f.id, target_id, include_weight, reverse_factor);
231 }
232 }
233 }
234
235 let own_defs = frag_own_defs.get(&f.id).cloned().unwrap_or_default();
236 let (calls, type_refs) = extract_references(&f.content, &own_defs);
237
238 for call in &calls {
239 for def_id in func_defs_map.get(call).unwrap_or(&vec![]) {
240 if def_id != &f.id {
241 add_edge(&mut edges, &f.id, def_id, call_weight, reverse_factor);
242 }
243 }
244 }
245
246 for t in &type_refs {
247 for def_id in type_defs_map.get(t).unwrap_or(&vec![]) {
248 if def_id != &f.id {
249 add_edge(&mut edges, &f.id, def_id, type_weight, reverse_factor);
250 }
251 }
252 }
253
254 for cap in INHERITANCE_RE.captures_iter(&f.content) {
255 let base = cap[2].to_string();
256 for def_id in type_defs_map.get(&base).unwrap_or(&vec![]) {
257 if def_id != &f.id {
258 add_edge(
259 &mut edges,
260 &f.id,
261 def_id,
262 inheritance_weight,
263 reverse_factor,
264 );
265 }
266 }
267 }
268 }
269
270 let mut by_stem: FxHashMap<String, Vec<&Fragment>> = FxHashMap::default();
271 for f in &c_frags {
272 let stem = Path::new(f.path())
273 .file_stem()
274 .map(|s| s.to_string_lossy().to_lowercase())
275 .unwrap_or_default();
276 by_stem.entry(stem).or_default().push(f);
277 }
278
279 for (_stem, group) in &by_stem {
280 if group.len() < 2 {
281 continue;
282 }
283 let headers: Vec<&&Fragment> = group
284 .iter()
285 .filter(|f| {
286 HEADER_EXTENSIONS.contains(base::file_ext(Path::new(f.path())).as_str())
287 })
288 .collect();
289 let impls: Vec<&&Fragment> = group
290 .iter()
291 .filter(|f| IMPL_EXTENSIONS.contains(base::file_ext(Path::new(f.path())).as_str()))
292 .collect();
293 for h in &headers {
294 for imp in &impls {
295 add_edge(&mut edges, &h.id, &imp.id, base_weight, reverse_factor);
296 }
297 }
298 }
299
300 edges
301 }
302
303 fn discover_related_files(
304 &self,
305 changed: &[PathBuf],
306 candidates: &[PathBuf],
307 _repo_root: Option<&Path>,
308 _file_cache: Option<&FxHashMap<PathBuf, String>>,
309 ) -> Vec<PathBuf> {
310 let c_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_c_family(f)).collect();
311 if c_changed.is_empty() {
312 return vec![];
313 }
314
315 let changed_set: FxHashSet<PathBuf> = changed.iter().cloned().collect();
316 let mut discovered: FxHashSet<PathBuf> = FxHashSet::default();
317 let mut frontier: Vec<PathBuf> = c_changed.iter().map(|f| (*f).clone()).collect();
318
319 for _ in 0..SEMANTIC_DISCOVERY.max_depth {
320 let mut hop_found: Vec<PathBuf> = Vec::new();
321
322 let mut included_headers: FxHashSet<String> = FxHashSet::default();
323 for f in &frontier {
324 if let Ok(content) = std::fs::read_to_string(f) {
325 included_headers.extend(extract_includes(&content));
326 }
327 }
328
329 let mut changed_names: FxHashSet<String> = FxHashSet::default();
330 for f in &frontier {
331 if let Some(name) = f.file_name() {
332 changed_names.insert(name.to_string_lossy().to_string());
333 }
334 if let Some(stem) = f.file_stem() {
335 let s = stem.to_string_lossy().to_string();
336 changed_names.insert(format!("{}.h", s));
337 changed_names.insert(format!("{}.hpp", s));
338 }
339 }
340
341 for candidate in candidates {
342 if changed_set.contains(candidate)
343 || discovered.contains(candidate)
344 || !is_c_family(candidate)
345 {
346 continue;
347 }
348 let cand_name = candidate
349 .file_name()
350 .map(|n| n.to_string_lossy().to_string())
351 .unwrap_or_default();
352 if included_headers.contains(&cand_name) {
353 hop_found.push(candidate.clone());
354 continue;
355 }
356 if let Ok(content) = std::fs::read_to_string(candidate) {
357 let cand_includes = extract_includes(&content);
358 for inc in &cand_includes {
359 let inc_name = if inc.contains('/') {
360 inc.split('/').next_back().unwrap().to_string()
361 } else {
362 inc.clone()
363 };
364 if changed_names.contains(&inc_name) {
365 hop_found.push(candidate.clone());
366 break;
367 }
368 }
369 }
370 }
371
372 let new_files: Vec<PathBuf> = hop_found
373 .into_iter()
374 .filter(|f| !discovered.contains(f))
375 .collect();
376 if new_files.is_empty() {
377 break;
378 }
379 discovered.extend(new_files.iter().cloned());
380 frontier = new_files;
381 }
382
383 let mut result: Vec<PathBuf> = discovered.into_iter().collect();
384 result.sort();
385 result
386 }
387}