Skip to main content

_diffctx/edges/semantic/
rust_lang.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::SEMANTIC_DISCOVERY;
8use crate::config::extensions::RUST_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, add_edges_from_ids};
14
15fn is_rust_file(path: &Path) -> bool {
16    let ext = base::file_ext(path);
17    RUST_EXTENSIONS.contains(ext.as_str())
18}
19
20static USE_RE: Lazy<Regex> =
21    Lazy::new(|| Regex::new(r"(?m)^\s*use\s+([\w:]+(?:::\{[^}]+\})?)").unwrap());
22static MOD_RE: Lazy<Regex> =
23    Lazy::new(|| Regex::new(r"(?m)^\s*(?:pub\s+)?mod\s+(\w+)\s*[;{]").unwrap());
24static TYPE_DEF_RE: Lazy<Regex> = Lazy::new(|| {
25    Regex::new(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait|type|union)\s+([A-Z]\w*)")
26        .unwrap()
27});
28static FN_DEF_RE: Lazy<Regex> = Lazy::new(|| {
29    Regex::new(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-z_]\w*)").unwrap()
30});
31static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
32static FN_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([a-z_]\w+)\s*[(<]").unwrap());
33static PATH_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(\w+)::(\w+)").unwrap());
34static IMPL_RE: Lazy<Regex> =
35    Lazy::new(|| Regex::new(r"(?m)^\s*impl(?:<[^>]*>)?\s+(\w+)\s+for\s+(\w+)").unwrap());
36static PUB_USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*pub\s+use\s+([\w:]+)").unwrap());
37
38static RUST_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
39    [
40        "if",
41        "else",
42        "for",
43        "while",
44        "loop",
45        "match",
46        "return",
47        "break",
48        "continue",
49        "let",
50        "mut",
51        "ref",
52        "fn",
53        "pub",
54        "mod",
55        "use",
56        "struct",
57        "enum",
58        "trait",
59        "impl",
60        "type",
61        "where",
62        "as",
63        "in",
64        "self",
65        "super",
66        "crate",
67        "extern",
68        "async",
69        "await",
70        "move",
71        "unsafe",
72        "const",
73        "static",
74        "dyn",
75        "box",
76        "true",
77        "false",
78        "Some",
79        "None",
80        "Ok",
81        "Err",
82        "vec",
83        "println",
84        "eprintln",
85        "format",
86        "write",
87        "writeln",
88        "panic",
89        "assert",
90        "assert_eq",
91        "assert_ne",
92        "debug_assert",
93        "todo",
94        "unimplemented",
95        "unreachable",
96        "cfg",
97        "derive",
98        "allow",
99        "deny",
100        "warn",
101    ]
102    .iter()
103    .copied()
104    .collect()
105});
106
107fn extract_uses(content: &str) -> FxHashSet<String> {
108    USE_RE
109        .captures_iter(content)
110        .map(|c| c[1].to_string())
111        .collect()
112}
113
114fn extract_mods(content: &str) -> FxHashSet<String> {
115    MOD_RE
116        .captures_iter(content)
117        .map(|c| c[1].to_string())
118        .collect()
119}
120
121fn extract_definitions(content: &str) -> (FxHashSet<String>, FxHashSet<String>) {
122    let funcs: FxHashSet<String> = FN_DEF_RE
123        .captures_iter(content)
124        .map(|c| c[1].to_string())
125        .collect();
126    let types: FxHashSet<String> = TYPE_DEF_RE
127        .captures_iter(content)
128        .map(|c| c[1].to_string())
129        .collect();
130    (funcs, types)
131}
132
133fn extract_trait_impls(content: &str) -> Vec<(String, String)> {
134    IMPL_RE
135        .captures_iter(content)
136        .map(|c| (c[1].to_string(), c[2].to_string()))
137        .collect()
138}
139
140fn extract_pub_uses(content: &str) -> Vec<String> {
141    PUB_USE_RE
142        .captures_iter(content)
143        .map(|c| c[1].to_string())
144        .collect()
145}
146
147fn extract_references(
148    content: &str,
149) -> (
150    FxHashSet<String>,
151    FxHashSet<String>,
152    FxHashSet<(String, String)>,
153) {
154    let type_refs: FxHashSet<String> = TYPE_REF_RE
155        .captures_iter(content)
156        .map(|c| c[1].to_string())
157        .filter(|n| !RUST_KEYWORDS.contains(n.as_str()))
158        .collect();
159    let fn_calls: FxHashSet<String> = FN_CALL_RE
160        .captures_iter(content)
161        .map(|c| c[1].to_string())
162        .filter(|n| !RUST_KEYWORDS.contains(n.as_str()))
163        .collect();
164    let path_calls: FxHashSet<(String, String)> = PATH_CALL_RE
165        .captures_iter(content)
166        .map(|c| (c[1].to_string(), c[2].to_string()))
167        .collect();
168    (type_refs, fn_calls, path_calls)
169}
170
171fn stem_to_mod_name(path: &Path) -> String {
172    let stem = path
173        .file_stem()
174        .map(|s| s.to_string_lossy().to_lowercase())
175        .unwrap_or_default();
176    if stem == "mod" || stem == "lib" {
177        path.parent()
178            .and_then(|p| p.file_name())
179            .map(|n| n.to_string_lossy().to_lowercase())
180            .unwrap_or(stem)
181    } else {
182        stem
183    }
184}
185
186pub struct RustEdgeBuilder;
187
188impl EdgeBuilder for RustEdgeBuilder {
189    fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
190        let rust_frags: Vec<&Fragment> = fragments
191            .iter()
192            .filter(|f| is_rust_file(Path::new(f.path())))
193            .collect();
194        if rust_frags.is_empty() {
195            return FxHashMap::default();
196        }
197
198        let mod_weight = EDGE_WEIGHTS["rust_mod"].forward;
199        let use_weight = EDGE_WEIGHTS["rust_use"].forward;
200        let type_weight = EDGE_WEIGHTS["rust_type"].forward;
201        let fn_weight = EDGE_WEIGHTS["rust_fn"].forward;
202        let same_crate_weight = EDGE_WEIGHTS["rust_same_crate"].forward;
203        let reverse_factor = EDGE_WEIGHTS["rust_mod"].reverse_factor;
204
205        let mut name_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
206        let mut mod_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
207        let mut type_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
208        let mut fn_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
209        let mut trait_impls: FxHashMap<FragmentId, Vec<(String, String)>> = FxHashMap::default();
210
211        for f in &rust_frags {
212            let path = Path::new(f.path());
213            let stem = path
214                .file_stem()
215                .map(|s| s.to_string_lossy().to_lowercase())
216                .unwrap_or_default();
217            name_to_frags
218                .entry(stem.clone())
219                .or_default()
220                .push(f.id.clone());
221
222            if stem == "mod" || stem == "lib" {
223                if let Some(parent_name) = path.parent().and_then(|p| p.file_name()) {
224                    mod_to_frags
225                        .entry(parent_name.to_string_lossy().to_lowercase())
226                        .or_default()
227                        .push(f.id.clone());
228                }
229            } else {
230                mod_to_frags.entry(stem).or_default().push(f.id.clone());
231            }
232
233            let (funcs, types) = extract_definitions(&f.content);
234            for t in types {
235                type_defs
236                    .entry(t.to_lowercase())
237                    .or_default()
238                    .push(f.id.clone());
239            }
240            for func in funcs {
241                fn_defs
242                    .entry(func.to_lowercase())
243                    .or_default()
244                    .push(f.id.clone());
245            }
246
247            for mod_name in extract_mods(&f.content) {
248                mod_to_frags
249                    .entry(mod_name.to_lowercase())
250                    .or_default()
251                    .push(f.id.clone());
252            }
253
254            let impls = extract_trait_impls(&f.content);
255            if !impls.is_empty() {
256                trait_impls.insert(f.id.clone(), impls);
257            }
258
259            for pub_use_path in extract_pub_uses(&f.content) {
260                let leaf_lower = pub_use_path.split("::").last().unwrap_or("").to_lowercase();
261                if !leaf_lower.is_empty() && !name_to_frags.contains_key(&leaf_lower) {
262                    let has_target = type_defs
263                        .get(&leaf_lower)
264                        .map_or(false, |v| v.iter().any(|fid| fid != &f.id))
265                        || fn_defs
266                            .get(&leaf_lower)
267                            .map_or(false, |v| v.iter().any(|fid| fid != &f.id));
268                    if has_target {
269                        name_to_frags
270                            .entry(leaf_lower)
271                            .or_default()
272                            .push(f.id.clone());
273                    }
274                }
275            }
276        }
277
278        let mut edges: EdgeDict = FxHashMap::default();
279
280        for (impl_fid, pairs) in &trait_impls {
281            for (trait_name, _type_name) in pairs {
282                for trait_fid in type_defs.get(&trait_name.to_lowercase()).unwrap_or(&vec![]) {
283                    if trait_fid != impl_fid {
284                        add_edge(&mut edges, impl_fid, trait_fid, type_weight, reverse_factor);
285                    }
286                }
287            }
288        }
289
290        for f in &rust_frags {
291            for pub_use_path in extract_pub_uses(&f.content) {
292                let leaf_lower = pub_use_path.split("::").last().unwrap_or("").to_lowercase();
293                for target_list in [type_defs.get(&leaf_lower), fn_defs.get(&leaf_lower)]
294                    .iter()
295                    .flatten()
296                {
297                    for target_fid in *target_list {
298                        if target_fid != &f.id {
299                            add_edge(&mut edges, &f.id, target_fid, use_weight, reverse_factor);
300                        }
301                    }
302                }
303            }
304        }
305
306        for rf in &rust_frags {
307            let (type_refs, fn_calls, path_calls) = extract_references(&rf.content);
308
309            for use_path in extract_uses(&rf.content) {
310                for part in use_path.split("::") {
311                    let part_lower = part.to_lowercase();
312                    add_edges_from_ids(
313                        &mut edges,
314                        &rf.id,
315                        mod_to_frags.get(&part_lower).unwrap_or(&vec![]),
316                        use_weight,
317                        reverse_factor,
318                    );
319                    add_edges_from_ids(
320                        &mut edges,
321                        &rf.id,
322                        name_to_frags.get(&part_lower).unwrap_or(&vec![]),
323                        use_weight,
324                        reverse_factor,
325                    );
326                }
327            }
328
329            for mod_name in extract_mods(&rf.content) {
330                for fid in name_to_frags
331                    .get(&mod_name.to_lowercase())
332                    .unwrap_or(&vec![])
333                {
334                    if fid != &rf.id {
335                        add_edge(&mut edges, &rf.id, fid, mod_weight, reverse_factor);
336                    }
337                }
338            }
339
340            for type_ref in &type_refs {
341                for fid in type_defs.get(&type_ref.to_lowercase()).unwrap_or(&vec![]) {
342                    if fid != &rf.id {
343                        add_edge(&mut edges, &rf.id, fid, type_weight, reverse_factor);
344                    }
345                }
346            }
347
348            for fn_call in &fn_calls {
349                for fid in fn_defs.get(&fn_call.to_lowercase()).unwrap_or(&vec![]) {
350                    if fid != &rf.id {
351                        add_edge(&mut edges, &rf.id, fid, fn_weight, reverse_factor);
352                    }
353                }
354            }
355
356            for (mod_name, _symbol) in &path_calls {
357                for fid in mod_to_frags
358                    .get(&mod_name.to_lowercase())
359                    .unwrap_or(&vec![])
360                {
361                    if fid != &rf.id {
362                        add_edge(&mut edges, &rf.id, fid, use_weight, reverse_factor);
363                    }
364                }
365            }
366
367            let stem = Path::new(rf.path())
368                .file_stem()
369                .map(|s| s.to_string_lossy().to_lowercase())
370                .unwrap_or_default();
371            if stem == "lib" || stem == "mod" {
372                let parent_dir = Path::new(rf.path()).parent();
373                for other in &rust_frags {
374                    if let Some(pd) = parent_dir {
375                        if Path::new(other.path()).parent() == Some(pd) && other.id != rf.id {
376                            add_edge(
377                                &mut edges,
378                                &rf.id,
379                                &other.id,
380                                same_crate_weight,
381                                reverse_factor,
382                            );
383                        }
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 rust_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_rust_file(f)).collect();
400        if rust_changed.is_empty() {
401            return vec![];
402        }
403
404        let rust_candidates: Vec<PathBuf> = candidates
405            .iter()
406            .filter(|c| is_rust_file(c))
407            .cloned()
408            .collect();
409
410        let mut mod_name_to_files: FxHashMap<String, Vec<PathBuf>> = FxHashMap::default();
411        let mut file_uses: FxHashMap<PathBuf, FxHashSet<String>> = FxHashMap::default();
412        let mut file_mods: FxHashMap<PathBuf, FxHashSet<String>> = FxHashMap::default();
413
414        for candidate in &rust_candidates {
415            mod_name_to_files
416                .entry(stem_to_mod_name(candidate))
417                .or_default()
418                .push(candidate.clone());
419            if let Some(content) = base::read_file_cached(candidate, file_cache) {
420                file_uses.insert(candidate.clone(), extract_uses(&content));
421                file_mods.insert(candidate.clone(), extract_mods(&content));
422            }
423        }
424
425        let changed_set: FxHashSet<PathBuf> = changed.iter().cloned().collect();
426        let mut discovered: FxHashSet<PathBuf> = FxHashSet::default();
427        let mut frontier: FxHashSet<PathBuf> = rust_changed.iter().map(|f| (*f).clone()).collect();
428
429        for _ in 0..SEMANTIC_DISCOVERY.max_depth {
430            let skip: FxHashSet<PathBuf> = changed_set.union(&discovered).cloned().collect();
431            let frontier_mod_names: FxHashSet<String> =
432                frontier.iter().map(|f| stem_to_mod_name(f)).collect();
433
434            let mut forward_targets: FxHashSet<String> = FxHashSet::default();
435            for f in &frontier {
436                if let Some(uses) = file_uses.get(f) {
437                    for use_path in uses {
438                        for part in use_path.split("::") {
439                            forward_targets.insert(part.to_lowercase());
440                        }
441                    }
442                }
443                if let Some(mods) = file_mods.get(f) {
444                    for m in mods {
445                        forward_targets.insert(m.to_lowercase());
446                    }
447                }
448            }
449
450            let mut found: FxHashSet<PathBuf> = FxHashSet::default();
451
452            for target_name in &forward_targets {
453                for candidate in mod_name_to_files.get(target_name).unwrap_or(&vec![]) {
454                    if !skip.contains(candidate) && !found.contains(candidate) {
455                        found.insert(candidate.clone());
456                    }
457                }
458            }
459
460            for candidate in &rust_candidates {
461                if skip.contains(candidate) || found.contains(candidate) {
462                    continue;
463                }
464                let cand_mods = file_mods.get(candidate).cloned().unwrap_or_default();
465                if !cand_mods.is_disjoint(&frontier_mod_names) {
466                    found.insert(candidate.clone());
467                    continue;
468                }
469                if let Some(uses) = file_uses.get(candidate) {
470                    let use_parts: FxHashSet<String> = uses
471                        .iter()
472                        .flat_map(|p| p.split("::").map(|s| s.to_lowercase()))
473                        .collect();
474                    if !use_parts.is_disjoint(&frontier_mod_names) {
475                        found.insert(candidate.clone());
476                    }
477                }
478            }
479
480            if found.is_empty() {
481                break;
482            }
483            discovered.extend(found.iter().cloned());
484            frontier = found;
485        }
486
487        let mut result: Vec<PathBuf> = discovered.into_iter().collect();
488        result.sort();
489        result
490    }
491}