Skip to main content

_diffctx/edges/semantic/
bazel.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;
9
10use super::super::EdgeDict;
11use super::super::base::{
12    self, EdgeBuilder, FragmentIndex, add_edge, discover_files_by_refs, link_by_path_match,
13};
14
15static BAZEL_NAMES: Lazy<FxHashSet<&str>> = Lazy::new(|| {
16    ["BUILD", "BUILD.bazel", "WORKSPACE", "WORKSPACE.bazel"]
17        .iter()
18        .copied()
19        .collect()
20});
21
22static DEPS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"["']([/@][^"']{1,300})["']"#).unwrap());
23static LOAD_RE: Lazy<Regex> =
24    Lazy::new(|| Regex::new(r#"load\(\s*["']([^"']{1,300})["']\s*,"#).unwrap());
25static SRCS_RE: Lazy<Regex> =
26    Lazy::new(|| Regex::new(r#"["']([^"']{1,300}\.\w{1,10})["']"#).unwrap());
27static LABEL_RE: Lazy<Regex> =
28    Lazy::new(|| Regex::new(r#"//([^:"']{1,200}):([^"'\s,\]]{1,200})"#).unwrap());
29
30fn is_bazel_file(path: &Path) -> bool {
31    let name = path
32        .file_name()
33        .map(|n| n.to_string_lossy().to_string())
34        .unwrap_or_default();
35    if BAZEL_NAMES.contains(name.as_str()) {
36        return true;
37    }
38    let ext = base::file_ext(path);
39    ext == ".bzl" || ext == ".bazel"
40}
41
42fn extract_labels(content: &str) -> FxHashSet<String> {
43    DEPS_RE
44        .captures_iter(content)
45        .map(|c| c[1].to_string())
46        .collect()
47}
48
49fn extract_loads(content: &str) -> FxHashSet<String> {
50    LOAD_RE
51        .captures_iter(content)
52        .map(|c| c[1].to_string())
53        .collect()
54}
55
56fn extract_srcs(content: &str) -> FxHashSet<String> {
57    let mut srcs = FxHashSet::default();
58    let mut in_srcs = false;
59    for line in content.lines() {
60        let stripped = line.trim();
61        if stripped.contains("srcs") && stripped.contains('=') {
62            in_srcs = true;
63        }
64        if in_srcs {
65            for m in SRCS_RE.captures_iter(line) {
66                srcs.insert(m[1].to_string());
67            }
68            if stripped.contains(']') {
69                in_srcs = false;
70            }
71        }
72    }
73    srcs
74}
75
76fn label_to_path(label: &str) -> Option<String> {
77    if let Some(c) = LABEL_RE.captures(label) {
78        return Some(c[1].to_string());
79    }
80    if label.starts_with("//") {
81        let cleaned = label
82            .trim_start_matches('/')
83            .split(':')
84            .next()
85            .unwrap_or("");
86        if !cleaned.is_empty() {
87            return Some(cleaned.to_string());
88        }
89    }
90    None
91}
92
93fn ref_to_filename(r: &str) -> String {
94    r.trim_end_matches('/')
95        .split('/')
96        .next_back()
97        .unwrap_or(r)
98        .split(':')
99        .next_back()
100        .unwrap_or(r)
101        .to_lowercase()
102}
103
104pub struct BazelEdgeBuilder;
105
106impl EdgeBuilder for BazelEdgeBuilder {
107    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
108        let frags: Vec<&Fragment> = fragments
109            .iter()
110            .filter(|f| is_bazel_file(Path::new(f.path())))
111            .collect();
112        if frags.is_empty() {
113            return FxHashMap::default();
114        }
115
116        let deps_w = EDGE_WEIGHTS["bazel_deps"].forward;
117        let load_w = EDGE_WEIGHTS["bazel_load"].forward;
118        let srcs_w = EDGE_WEIGHTS["bazel_srcs"].forward;
119        let rev = EDGE_WEIGHTS["bazel_deps"].reverse_factor;
120
121        let idx = FragmentIndex::new(fragments, repo_root);
122        let mut edges: EdgeDict = FxHashMap::default();
123
124        for bf in &frags {
125            for label in extract_labels(&bf.content) {
126                if let Some(path) = label_to_path(&label) {
127                    for build_name in ["BUILD", "BUILD.bazel"] {
128                        link_by_path_match(
129                            &bf.id,
130                            &format!("{}/{}", path, build_name),
131                            &idx,
132                            &mut edges,
133                            deps_w,
134                            rev,
135                        );
136                    }
137                    link_by_path_match(&bf.id, &path, &idx, &mut edges, deps_w, rev);
138                }
139            }
140
141            for load in extract_loads(&bf.content) {
142                let filename = ref_to_filename(&load);
143                let stem = filename.strip_suffix(".bzl").unwrap_or(&filename);
144                let mut linked = false;
145                for (name, frag_ids) in &idx.by_name {
146                    if name == &filename || name == stem {
147                        for fid in frag_ids {
148                            if fid != &bf.id {
149                                add_edge(&mut edges, &bf.id, fid, load_w, rev);
150                                linked = true;
151                                break;
152                            }
153                        }
154                        if linked {
155                            break;
156                        }
157                    }
158                }
159                if !linked {
160                    if let Some(path) = label_to_path(&load) {
161                        link_by_path_match(&bf.id, &path, &idx, &mut edges, load_w, rev);
162                    }
163                }
164            }
165
166            let build_parent = Path::new(bf.path()).parent().unwrap_or(Path::new(""));
167            for src in extract_srcs(&bf.content) {
168                let src_lower = src.to_lowercase();
169                let mut found = false;
170                if let Some(frag_ids) = idx.by_name.get(&src_lower) {
171                    for fid in frag_ids {
172                        if fid == &bf.id {
173                            continue;
174                        }
175                        let frag_parent = Path::new(fid.path.as_ref()).parent();
176                        if frag_parent == Some(build_parent) {
177                            add_edge(&mut edges, &bf.id, fid, srcs_w, rev);
178                            found = true;
179                            break;
180                        }
181                    }
182                }
183                if !found {
184                    let rel = build_parent.join(&src).to_string_lossy().to_string();
185                    link_by_path_match(&bf.id, &rel, &idx, &mut edges, srcs_w, rev);
186                }
187            }
188        }
189        edges
190    }
191
192    fn discover_related_files(
193        &self,
194        changed: &[PathBuf],
195        candidates: &[PathBuf],
196        repo_root: Option<&Path>,
197        file_cache: Option<&FxHashMap<PathBuf, String>>,
198    ) -> Vec<PathBuf> {
199        let bazel_changed: Vec<&PathBuf> = changed.iter().filter(|p| is_bazel_file(p)).collect();
200        if bazel_changed.is_empty() {
201            return vec![];
202        }
203
204        let mut refs = FxHashSet::default();
205        for f in &bazel_changed {
206            let content = match base::read_file_cached(f, file_cache) {
207                Some(c) => c,
208                None => continue,
209            };
210            for label in extract_labels(&content) {
211                if let Some(path) = label_to_path(&label) {
212                    refs.insert(format!("{}/BUILD", path));
213                    refs.insert(format!("{}/BUILD.bazel", path));
214                    refs.insert(path);
215                }
216            }
217            for load in extract_loads(&content) {
218                if let Some(path) = label_to_path(&load) {
219                    refs.insert(path);
220                }
221                refs.insert(ref_to_filename(&load));
222            }
223            for src in extract_srcs(&content) {
224                let parent = f.parent().unwrap_or(Path::new(""));
225                refs.insert(parent.join(&src).to_string_lossy().to_string());
226                refs.insert(src);
227            }
228        }
229
230        let changed_names: FxHashSet<String> = bazel_changed
231            .iter()
232            .filter_map(|f| f.file_name().map(|n| n.to_string_lossy().to_lowercase()))
233            .collect();
234        let mut changed_paths: FxHashSet<String> = FxHashSet::default();
235        for f in &bazel_changed {
236            changed_paths.insert(f.to_string_lossy().to_string());
237            if base::file_ext(f) == ".bzl" {
238                if let Some(stem) = f.file_stem() {
239                    changed_paths.insert(stem.to_string_lossy().to_string());
240                }
241            }
242        }
243
244        for candidate in candidates {
245            if !is_bazel_file(candidate) {
246                continue;
247            }
248            if let Some(content) = base::read_file_cached(candidate, file_cache) {
249                for load in extract_loads(&content) {
250                    let load_file = ref_to_filename(&load);
251                    if changed_names.contains(&load_file)
252                        || changed_paths.iter().any(|cp| load.contains(cp.as_str()))
253                    {
254                        if let Some(name) = candidate.file_name() {
255                            refs.insert(name.to_string_lossy().to_lowercase());
256                        }
257                    }
258                }
259            }
260        }
261
262        discover_files_by_refs(&refs, changed, candidates, repo_root)
263    }
264
265    fn category_label(&self) -> Option<&str> {
266        Some("semantic")
267    }
268}