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 reps = super::super::base::file_representatives(fragments);
183 let mut header_to_files: FxHashMap<String, (Vec<Arc<str>>, FxHashSet<Arc<str>>)> =
184 FxHashMap::default();
185 let mut func_defs_map: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
186 let mut func_def_files: FxHashMap<String, FxHashSet<Arc<str>>> = FxHashMap::default();
187 let mut type_defs_map: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
188 let mut type_def_files: FxHashMap<String, FxHashSet<Arc<str>>> = FxHashMap::default();
189 let mut frag_own_defs: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
190
191 let push_file_key = |map: &mut FxHashMap<String, (Vec<Arc<str>>, FxHashSet<Arc<str>>)>,
195 key: String,
196 f: &Fragment| {
197 let (order, seen) = map.entry(key).or_default();
198 if seen.insert(f.id.path.clone()) {
199 order.push(f.id.path.clone());
200 }
201 };
202
203 for f in &c_frags {
204 let path = Path::new(f.path());
205 let name = path
206 .file_name()
207 .map(|n| n.to_string_lossy().to_string())
208 .unwrap_or_default();
209 let stem = path
210 .file_stem()
211 .map(|s| s.to_string_lossy().to_string())
212 .unwrap_or_default();
213
214 push_file_key(&mut header_to_files, name, f);
215 if !stem.is_empty() {
216 push_file_key(&mut header_to_files, format!("{stem}.h"), f);
217 push_file_key(&mut header_to_files, format!("{stem}.hpp"), f);
218 }
219
220 let (functions, types) = extract_definitions(&f.content);
221 let mut own_defs = FxHashSet::default();
222 for func in &functions {
223 func_defs_map
224 .entry(func.clone())
225 .or_default()
226 .push(f.id.clone());
227 func_def_files
228 .entry(func.clone())
229 .or_default()
230 .insert(f.id.path.clone());
231 own_defs.insert(func.clone());
232 }
233 for t in &types {
234 type_defs_map
235 .entry(t.clone())
236 .or_default()
237 .push(f.id.clone());
238 type_def_files
239 .entry(t.clone())
240 .or_default()
241 .insert(f.id.path.clone());
242 own_defs.insert(t.clone());
243 }
244 frag_own_defs.insert(f.id.clone(), own_defs);
245 }
246
247 let max_files = C_FAMILY_SEMANTIC.max_files_per_name;
248 let unambiguous = |files: Option<&FxHashSet<Arc<str>>>| {
249 files.map(|s| s.len() <= max_files).unwrap_or(true)
250 };
251
252 let mut edges: EdgeDict = FxHashMap::default();
253
254 for (i, f) in c_frags.iter().enumerate() {
255 crate::deadline::check_current_every(i, 256, "edge construction (c_family)");
259 for inc in extract_includes(&f.content) {
260 let inc_name = if inc.contains('/') {
261 inc.split('/').next_back().unwrap().to_string()
262 } else {
263 inc.clone()
264 };
265 let Some((candidates, _)) = header_to_files.get(&inc_name) else {
266 continue;
267 };
268 let suffix = format!("/{inc}");
275 let matched: Vec<&Arc<str>> = if inc.contains('/') {
276 candidates
277 .iter()
278 .filter(|p| p.as_ref() == inc || p.ends_with(&suffix))
279 .collect()
280 } else {
281 candidates.iter().collect()
282 };
283 if matched.is_empty() || matched.len() > max_files {
284 continue;
285 }
286 for path in matched {
287 let Some(rep) = reps.get(path.as_ref()) else {
288 continue;
289 };
290 if rep != &f.id {
291 add_edge(&mut edges, &f.id, rep, include_weight, reverse_factor);
292 }
293 }
294 }
295
296 let own_defs = frag_own_defs.get(&f.id).cloned().unwrap_or_default();
297 let (calls, type_refs) = extract_references(&f.content, &own_defs);
298
299 for call in &calls {
300 if !unambiguous(func_def_files.get(call)) {
301 continue;
302 }
303 for def_id in func_defs_map.get(call).unwrap_or(&vec![]) {
304 if def_id != &f.id {
305 add_edge(&mut edges, &f.id, def_id, call_weight, reverse_factor);
306 }
307 }
308 }
309
310 for t in &type_refs {
311 if !unambiguous(type_def_files.get(t)) {
312 continue;
313 }
314 for def_id in type_defs_map.get(t).unwrap_or(&vec![]) {
315 if def_id != &f.id {
316 add_edge(&mut edges, &f.id, def_id, type_weight, reverse_factor);
317 }
318 }
319 }
320
321 for cap in INHERITANCE_RE.captures_iter(&f.content) {
322 let base = cap[2].to_string();
323 if !unambiguous(type_def_files.get(&base)) {
324 continue;
325 }
326 for def_id in type_defs_map.get(&base).unwrap_or(&vec![]) {
327 if def_id != &f.id {
328 add_edge(
329 &mut edges,
330 &f.id,
331 def_id,
332 inheritance_weight,
333 reverse_factor,
334 );
335 }
336 }
337 }
338 }
339
340 let mut by_stem: FxHashMap<(String, String), (Vec<&str>, FxHashSet<&str>)> =
347 FxHashMap::default();
348 for f in &c_frags {
349 let path = Path::new(f.path());
350 let stem = path
351 .file_stem()
352 .map(|s| s.to_string_lossy().to_lowercase())
353 .unwrap_or_default();
354 let dir = path
355 .parent()
356 .map(|d| d.to_string_lossy().to_string())
357 .unwrap_or_default();
358 let (order, seen) = by_stem.entry((dir, stem)).or_default();
359 if seen.insert(f.path()) {
360 order.push(f.path());
361 }
362 }
363
364 for (i, (_key, (files, _))) in by_stem.iter().enumerate() {
368 crate::deadline::check_current_every(i, 256, "edge construction (c_family pairing)");
369 if files.len() < 2 {
370 continue;
371 }
372 let headers: Vec<&&str> = files
373 .iter()
374 .filter(|p| HEADER_EXTENSIONS.contains(base::file_ext(Path::new(**p)).as_str()))
375 .collect();
376 let impls: Vec<&&str> = files
377 .iter()
378 .filter(|p| IMPL_EXTENSIONS.contains(base::file_ext(Path::new(**p)).as_str()))
379 .collect();
380 for h in &headers {
381 for imp in &impls {
382 if let (Some(hr), Some(ir)) = (reps.get(**h), reps.get(**imp)) {
383 add_edge(&mut edges, hr, ir, base_weight, reverse_factor);
384 }
385 }
386 }
387 }
388
389 edges
390 }
391
392 fn discover_related_files(
393 &self,
394 changed: &[PathBuf],
395 candidates: &[PathBuf],
396 _repo_root: Option<&Path>,
397 _file_cache: Option<&FxHashMap<PathBuf, String>>,
398 ) -> Vec<PathBuf> {
399 let c_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_c_family(f)).collect();
400 if c_changed.is_empty() {
401 return vec![];
402 }
403
404 let changed_set: FxHashSet<PathBuf> = changed.iter().cloned().collect();
405 let mut discovered: FxHashSet<PathBuf> = FxHashSet::default();
406 let mut frontier: Vec<PathBuf> = c_changed.iter().map(|f| (*f).clone()).collect();
407
408 for _ in 0..SEMANTIC_DISCOVERY.max_depth {
409 let mut hop_found: Vec<PathBuf> = Vec::new();
410
411 let mut included_headers: FxHashSet<String> = FxHashSet::default();
412 for f in &frontier {
413 if let Ok(content) = std::fs::read_to_string(f) {
414 included_headers.extend(extract_includes(&content));
415 }
416 }
417
418 let mut changed_names: FxHashSet<String> = FxHashSet::default();
419 for f in &frontier {
420 if let Some(name) = f.file_name() {
421 changed_names.insert(name.to_string_lossy().to_string());
422 }
423 if let Some(stem) = f.file_stem() {
424 let s = stem.to_string_lossy().to_string();
425 changed_names.insert(format!("{}.h", s));
426 changed_names.insert(format!("{}.hpp", s));
427 }
428 }
429
430 for candidate in candidates {
431 if changed_set.contains(candidate)
432 || discovered.contains(candidate)
433 || !is_c_family(candidate)
434 {
435 continue;
436 }
437 let cand_name = candidate
438 .file_name()
439 .map(|n| n.to_string_lossy().to_string())
440 .unwrap_or_default();
441 if included_headers.contains(&cand_name) {
442 hop_found.push(candidate.clone());
443 continue;
444 }
445 if let Ok(content) = std::fs::read_to_string(candidate) {
446 let cand_includes = extract_includes(&content);
447 for inc in &cand_includes {
448 let inc_name = if inc.contains('/') {
449 inc.split('/').next_back().unwrap().to_string()
450 } else {
451 inc.clone()
452 };
453 if changed_names.contains(&inc_name) {
454 hop_found.push(candidate.clone());
455 break;
456 }
457 }
458 }
459 }
460
461 let new_files: Vec<PathBuf> = hop_found
462 .into_iter()
463 .filter(|f| !discovered.contains(f))
464 .collect();
465 if new_files.is_empty() {
466 break;
467 }
468 discovered.extend(new_files.iter().cloned());
469 frontier = new_files;
470 }
471
472 let mut result: Vec<PathBuf> = discovered.into_iter().collect();
473 result.sort();
474 result
475 }
476}