Skip to main content

_diffctx/edges/semantic/
haskell.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, FragmentId};
9
10use super::super::EdgeDict;
11use super::super::base::{
12    self, EdgeBuilder, FragmentIndex, add_edge, discover_files_by_refs, link_by_name,
13};
14
15static HASKELL_EXTENSIONS: Lazy<FxHashSet<&str>> =
16    Lazy::new(|| [".hs", ".lhs"].iter().copied().collect());
17
18fn is_haskell_file(path: &Path) -> bool {
19    let ext = base::file_ext(path);
20    HASKELL_EXTENSIONS.contains(ext.as_str())
21}
22
23static IMPORT_RE: Lazy<Regex> =
24    Lazy::new(|| Regex::new(r"(?m)^\s*import\s+(?:qualified\s+)?([A-Z][\w.]+)").unwrap());
25static MODULE_RE: Lazy<Regex> =
26    Lazy::new(|| Regex::new(r"(?m)^\s*module\s+([A-Z][\w.]+)").unwrap());
27static DATA_RE: Lazy<Regex> =
28    Lazy::new(|| Regex::new(r"(?m)^\s*(?:data|newtype|type)\s+([A-Z]\w+)").unwrap());
29static CLASS_RE: Lazy<Regex> =
30    Lazy::new(|| Regex::new(r"(?m)^\s*class\s+(?:.*?=>\s*)?([A-Z]\w+)").unwrap());
31static INSTANCE_RE: Lazy<Regex> =
32    Lazy::new(|| Regex::new(r"(?m)^\s*instance\s+.*?\b([A-Z]\w+)\s+([A-Z]\w+)").unwrap());
33static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w+)\b").unwrap());
34static FUNC_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^([a-z_]\w*)\s*::").unwrap());
35static CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([a-z_]\w+)\b").unwrap());
36
37static HASKELL_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
38    [
39        "module",
40        "where",
41        "import",
42        "qualified",
43        "as",
44        "hiding",
45        "data",
46        "newtype",
47        "type",
48        "class",
49        "instance",
50        "deriving",
51        "if",
52        "then",
53        "else",
54        "case",
55        "of",
56        "let",
57        "in",
58        "do",
59        "return",
60        "where",
61        "forall",
62        "foreign",
63        "default",
64        "infixl",
65        "infixr",
66        "infix",
67        "otherwise",
68        "undefined",
69        "error",
70        "show",
71        "read",
72        "map",
73        "filter",
74        "foldl",
75        "foldr",
76        "head",
77        "tail",
78        "null",
79        "length",
80        "print",
81        "putStrLn",
82        "getLine",
83        "main",
84        "IO",
85        "Maybe",
86        "Just",
87        "Nothing",
88        "Either",
89        "Left",
90        "Right",
91        "True",
92        "False",
93        "Bool",
94        "Int",
95        "Integer",
96        "Float",
97        "Double",
98        "Char",
99        "String",
100    ]
101    .iter()
102    .copied()
103    .collect()
104});
105
106fn extract_imports(content: &str) -> FxHashSet<String> {
107    IMPORT_RE
108        .captures_iter(content)
109        .map(|c| c[1].to_string())
110        .collect()
111}
112
113fn extract_modules(content: &str) -> FxHashSet<String> {
114    MODULE_RE
115        .captures_iter(content)
116        .map(|c| c[1].to_string())
117        .collect()
118}
119
120fn extract_defines(content: &str) -> FxHashSet<String> {
121    let mut defs = FxHashSet::default();
122    for cap in DATA_RE.captures_iter(content) {
123        defs.insert(cap[1].to_string());
124    }
125    for cap in CLASS_RE.captures_iter(content) {
126        defs.insert(cap[1].to_string());
127    }
128    for cap in FUNC_RE.captures_iter(content) {
129        defs.insert(cap[1].to_string());
130    }
131    defs
132}
133
134fn extract_instance_refs(content: &str) -> Vec<(String, String)> {
135    INSTANCE_RE
136        .captures_iter(content)
137        .map(|c| (c[1].to_string(), c[2].to_string()))
138        .collect()
139}
140
141fn extract_type_refs(content: &str) -> FxHashSet<String> {
142    TYPE_REF_RE
143        .captures_iter(content)
144        .map(|c| c[1].to_string())
145        .filter(|n| !HASKELL_KEYWORDS.contains(n.as_str()))
146        .collect()
147}
148
149fn extract_calls(content: &str) -> FxHashSet<String> {
150    CALL_RE
151        .captures_iter(content)
152        .map(|c| c[1].to_string())
153        .filter(|n| !HASKELL_KEYWORDS.contains(n.as_str()))
154        .collect()
155}
156
157pub struct HaskellEdgeBuilder;
158
159impl EdgeBuilder for HaskellEdgeBuilder {
160    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
161        let hs_frags: Vec<&Fragment> = fragments
162            .iter()
163            .filter(|f| is_haskell_file(Path::new(f.path())))
164            .collect();
165        if hs_frags.is_empty() {
166            return FxHashMap::default();
167        }
168
169        let import_weight = EDGE_WEIGHTS["haskell_import"].forward;
170        let type_weight = EDGE_WEIGHTS["haskell_type"].forward;
171        let fn_weight = EDGE_WEIGHTS["haskell_fn"].forward;
172        let instance_weight = EDGE_WEIGHTS["haskell_instance"].forward;
173        let reverse_factor = EDGE_WEIGHTS["haskell_import"].reverse_factor;
174
175        let idx = FragmentIndex::new(fragments, repo_root);
176
177        let mut name_to_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
178        let mut frag_defines: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
179        let mut module_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
180
181        for f in &hs_frags {
182            let defs = extract_defines(&f.content);
183            for name in &defs {
184                name_to_defs
185                    .entry(name.clone())
186                    .or_default()
187                    .push(f.id.clone());
188            }
189            frag_defines.insert(f.id.clone(), defs);
190
191            let modules = extract_modules(&f.content);
192            for m in &modules {
193                module_to_frags
194                    .entry(m.clone())
195                    .or_default()
196                    .push(f.id.clone());
197            }
198        }
199
200        let mut edges: EdgeDict = FxHashMap::default();
201
202        for f in &hs_frags {
203            let self_defs = frag_defines.get(&f.id).cloned().unwrap_or_default();
204
205            let imports = extract_imports(&f.content);
206            for imp in &imports {
207                if let Some(targets) = module_to_frags.get(imp) {
208                    for tgt in targets {
209                        if tgt != &f.id {
210                            add_edge(&mut edges, &f.id, tgt, import_weight, reverse_factor);
211                        }
212                    }
213                }
214                link_by_name(&f.id, imp, &idx, &mut edges, import_weight, reverse_factor);
215            }
216
217            let instances = extract_instance_refs(&f.content);
218            for (class_name, type_name) in &instances {
219                for name in [class_name, type_name] {
220                    if let Some(dst_ids) = name_to_defs.get(name) {
221                        for dst_id in dst_ids {
222                            if dst_id != &f.id {
223                                add_edge(
224                                    &mut edges,
225                                    &f.id,
226                                    dst_id,
227                                    instance_weight,
228                                    reverse_factor,
229                                );
230                            }
231                        }
232                    }
233                }
234            }
235
236            let type_refs = extract_type_refs(&f.content);
237            for name in &type_refs {
238                if self_defs.contains(name) {
239                    continue;
240                }
241                if let Some(dst_ids) = name_to_defs.get(name) {
242                    for dst_id in dst_ids {
243                        if dst_id != &f.id {
244                            add_edge(&mut edges, &f.id, dst_id, type_weight, reverse_factor);
245                        }
246                    }
247                }
248            }
249
250            let calls = extract_calls(&f.content);
251            for name in &calls {
252                if self_defs.contains(name) {
253                    continue;
254                }
255                if let Some(dst_ids) = name_to_defs.get(name) {
256                    for dst_id in dst_ids {
257                        if dst_id != &f.id {
258                            add_edge(&mut edges, &f.id, dst_id, fn_weight, reverse_factor);
259                        }
260                    }
261                }
262            }
263        }
264
265        edges
266    }
267
268    fn discover_related_files(
269        &self,
270        changed: &[PathBuf],
271        candidates: &[PathBuf],
272        repo_root: Option<&Path>,
273        file_cache: Option<&FxHashMap<PathBuf, String>>,
274    ) -> Vec<PathBuf> {
275        let hs_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_haskell_file(f)).collect();
276        if hs_changed.is_empty() {
277            return vec![];
278        }
279
280        let mut all_refs = FxHashSet::default();
281        for f in &hs_changed {
282            let content = base::read_file_cached(f, file_cache);
283            if let Some(c) = content {
284                all_refs.extend(extract_imports(&c));
285                all_refs.extend(extract_modules(&c));
286            }
287        }
288
289        discover_files_by_refs(&all_refs, changed, candidates, repo_root)
290    }
291}