Skip to main content

_diffctx/edges/semantic/
julia.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::{self, EdgeBuilder, add_edge, discover_files_by_refs};
12
13fn is_julia_file(path: &Path) -> bool {
14    base::file_ext(path) == ".jl"
15}
16
17static USING_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*using\s+([\w.,\s]+)").unwrap());
18static IMPORT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*import\s+([\w.,:\s]+)").unwrap());
19static INCLUDE_RE: Lazy<Regex> =
20    Lazy::new(|| Regex::new(r#"(?m)^\s*include\s*\(\s*['"]([^'"]+)['"]"#).unwrap());
21static STRUCT_RE: Lazy<Regex> =
22    Lazy::new(|| Regex::new(r"(?m)^\s*(?:mutable\s+)?struct\s+(\w+)").unwrap());
23static ABSTRACT_RE: Lazy<Regex> =
24    Lazy::new(|| Regex::new(r"(?m)^\s*abstract\s+type\s+(\w+)").unwrap());
25static FUNC_RE: Lazy<Regex> =
26    Lazy::new(|| Regex::new(r"(?m)^\s*(?:function|macro)\s+(\w+)").unwrap());
27static SHORT_FUNC_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^(\w+)\s*\(.*\)\s*=").unwrap());
28static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w+)\b").unwrap());
29
30fn extract_imports(content: &str) -> FxHashSet<String> {
31    let mut refs = FxHashSet::default();
32    for cap in USING_RE.captures_iter(content) {
33        for part in cap[1].split(',') {
34            let name = part.split(':').next().unwrap_or("").trim();
35            if !name.is_empty() {
36                refs.insert(name.to_string());
37            }
38        }
39    }
40    for cap in IMPORT_RE.captures_iter(content) {
41        for part in cap[1].split(',') {
42            let name = part.split(':').next().unwrap_or("").trim();
43            if !name.is_empty() {
44                refs.insert(name.to_string());
45            }
46        }
47    }
48    refs.extend(INCLUDE_RE.captures_iter(content).map(|c| c[1].to_string()));
49    refs
50}
51
52fn extract_defs(content: &str) -> FxHashSet<String> {
53    let mut defs: FxHashSet<String> = STRUCT_RE
54        .captures_iter(content)
55        .map(|c| c[1].to_string())
56        .collect();
57    defs.extend(ABSTRACT_RE.captures_iter(content).map(|c| c[1].to_string()));
58    defs.extend(FUNC_RE.captures_iter(content).map(|c| c[1].to_string()));
59    defs.extend(
60        SHORT_FUNC_RE
61            .captures_iter(content)
62            .map(|c| c[1].to_string()),
63    );
64    defs
65}
66
67pub struct JuliaEdgeBuilder;
68
69impl EdgeBuilder for JuliaEdgeBuilder {
70    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
71        let frags: Vec<&Fragment> = fragments
72            .iter()
73            .filter(|f| is_julia_file(Path::new(f.path())))
74            .collect();
75        if frags.is_empty() {
76            return FxHashMap::default();
77        }
78
79        let using_w = EDGE_WEIGHTS["julia_using"].forward;
80        let include_w = EDGE_WEIGHTS["julia_include"].forward;
81        let type_w = EDGE_WEIGHTS["julia_type"].forward;
82        let fn_w = EDGE_WEIGHTS["julia_fn"].forward;
83        let reverse_factor = EDGE_WEIGHTS["julia_using"].reverse_factor;
84
85        let idx = base::FragmentIndex::new(fragments, repo_root);
86        let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
87        for f in &frags {
88            for name in extract_defs(&f.content) {
89                name_to_defs
90                    .entry(name.to_lowercase())
91                    .or_default()
92                    .push(f.id.clone());
93            }
94        }
95
96        let mut edges: EdgeDict = FxHashMap::default();
97
98        for f in &frags {
99            let self_defs = extract_defs(&f.content);
100            for imp in extract_imports(&f.content) {
101                let w = if INCLUDE_RE.is_match(&format!("include(\"{}\")", imp)) {
102                    include_w
103                } else {
104                    using_w
105                };
106                base::link_by_name(&f.id, &imp, &idx, &mut edges, w, reverse_factor);
107            }
108            for cap in TYPE_REF_RE.captures_iter(&f.content) {
109                let name = &cap[1];
110                if self_defs.contains(name) {
111                    continue;
112                }
113                if let Some(targets) = name_to_defs.get(&name.to_lowercase()) {
114                    for t in targets {
115                        if t != &f.id {
116                            add_edge(&mut edges, &f.id, t, type_w, reverse_factor);
117                        }
118                    }
119                }
120            }
121            for id in &f.identifiers {
122                if self_defs.contains(id) {
123                    continue;
124                }
125                if let Some(targets) = name_to_defs.get(&id.to_lowercase()) {
126                    for t in targets {
127                        if t != &f.id {
128                            add_edge(&mut edges, &f.id, t, fn_w, reverse_factor);
129                        }
130                    }
131                }
132            }
133        }
134        edges
135    }
136
137    fn discover_related_files(
138        &self,
139        changed: &[PathBuf],
140        candidates: &[PathBuf],
141        repo_root: Option<&Path>,
142        file_cache: Option<&FxHashMap<PathBuf, String>>,
143    ) -> Vec<PathBuf> {
144        let jl_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_julia_file(f)).collect();
145        if jl_changed.is_empty() {
146            return vec![];
147        }
148        let mut refs = FxHashSet::default();
149        for f in &jl_changed {
150            if let Some(content) = base::read_file_cached(f, file_cache) {
151                refs.extend(extract_imports(&content));
152            }
153        }
154        discover_files_by_refs(&refs, changed, candidates, repo_root)
155    }
156}