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}