Skip to main content

_diffctx/edges/semantic/
elixir.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, add_edges_from_ids, discover_files_by_refs};
12
13fn is_elixir_file(path: &Path) -> bool {
14    let ext = base::file_ext(path);
15    ext == ".ex" || ext == ".exs"
16}
17
18static ALIAS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*alias\s+([\w.]+)").unwrap());
19static IMPORT_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*import\s+([\w.]+)").unwrap());
20static USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*use\s+([\w.]+)").unwrap());
21static REQUIRE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*require\s+([\w.]+)").unwrap());
22static DEFMODULE_RE: Lazy<Regex> =
23    Lazy::new(|| Regex::new(r"(?m)^\s*defmodule\s+([\w.]+)").unwrap());
24static DEF_RE: Lazy<Regex> = Lazy::new(|| {
25    Regex::new(r"(?m)^\s*(?:def|defp|defmacro|defmacrop|defguard|defdelegate)\s+([a-z_]\w*)")
26        .unwrap()
27});
28static BEHAVIOUR_RE: Lazy<Regex> =
29    Lazy::new(|| Regex::new(r"(?m)^\s*@behaviour\s+([\w.]+)").unwrap());
30static MODULE_REF_RE: Lazy<Regex> =
31    Lazy::new(|| Regex::new(r"\b([A-Z]\w*(?:\.[A-Z]\w*)*)\b").unwrap());
32
33fn extract_refs(content: &str) -> FxHashSet<String> {
34    let mut refs = FxHashSet::default();
35    for re in [
36        &*ALIAS_RE,
37        &*IMPORT_RE,
38        &*USE_RE,
39        &*REQUIRE_RE,
40        &*BEHAVIOUR_RE,
41    ] {
42        refs.extend(re.captures_iter(content).map(|c| c[1].to_string()));
43    }
44    refs
45}
46
47fn extract_module_defs(content: &str) -> FxHashSet<String> {
48    DEFMODULE_RE
49        .captures_iter(content)
50        .map(|c| c[1].to_string())
51        .collect()
52}
53
54fn extract_func_defs(content: &str) -> FxHashSet<String> {
55    DEF_RE
56        .captures_iter(content)
57        .map(|c| c[1].to_string())
58        .collect()
59}
60
61pub struct ElixirEdgeBuilder;
62
63impl EdgeBuilder for ElixirEdgeBuilder {
64    fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
65        let frags: Vec<&Fragment> = fragments
66            .iter()
67            .filter(|f| is_elixir_file(Path::new(f.path())))
68            .collect();
69        if frags.is_empty() {
70            return FxHashMap::default();
71        }
72
73        let use_w = EDGE_WEIGHTS["elixir_use"].forward;
74        let alias_w = EDGE_WEIGHTS["elixir_alias"].forward;
75        let behaviour_w = EDGE_WEIGHTS["elixir_behaviour"].forward;
76        let fn_w = EDGE_WEIGHTS["elixir_fn"].forward;
77        let reverse_factor = EDGE_WEIGHTS["elixir_use"].reverse_factor;
78
79        let mut module_to_frags: FxHashMap<String, Vec<_>> = FxHashMap::default();
80        let mut fn_to_frags: FxHashMap<String, Vec<_>> = FxHashMap::default();
81        for f in &frags {
82            for m in extract_module_defs(&f.content) {
83                let leaf = m.split('.').last().unwrap_or(&m).to_lowercase();
84                module_to_frags.entry(leaf).or_default().push(f.id.clone());
85                module_to_frags
86                    .entry(m.to_lowercase())
87                    .or_default()
88                    .push(f.id.clone());
89            }
90            for name in extract_func_defs(&f.content) {
91                fn_to_frags
92                    .entry(name.to_lowercase())
93                    .or_default()
94                    .push(f.id.clone());
95            }
96        }
97
98        let mut edges: EdgeDict = FxHashMap::default();
99
100        for f in &frags {
101            let self_fns = extract_func_defs(&f.content);
102            let refs = extract_refs(&f.content);
103            for r in &refs {
104                let leaf = r.split('.').last().unwrap_or(r).to_lowercase();
105                let full = r.to_lowercase();
106                let w = if BEHAVIOUR_RE.is_match(&format!("@behaviour {}", r)) {
107                    behaviour_w
108                } else if USE_RE.is_match(&format!("use {}", r)) {
109                    use_w
110                } else {
111                    alias_w
112                };
113                for key in [&leaf, &full] {
114                    if let Some(targets) = module_to_frags.get(key) {
115                        add_edges_from_ids(&mut edges, &f.id, targets, w, reverse_factor);
116                    }
117                }
118            }
119            for mref in MODULE_REF_RE.captures_iter(&f.content) {
120                let name = &mref[1];
121                let leaf = name.split('.').last().unwrap_or(name).to_lowercase();
122                if let Some(targets) = module_to_frags.get(&leaf) {
123                    for t in targets {
124                        if t != &f.id {
125                            add_edge(&mut edges, &f.id, t, fn_w, reverse_factor);
126                        }
127                    }
128                }
129            }
130            for id in &f.identifiers {
131                if self_fns.contains(id) {
132                    continue;
133                }
134                if let Some(targets) = fn_to_frags.get(&id.to_lowercase()) {
135                    for t in targets {
136                        if t != &f.id {
137                            add_edge(&mut edges, &f.id, t, fn_w, reverse_factor);
138                        }
139                    }
140                }
141            }
142        }
143        edges
144    }
145
146    fn discover_related_files(
147        &self,
148        changed: &[PathBuf],
149        candidates: &[PathBuf],
150        repo_root: Option<&Path>,
151        file_cache: Option<&FxHashMap<PathBuf, String>>,
152    ) -> Vec<PathBuf> {
153        let ex_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_elixir_file(f)).collect();
154        if ex_changed.is_empty() {
155            return vec![];
156        }
157        let mut refs = FxHashSet::default();
158        for f in &ex_changed {
159            if let Some(content) = base::read_file_cached(f, file_cache) {
160                refs.extend(extract_refs(&content));
161            }
162        }
163        discover_files_by_refs(&refs, changed, candidates, repo_root)
164    }
165}