1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::edge_weights::SEMANTIC_DISCOVERY;
8use crate::config::extensions::{
9 JAVA_EXTENSIONS, JVM_EXTENSIONS, KOTLIN_EXTENSIONS, SCALA_EXTENSIONS,
10};
11use crate::config::weights::EDGE_WEIGHTS;
12use crate::types::{Fragment, FragmentId};
13
14use super::super::EdgeDict;
15use super::super::base::{self, EdgeBuilder, add_edge};
16
17fn is_jvm_file(path: &Path) -> bool {
18 let ext = base::file_ext(path);
19 JVM_EXTENSIONS.contains(ext.as_str())
20}
21
22fn is_java(path: &Path) -> bool {
23 let ext = base::file_ext(path);
24 JAVA_EXTENSIONS.contains(ext.as_str())
25}
26
27fn is_kotlin(path: &Path) -> bool {
28 let ext = base::file_ext(path);
29 KOTLIN_EXTENSIONS.contains(ext.as_str())
30}
31
32fn is_scala(path: &Path) -> bool {
33 let ext = base::file_ext(path);
34 SCALA_EXTENSIONS.contains(ext.as_str())
35}
36
37static KOTLIN_IMPORT_RE: Lazy<Regex> = Lazy::new(|| {
38 Regex::new(r"(?m)^\s*import\s+([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*(?:\.[A-Z]\w*|\.\*)?)")
39 .unwrap()
40});
41static KOTLIN_CLASS_RE: Lazy<Regex> = Lazy::new(|| {
42 Regex::new(r"(?m)^\s*(?:\w+\s+)*(?:class|interface|object|enum)\s+([A-Z]\w*)").unwrap()
43});
44static JAVA_IMPORT_RE: Lazy<Regex> = Lazy::new(|| {
45 Regex::new(r"(?m)^\s*import\s+(?:static\s+)?([a-z][a-z0-9_.]*(?:\.\*)?)\s*;").unwrap()
46});
47static JAVA_PACKAGE_RE: Lazy<Regex> =
48 Lazy::new(|| Regex::new(r"(?m)^\s*package\s+([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*)").unwrap());
49static JAVA_CLASS_RE: Lazy<Regex> = Lazy::new(|| {
50 Regex::new(r"(?m)^\s*(?:\w+\s+)*(?:class|interface|enum|@interface)\s+([A-Z]\w*)").unwrap()
51});
52static SCALA_IMPORT_RE: Lazy<Regex> = Lazy::new(|| {
53 Regex::new(r"(?m)^\s*import\s+([a-z][a-z0-9_.]+(?:\.[A-Z]\w*|\._|\.\{[^}]+\})?)").unwrap()
54});
55static SCALA_CLASS_RE: Lazy<Regex> =
56 Lazy::new(|| Regex::new(r"(?m)^\s*(?:\w+\s+)*(?:class|trait|object)\s+([A-Z]\w*)").unwrap());
57static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
58static ANNOTATION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"@([A-Z]\w*)").unwrap());
59static KOTLIN_EXTENDS_RE: Lazy<Regex> = Lazy::new(|| {
60 Regex::new(r"(?:class|interface|object)\s+\w+(?:<[^>]*>)?(?:\([^)]*\))?\s*:\s*([^{]+)").unwrap()
61});
62static JAVA_EXTENDS_RE: Lazy<Regex> = Lazy::new(|| {
63 Regex::new(r"(?m)\b(?:extends|implements)\s+([A-Z]\w*(?:\s*,\s*[A-Z]\w*)*)").unwrap()
64});
65static SCALA_EXTENDS_RE: Lazy<Regex> =
66 Lazy::new(|| Regex::new(r"(?m)\b(?:extends|with)\s+([A-Z]\w*)").unwrap());
67
68static JVM_STDLIB_TYPES: Lazy<FxHashSet<&str>> = Lazy::new(|| {
69 [
70 "String",
71 "Integer",
72 "Long",
73 "Double",
74 "Float",
75 "Boolean",
76 "Byte",
77 "Short",
78 "Character",
79 "Object",
80 "Class",
81 "System",
82 "Math",
83 "Collections",
84 "Arrays",
85 "Optional",
86 "HashMap",
87 "ArrayList",
88 "LinkedList",
89 "Iterator",
90 "Iterable",
91 "Comparable",
92 "Runnable",
93 "Thread",
94 "Exception",
95 "RuntimeException",
96 "IllegalArgumentException",
97 "IllegalStateException",
98 "NullPointerException",
99 "IndexOutOfBoundsException",
100 "IOException",
101 "InputStream",
102 "OutputStream",
103 "StringBuilder",
104 "StringBuffer",
105 "Number",
106 "Enum",
107 "Void",
108 "Override",
109 "Unit",
110 "Any",
111 "AnyVal",
112 "AnyRef",
113 "Nothing",
114 "Option",
115 "Some",
116 "Either",
117 "Left",
118 "Right",
119 "Try",
120 "Success",
121 "Failure",
122 "Future",
123 "Promise",
124 "Seq",
125 "Vector",
126 "Map",
127 "Set",
128 "Tuple",
129 "Function",
130 "Product",
131 "Serializable",
132 "Pair",
133 "Triple",
134 "Sequence",
135 ]
136 .iter()
137 .copied()
138 .collect()
139});
140
141fn extract_imports(content: &str, path: &Path) -> FxHashSet<String> {
142 if is_java(path) {
143 JAVA_IMPORT_RE
144 .captures_iter(content)
145 .map(|c| c[1].to_string())
146 .collect()
147 } else if is_kotlin(path) {
148 KOTLIN_IMPORT_RE
149 .captures_iter(content)
150 .map(|c| c[1].to_string())
151 .collect()
152 } else if is_scala(path) {
153 SCALA_IMPORT_RE
154 .captures_iter(content)
155 .map(|c| c[1].to_string())
156 .collect()
157 } else {
158 FxHashSet::default()
159 }
160}
161
162fn extract_classes(content: &str, path: &Path) -> FxHashSet<String> {
163 if is_java(path) {
164 JAVA_CLASS_RE
165 .captures_iter(content)
166 .map(|c| c[1].to_string())
167 .collect()
168 } else if is_kotlin(path) {
169 KOTLIN_CLASS_RE
170 .captures_iter(content)
171 .map(|c| c[1].to_string())
172 .collect()
173 } else if is_scala(path) {
174 SCALA_CLASS_RE
175 .captures_iter(content)
176 .map(|c| c[1].to_string())
177 .collect()
178 } else {
179 FxHashSet::default()
180 }
181}
182
183fn extract_package(content: &str) -> Option<String> {
184 JAVA_PACKAGE_RE.captures(content).map(|c| c[1].to_string())
185}
186
187fn extract_inheritance(content: &str, path: &Path) -> FxHashSet<String> {
188 let mut refs = FxHashSet::default();
189 if is_kotlin(path) {
190 for cap in KOTLIN_EXTENDS_RE.captures_iter(content) {
191 for type_cap in TYPE_REF_RE.captures_iter(&cap[1]) {
192 refs.insert(type_cap[1].to_string());
193 }
194 }
195 } else if is_java(path) {
196 for cap in JAVA_EXTENDS_RE.captures_iter(content) {
197 for type_cap in TYPE_REF_RE.captures_iter(&cap[1]) {
198 refs.insert(type_cap[1].to_string());
199 }
200 }
201 } else if is_scala(path) {
202 for cap in SCALA_EXTENDS_RE.captures_iter(content) {
203 refs.insert(cap[1].to_string());
204 }
205 }
206 refs
207}
208
209fn extract_type_refs(content: &str) -> FxHashSet<String> {
210 TYPE_REF_RE
211 .captures_iter(content)
212 .map(|c| c[1].to_string())
213 .collect()
214}
215
216fn extract_annotations(content: &str) -> FxHashSet<String> {
217 ANNOTATION_RE
218 .captures_iter(content)
219 .map(|c| c[1].to_string())
220 .collect()
221}
222
223pub struct JVMEdgeBuilder;
224
225impl EdgeBuilder for JVMEdgeBuilder {
226 fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
227 let jvm_frags: Vec<&Fragment> = fragments
228 .iter()
229 .filter(|f| is_jvm_file(Path::new(f.path())))
230 .collect();
231 if jvm_frags.is_empty() {
232 return FxHashMap::default();
233 }
234
235 let import_weight = EDGE_WEIGHTS["jvm_import"].forward;
236 let inheritance_weight = EDGE_WEIGHTS["jvm_inheritance"].forward;
237 let type_weight = EDGE_WEIGHTS["jvm_type"].forward;
238 let same_package_weight = EDGE_WEIGHTS["jvm_same_package"].forward;
239 let annotation_weight = EDGE_WEIGHTS["jvm_annotation"].forward;
240 let reverse_factor = EDGE_WEIGHTS["jvm_import"].reverse_factor;
241
242 let mut package_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
243 let mut class_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
244 let mut fqn_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
245
246 for f in &jvm_frags {
247 let path = Path::new(f.path());
248 let pkg = extract_package(&f.content);
249 if let Some(ref pkg) = pkg {
250 package_to_frags
251 .entry(pkg.clone())
252 .or_default()
253 .push(f.id.clone());
254 }
255 for cls in extract_classes(&f.content, path) {
256 class_to_frags
257 .entry(cls.to_lowercase())
258 .or_default()
259 .push(f.id.clone());
260 if let Some(ref pkg) = pkg {
261 fqn_to_frags
262 .entry(format!("{}.{}", pkg, cls).to_lowercase())
263 .or_default()
264 .push(f.id.clone());
265 }
266 }
267 }
268
269 let mut edges: EdgeDict = FxHashMap::default();
270
271 for jf in &jvm_frags {
272 let path = Path::new(jf.path());
273
274 for imp in extract_imports(&jf.content, path) {
275 if imp.ends_with(".*") {
276 let pkg_prefix = &imp[..imp.len() - 2];
277 for fid in package_to_frags.get(pkg_prefix).unwrap_or(&vec![]) {
278 if fid != &jf.id {
279 add_edge(&mut edges, &jf.id, fid, import_weight, reverse_factor);
280 }
281 }
282 } else {
283 for fid in fqn_to_frags.get(&imp.to_lowercase()).unwrap_or(&vec![]) {
284 if fid != &jf.id {
285 add_edge(&mut edges, &jf.id, fid, import_weight, reverse_factor);
286 }
287 }
288 if let Some(last) = imp.split('.').next_back() {
289 for fid in class_to_frags.get(&last.to_lowercase()).unwrap_or(&vec![]) {
290 if fid != &jf.id {
291 add_edge(&mut edges, &jf.id, fid, import_weight, reverse_factor);
292 }
293 }
294 }
295 }
296 }
297
298 for inh_ref in extract_inheritance(&jf.content, path) {
299 for fid in class_to_frags
300 .get(&inh_ref.to_lowercase())
301 .unwrap_or(&vec![])
302 {
303 if fid != &jf.id {
304 add_edge(&mut edges, &jf.id, fid, inheritance_weight, reverse_factor);
305 }
306 }
307 }
308
309 for type_ref in extract_type_refs(&jf.content) {
310 if !JVM_STDLIB_TYPES.contains(type_ref.as_str()) {
311 for fid in class_to_frags
312 .get(&type_ref.to_lowercase())
313 .unwrap_or(&vec![])
314 {
315 if fid != &jf.id {
316 add_edge(&mut edges, &jf.id, fid, type_weight, reverse_factor);
317 }
318 }
319 }
320 }
321
322 for ann_ref in extract_annotations(&jf.content) {
323 for fid in class_to_frags
324 .get(&ann_ref.to_lowercase())
325 .unwrap_or(&vec![])
326 {
327 if fid != &jf.id {
328 add_edge(&mut edges, &jf.id, fid, annotation_weight, reverse_factor);
329 }
330 }
331 }
332
333 if let Some(current_pkg) = extract_package(&jf.content) {
334 for fid in package_to_frags.get(¤t_pkg).unwrap_or(&vec![]) {
335 if fid != &jf.id {
336 add_edge(&mut edges, &jf.id, fid, same_package_weight, reverse_factor);
337 }
338 }
339 }
340 }
341
342 edges
343 }
344
345 fn discover_related_files(
346 &self,
347 changed: &[PathBuf],
348 candidates: &[PathBuf],
349 _repo_root: Option<&Path>,
350 _file_cache: Option<&FxHashMap<PathBuf, String>>,
351 ) -> Vec<PathBuf> {
352 let jvm_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_jvm_file(f)).collect();
353 if jvm_changed.is_empty() {
354 return vec![];
355 }
356
357 let changed_set: FxHashSet<PathBuf> = changed.iter().cloned().collect();
358 let jvm_candidates: Vec<PathBuf> = candidates
359 .iter()
360 .filter(|c| is_jvm_file(c) && !changed_set.contains(*c))
361 .cloned()
362 .collect();
363
364 let mut discovered: FxHashSet<PathBuf> = FxHashSet::default();
365 let mut frontier: Vec<PathBuf> = jvm_changed.iter().map(|f| (*f).clone()).collect();
366
367 for _ in 0..SEMANTIC_DISCOVERY.max_depth {
368 let mut type_refs: FxHashSet<String> = FxHashSet::default();
369 let mut frontier_classes: FxHashSet<String> = FxHashSet::default();
370
371 for f in &frontier {
372 if let Ok(content) = std::fs::read_to_string(f) {
373 type_refs.extend(extract_type_refs(&content));
374 frontier_classes.extend(extract_classes(&content, f));
375 }
376 }
377
378 let mut hop_found: Vec<PathBuf> = Vec::new();
379 for c in &jvm_candidates {
380 if discovered.contains(c) {
381 continue;
382 }
383 if let Ok(content) = std::fs::read_to_string(c) {
384 let cand_classes = extract_classes(&content, c);
385 let cand_type_refs = extract_type_refs(&content);
386
387 if !cand_classes.is_disjoint(&type_refs)
388 || !cand_type_refs.is_disjoint(&frontier_classes)
389 {
390 hop_found.push(c.clone());
391 continue;
392 }
393 let cand_imports = extract_imports(&content, c);
394 for imp in &cand_imports {
395 if let Some(last) = imp.rsplit('.').next() {
396 if frontier_classes.contains(last) {
397 hop_found.push(c.clone());
398 break;
399 }
400 }
401 }
402 }
403 }
404
405 let new_files: Vec<PathBuf> = hop_found
406 .into_iter()
407 .filter(|f| !discovered.contains(f))
408 .collect();
409 if new_files.is_empty() {
410 break;
411 }
412 discovered.extend(new_files.iter().cloned());
413 frontier = new_files;
414 }
415
416 let mut result: Vec<PathBuf> = discovered.into_iter().collect();
417 result.sort();
418 result
419 }
420}