Skip to main content

_diffctx/edges/semantic/
c_family.rs

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}