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