Skip to main content

_diffctx/edges/semantic/
go.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::{GO_SEMANTIC, SEMANTIC_DISCOVERY};
11use crate::config::extensions::GO_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_go_file(path: &Path) -> bool {
19    let ext = base::file_ext(path);
20    GO_EXTENSIONS.contains(ext.as_str())
21}
22
23static IMPORT_RE: Lazy<Regex> =
24    Lazy::new(|| Regex::new(r#"(?m)^\s*(?:import\s+)?"([^"]+)""#).unwrap());
25static PACKAGE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*package\s+(\w+)").unwrap());
26static TYPE_DEF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*type\s+([A-Z]\w*)").unwrap());
27static FUNC_DEF_RE: Lazy<Regex> =
28    Lazy::new(|| Regex::new(r"(?m)^\s*func\s+(?:\([^)]*\)\s+)?([A-Z]\w*)\s*\(").unwrap());
29static FUNC_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\s*\(").unwrap());
30static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
31static PKG_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([a-z]\w+)\.([A-Z]\w*)").unwrap());
32static INIT_FUNC_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*func\s+init\s*\(").unwrap());
33
34fn extract_imports(content: &str) -> FxHashSet<String> {
35    IMPORT_RE
36        .captures_iter(content)
37        .map(|c| c[1].to_string())
38        .collect()
39}
40
41/// None when the fragment carries no `package` line — i.e. every
42/// function-body fragment. The old `"main"` fallback funneled all of them
43/// into one bucket, and the same-package loop cross-linked essentially the
44/// whole Go universe through it: 103.8M edges on a gitpod-scale monorepo
45/// (#116).
46fn get_package_name(content: &str) -> Option<String> {
47    PACKAGE_RE.captures(content).map(|c| c[1].to_string())
48}
49
50fn extract_definitions(content: &str) -> (FxHashSet<String>, FxHashSet<String>) {
51    let funcs: FxHashSet<String> = FUNC_DEF_RE
52        .captures_iter(content)
53        .map(|c| c[1].to_string())
54        .collect();
55    let types: FxHashSet<String> = TYPE_DEF_RE
56        .captures_iter(content)
57        .map(|c| c[1].to_string())
58        .collect();
59    (funcs, types)
60}
61
62fn extract_references(
63    content: &str,
64) -> (
65    FxHashSet<String>,
66    FxHashSet<String>,
67    FxHashSet<(String, String)>,
68) {
69    let func_calls: FxHashSet<String> = FUNC_CALL_RE
70        .captures_iter(content)
71        .map(|c| c[1].to_string())
72        .collect();
73    let type_refs: FxHashSet<String> = TYPE_REF_RE
74        .captures_iter(content)
75        .map(|c| c[1].to_string())
76        .collect();
77    let pkg_calls: FxHashSet<(String, String)> = PKG_CALL_RE
78        .captures_iter(content)
79        .map(|c| (c[1].to_string(), c[2].to_string()))
80        .collect();
81    (func_calls, type_refs, pkg_calls)
82}
83
84fn has_init_func(content: &str) -> bool {
85    INIT_FUNC_RE.is_match(content)
86}
87
88pub struct GoEdgeBuilder;
89
90impl GoEdgeBuilder {
91    fn build_indices<'a>(
92        &self,
93        go_frags: &'a [&'a Fragment],
94        repo_root: Option<&Path>,
95    ) -> (
96        FxHashMap<String, Vec<FragmentId>>,
97        FxHashMap<String, Vec<FragmentId>>,
98        FxHashMap<String, Vec<FragmentId>>,
99        FxHashMap<String, Vec<FragmentId>>,
100        FxHashMap<String, FxHashSet<&'a str>>,
101        FxHashMap<String, FxHashSet<&'a str>>,
102    ) {
103        let mut pkg_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
104        let mut path_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
105        let mut type_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
106        let mut func_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
107        let mut def_files: FxHashMap<String, FxHashSet<&str>> = FxHashMap::default();
108        let mut pkg_files: FxHashMap<String, FxHashSet<&str>> = FxHashMap::default();
109
110        for f in go_frags {
111            if let Some(pkg) = get_package_name(&f.content) {
112                let lower = pkg.to_lowercase();
113                pkg_files.entry(lower.clone()).or_default().insert(f.path());
114                pkg_to_frags.entry(lower).or_default().push(f.id.clone());
115            }
116
117            if let Some(root) = repo_root {
118                if let Ok(rel) = Path::new(f.path()).strip_prefix(root) {
119                    if let Some(parent) = rel.parent() {
120                        path_to_frags
121                            .entry(parent.to_string_lossy().to_string())
122                            .or_default()
123                            .push(f.id.clone());
124                    }
125                }
126            }
127
128            let (funcs, types) = extract_definitions(&f.content);
129            for t in types {
130                let lower = t.to_lowercase();
131                def_files.entry(lower.clone()).or_default().insert(f.path());
132                type_defs.entry(lower).or_default().push(f.id.clone());
133            }
134            for func in funcs {
135                let lower = func.to_lowercase();
136                def_files.entry(lower.clone()).or_default().insert(f.path());
137                func_defs.entry(lower).or_default().push(f.id.clone());
138            }
139        }
140
141        (
142            pkg_to_frags,
143            path_to_frags,
144            type_defs,
145            func_defs,
146            def_files,
147            pkg_files,
148        )
149    }
150}
151
152impl EdgeBuilder for GoEdgeBuilder {
153    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
154        let go_frags: Vec<&Fragment> = fragments
155            .iter()
156            .filter(|f| is_go_file(Path::new(f.path())))
157            .collect();
158        if go_frags.is_empty() {
159            return FxHashMap::default();
160        }
161
162        let import_weight = EDGE_WEIGHTS["go_import"].forward;
163        let type_weight = EDGE_WEIGHTS["go_type"].forward;
164        let func_weight = EDGE_WEIGHTS["go_func"].forward;
165        let same_package_weight = EDGE_WEIGHTS["go_same_package"].forward;
166        let reverse_factor = EDGE_WEIGHTS["go_import"].reverse_factor;
167        let init_same_package_weight = GO_SEMANTIC.init_same_package_weight;
168
169        let (pkg_to_frags, path_to_frags, type_defs, func_defs, def_files, pkg_files) =
170            self.build_indices(&go_frags, repo_root);
171
172        let name_capped = |name: &str| {
173            def_files
174                .get(name)
175                .is_some_and(|s| s.len() > MAX_FILES_PER_NAME)
176        };
177        let pkg_capped = |name: &str| {
178            pkg_files
179                .get(name)
180                .is_some_and(|s| s.len() > MAX_FILES_PER_NAME)
181        };
182
183        let mut edges: EdgeDict = FxHashMap::default();
184
185        let mut path_last_comp: FxHashMap<&str, Vec<&String>> = FxHashMap::default();
186        for path_str in path_to_frags.keys() {
187            if let Some(last) = path_str.rsplit('/').next() {
188                path_last_comp.entry(last).or_default().push(path_str);
189            }
190        }
191
192        for gf in &go_frags {
193            let imports = extract_imports(&gf.content);
194            let (func_calls, type_refs, pkg_calls) = extract_references(&gf.content);
195
196            for imp in &imports {
197                let imp_pkg = imp.split('/').next_back().unwrap_or(imp).to_lowercase();
198                // Direct lookup: the previous full-map scan was
199                // O(imports x packages) and cost ~40s alone on a
200                // kubernetes-scale commit (#196).
201                if !pkg_capped(&imp_pkg) {
202                    if let Some(frag_ids) = pkg_to_frags.get(&imp_pkg) {
203                        add_edges_from_ids(
204                            &mut edges,
205                            &gf.id,
206                            frag_ids,
207                            import_weight,
208                            reverse_factor,
209                        );
210                    }
211                }
212                // Last-component index instead of a full scan: the old form
213                // was O(imports x dirs) with two String allocations per probe
214                // and stood at ~39s alone on a kubernetes commit (#196). A
215                // dir can only match if its last component appears as a
216                // component of the import, so only that posting list is
217                // verified against the full predicate.
218                for part in imp.split('/') {
219                    let Some(dirs) = path_last_comp.get(part) else {
220                        continue;
221                    };
222                    for path_str in dirs {
223                        if *imp == **path_str
224                            || imp.ends_with(&format!("/{}", path_str))
225                            || imp.contains(&format!("/{}/", path_str))
226                        {
227                            if let Some(frag_ids) = path_to_frags.get(*path_str) {
228                                add_edges_from_ids(
229                                    &mut edges,
230                                    &gf.id,
231                                    frag_ids,
232                                    import_weight,
233                                    reverse_factor,
234                                );
235                            }
236                        }
237                    }
238                }
239            }
240
241            for type_ref in &type_refs {
242                let lower = type_ref.to_lowercase();
243                if name_capped(&lower) {
244                    continue;
245                }
246                for fid in type_defs.get(&lower).unwrap_or(&vec![]) {
247                    if fid != &gf.id {
248                        add_edge(&mut edges, &gf.id, fid, type_weight, reverse_factor);
249                    }
250                }
251            }
252
253            for func_call in &func_calls {
254                let lower = func_call.to_lowercase();
255                if name_capped(&lower) {
256                    continue;
257                }
258                for fid in func_defs.get(&lower).unwrap_or(&vec![]) {
259                    if fid != &gf.id {
260                        add_edge(&mut edges, &gf.id, fid, func_weight, reverse_factor);
261                    }
262                }
263            }
264
265            for (pkg_name, _symbol) in &pkg_calls {
266                let lower = pkg_name.to_lowercase();
267                if pkg_capped(&lower) {
268                    continue;
269                }
270                for fid in pkg_to_frags.get(&lower).unwrap_or(&vec![]) {
271                    if fid != &gf.id {
272                        add_edge(&mut edges, &gf.id, fid, func_weight, reverse_factor);
273                    }
274                }
275            }
276
277            let has_init = has_init_func(&gf.content);
278            let sp_weight = if has_init {
279                init_same_package_weight
280            } else {
281                same_package_weight
282            };
283            if let Some(current_pkg) = get_package_name(&gf.content) {
284                for fid in pkg_to_frags
285                    .get(&current_pkg.to_lowercase())
286                    .unwrap_or(&vec![])
287                {
288                    if fid != &gf.id {
289                        add_edge(&mut edges, &gf.id, fid, sp_weight, reverse_factor);
290                    }
291                }
292            }
293        }
294
295        edges
296    }
297
298    fn discover_related_files(
299        &self,
300        changed: &[PathBuf],
301        candidates: &[PathBuf],
302        _repo_root: Option<&Path>,
303        file_cache: Option<&FxHashMap<PathBuf, String>>,
304    ) -> Vec<PathBuf> {
305        let go_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_go_file(f)).collect();
306        if go_changed.is_empty() {
307            return vec![];
308        }
309
310        let changed_set: FxHashSet<PathBuf> = changed.iter().cloned().collect();
311        let go_candidates: Vec<PathBuf> = candidates
312            .iter()
313            .filter(|c| !changed_set.contains(*c) && is_go_file(c))
314            .cloned()
315            .collect();
316
317        let mut discovered: FxHashSet<PathBuf> = FxHashSet::default();
318
319        let pkg_dirs: FxHashSet<PathBuf> = go_changed
320            .iter()
321            .filter_map(|f| f.parent().map(|p| p.to_path_buf()))
322            .collect();
323        for c in &go_candidates {
324            if let Some(parent) = c.parent() {
325                if pkg_dirs.contains(&parent.to_path_buf()) {
326                    discovered.insert(c.clone());
327                }
328            }
329        }
330
331        let mut candidate_index: FxHashMap<PathBuf, (String, FxHashSet<String>)> =
332            FxHashMap::default();
333        for c in &go_candidates {
334            let content = base::read_file_cached(c, file_cache);
335            if let Some(content) = content {
336                let Some(pkg) = get_package_name(&content).map(|p| p.to_lowercase()) else {
337                    continue;
338                };
339                let imports = extract_imports(&content);
340                candidate_index.insert(c.clone(), (pkg, imports));
341            }
342        }
343
344        let mut frontier: FxHashSet<PathBuf> = go_changed.iter().map(|f| (*f).clone()).collect();
345
346        for _ in 0..SEMANTIC_DISCOVERY.max_depth {
347            let mut next_frontier: FxHashSet<PathBuf> = FxHashSet::default();
348            for f in &frontier {
349                let content = base::read_file_cached(f, file_cache);
350                if let Some(content) = content {
351                    let f_imports = extract_imports(&content);
352                    let Some(f_pkg) = get_package_name(&content).map(|p| p.to_lowercase()) else {
353                        continue;
354                    };
355
356                    for c in &go_candidates {
357                        if changed_set.contains(c) || discovered.contains(c) {
358                            continue;
359                        }
360                        if let Some((c_pkg, c_imports)) = candidate_index.get(c) {
361                            let forward_match = f_imports.iter().any(|imp| {
362                                imp.split('/').next_back().unwrap_or(imp).to_lowercase() == *c_pkg
363                            });
364                            let reverse_match = c_imports.iter().any(|imp| {
365                                imp.split('/').next_back().unwrap_or(imp).to_lowercase() == f_pkg
366                            });
367                            if forward_match || reverse_match {
368                                discovered.insert(c.clone());
369                                next_frontier.insert(c.clone());
370                            }
371                        }
372                    }
373                }
374            }
375            if next_frontier.is_empty() {
376                break;
377            }
378            frontier = next_frontier;
379        }
380
381        let mut result: Vec<PathBuf> = discovered.into_iter().collect();
382        result.sort();
383        result
384    }
385}