_diffctx/edges/semantic/
cargo_edges.rs1use 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}