1use std::path::{Path, PathBuf};
2
3const 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
41fn 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 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 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(¤t_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}