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}