Skip to main content

_diffctx/edges/semantic/
rust_lang.rs

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