Skip to main content

_diffctx/edges/semantic/
jvm.rs

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(&current_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}