Skip to main content

_diffctx/edges/semantic/
cargo_edges.rs

1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::weights::EDGE_WEIGHTS;
8use crate::types::{Fragment, FragmentId};
9
10use super::super::EdgeDict;
11use super::super::base::{self, EdgeBuilder, FragmentIndex, add_edge, link_by_name};
12
13static WORKSPACE_MEMBERS_RE: Lazy<Regex> =
14    Lazy::new(|| Regex::new(r"(?s)\[workspace\][^\[]*?members\s*=\s*\[(.*?)\]").unwrap());
15static PATH_DEP_RE: Lazy<Regex> = Lazy::new(|| {
16    Regex::new(r##"(?m)^\s*(\w[\w-]{0,100})\s*=\s*\{[^}]*?path\s*=\s*["']([^"']{1,300})["']"##)
17        .unwrap()
18});
19static FEATURES_SECTION_RE: Lazy<Regex> =
20    Lazy::new(|| Regex::new(r"(?ms)^\[features\]\s*\n(.+?)(?:\n\[|\z)").unwrap());
21static FEATURE_DEP_RE: Lazy<Regex> =
22    Lazy::new(|| Regex::new(r##"["'](\w[\w-]{0,100})(?:/[^"']*)?["']"##).unwrap());
23static STRING_ITEM_RE: Lazy<Regex> =
24    Lazy::new(|| Regex::new(r##"["']([^"']{1,300})["']"##).unwrap());
25static BIN_SECTION_RE: Lazy<Regex> = Lazy::new(|| {
26    Regex::new(r##"(?s)\[\[bin\]\][^\[]*?path\s*=\s*["']([^"']{1,300})["']"##).unwrap()
27});
28static LIB_SECTION_RE: Lazy<Regex> =
29    Lazy::new(|| Regex::new(r##"(?s)\[lib\][^\[]*?path\s*=\s*["']([^"']{1,300})["']"##).unwrap());
30
31fn is_cargo_toml(path: &Path) -> bool {
32    path.file_name()
33        .map(|n| n.to_string_lossy().to_lowercase() == "cargo.toml")
34        .unwrap_or(false)
35}
36
37fn is_rust_source(path: &Path) -> bool {
38    base::file_ext(path) == ".rs"
39}
40
41fn extract_workspace_members(content: &str) -> Vec<String> {
42    WORKSPACE_MEMBERS_RE
43        .captures(content)
44        .map(|c| {
45            STRING_ITEM_RE
46                .captures_iter(&c[1])
47                .map(|m| m[1].to_string())
48                .collect()
49        })
50        .unwrap_or_default()
51}
52
53fn extract_path_deps(content: &str) -> Vec<(String, String)> {
54    PATH_DEP_RE
55        .captures_iter(content)
56        .map(|c| (c[1].to_string(), c[2].to_string()))
57        .collect()
58}
59
60fn extract_feature_deps(content: &str) -> FxHashSet<String> {
61    FEATURES_SECTION_RE
62        .captures(content)
63        .map(|c| {
64            FEATURE_DEP_RE
65                .captures_iter(&c[1])
66                .map(|m| m[1].to_string())
67                .collect()
68        })
69        .unwrap_or_default()
70}
71
72fn extract_entry_points(content: &str) -> Vec<String> {
73    let mut entries: Vec<String> = BIN_SECTION_RE
74        .captures_iter(content)
75        .map(|c| c[1].to_string())
76        .collect();
77    if let Some(c) = LIB_SECTION_RE.captures(content) {
78        entries.push(c[1].to_string());
79    }
80    for default in ["src/lib.rs", "src/main.rs"] {
81        if !entries.iter().any(|e| e == default) {
82            entries.push(default.to_string());
83        }
84    }
85    entries
86}
87
88pub struct CargoEdgeBuilder;
89
90impl EdgeBuilder for CargoEdgeBuilder {
91    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
92        let cargo_frags: Vec<&Fragment> = fragments
93            .iter()
94            .filter(|f| is_cargo_toml(Path::new(f.path())))
95            .collect();
96        if cargo_frags.is_empty() {
97            return FxHashMap::default();
98        }
99
100        let ws_w = EDGE_WEIGHTS["cargo_workspace"].forward;
101        let dep_w = EDGE_WEIGHTS["cargo_path_dep"].forward;
102        let entry_w = EDGE_WEIGHTS["cargo_entry_point"].forward;
103        let rev = EDGE_WEIGHTS["cargo_workspace"].reverse_factor;
104
105        let idx = FragmentIndex::new(fragments, repo_root);
106        let mut edges: EdgeDict = FxHashMap::default();
107
108        let mut cargo_by_dir: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
109        for f in &cargo_frags {
110            let dir = Path::new(f.path())
111                .parent()
112                .map(|p| p.to_string_lossy().to_string())
113                .unwrap_or_default();
114            cargo_by_dir.entry(dir).or_default().push(f.id.clone());
115        }
116
117        let mut rs_by_path: FxHashMap<String, FragmentId> = FxHashMap::default();
118        for f in fragments {
119            if is_rust_source(Path::new(f.path())) {
120                rs_by_path.insert(f.path().to_string(), f.id.clone());
121            }
122        }
123
124        for cf in &cargo_frags {
125            let parent = Path::new(cf.path()).parent().unwrap_or(Path::new(""));
126
127            for entry in extract_entry_points(&cf.content) {
128                let entry_path = parent.join(&entry).to_string_lossy().to_string();
129                if let Some(fid) = rs_by_path.get(&entry_path) {
130                    if fid != &cf.id {
131                        add_edge(&mut edges, &cf.id, fid, entry_w, rev);
132                    }
133                }
134            }
135
136            for (_, rel_path) in extract_path_deps(&cf.content) {
137                let dep_dir = parent.join(&rel_path).to_string_lossy().to_string();
138                if let Some(fids) = cargo_by_dir.get(&dep_dir) {
139                    for fid in fids {
140                        if fid != &cf.id {
141                            add_edge(&mut edges, &cf.id, fid, dep_w, rev);
142                        }
143                    }
144                }
145            }
146
147            for member in extract_workspace_members(&cf.content) {
148                let member_dir = parent.join(&member).to_string_lossy().to_string();
149                if let Some(fids) = cargo_by_dir.get(&member_dir) {
150                    for fid in fids {
151                        if fid != &cf.id {
152                            add_edge(&mut edges, &cf.id, fid, ws_w, rev);
153                        }
154                    }
155                }
156                link_by_name(
157                    &cf.id,
158                    &format!("{}/Cargo.toml", member),
159                    &idx,
160                    &mut edges,
161                    ws_w,
162                    rev,
163                );
164            }
165
166            let feature_deps = extract_feature_deps(&cf.content);
167            let path_deps = extract_path_deps(&cf.content);
168            for dep_name in &feature_deps {
169                for (name, rel_path) in &path_deps {
170                    if name != dep_name {
171                        continue;
172                    }
173                    let dep_dir = parent.join(rel_path).to_string_lossy().to_string();
174                    if let Some(fids) = cargo_by_dir.get(&dep_dir) {
175                        for fid in fids {
176                            if fid != &cf.id {
177                                add_edge(&mut edges, &cf.id, fid, dep_w, rev);
178                            }
179                        }
180                    }
181                }
182            }
183        }
184        edges
185    }
186
187    fn discover_related_files(
188        &self,
189        changed: &[PathBuf],
190        candidates: &[PathBuf],
191        repo_root: Option<&Path>,
192        file_cache: Option<&FxHashMap<PathBuf, String>>,
193    ) -> Vec<PathBuf> {
194        let cargo_changed: Vec<&PathBuf> = changed.iter().filter(|p| is_cargo_toml(p)).collect();
195        if cargo_changed.is_empty() {
196            return vec![];
197        }
198
199        let mut refs = FxHashSet::default();
200
201        for cf in &cargo_changed {
202            let content = match base::read_file_cached(cf, file_cache) {
203                Some(c) => c,
204                None => continue,
205            };
206            let parent = cf.parent().unwrap_or(Path::new(""));
207
208            for entry in extract_entry_points(&content) {
209                refs.insert(parent.join(&entry).to_string_lossy().to_string());
210            }
211            for (_, rel_path) in extract_path_deps(&content) {
212                refs.insert(
213                    parent
214                        .join(&rel_path)
215                        .join("Cargo.toml")
216                        .to_string_lossy()
217                        .to_string(),
218                );
219            }
220            for member in extract_workspace_members(&content) {
221                refs.insert(
222                    parent
223                        .join(&member)
224                        .join("Cargo.toml")
225                        .to_string_lossy()
226                        .to_string(),
227                );
228            }
229        }
230
231        base::discover_files_by_refs(&refs, changed, candidates, repo_root)
232    }
233
234    fn category_label(&self) -> Option<&str> {
235        Some("semantic")
236    }
237}