1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::extensions::DOTNET_EXTENSIONS;
8use crate::config::weights::EDGE_WEIGHTS;
9use crate::types::{Fragment, FragmentId};
10
11use super::super::EdgeDict;
12use super::super::base::{
13 self, EdgeBuilder, FragmentIndex, add_edge, discover_files_by_refs, link_by_name,
14};
15
16static EXTENDED_DOTNET_EXTENSIONS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
17 DOTNET_EXTENSIONS
18 .iter()
19 .copied()
20 .chain([".vb", ".csproj", ".fsproj", ".sln"])
21 .collect()
22});
23
24fn is_dotnet_file(path: &Path) -> bool {
25 let ext = base::file_ext(path);
26 EXTENDED_DOTNET_EXTENSIONS.contains(ext.as_str())
27}
28
29fn is_cs_file(path: &Path) -> bool {
30 base::file_ext(path) == ".cs"
31}
32
33fn is_fs_file(path: &Path) -> bool {
34 let ext = base::file_ext(path);
35 ext == ".fs" || ext == ".fsi" || ext == ".fsx"
36}
37
38static CS_USING_RE: Lazy<Regex> =
39 Lazy::new(|| Regex::new(r"(?m)^\s*(?:global\s+)?using\s+(?:static\s+)?([A-Z][\w.]+)").unwrap());
40static FS_OPEN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*open\s+([A-Z][\w.]+)").unwrap());
41static NAMESPACE_RE: Lazy<Regex> =
42 Lazy::new(|| Regex::new(r"(?m)^\s*namespace\s+([A-Z][\w.]+)").unwrap());
43static TYPE_DEF_RE: Lazy<Regex> = Lazy::new(|| {
44 Regex::new(
45 r"(?m)^\s*(?:public|internal|private|protected)?\s*(?:static|abstract|sealed|partial)?\s*(?:class|struct|interface|enum|record)\s+(\w+)",
46 )
47 .unwrap()
48});
49static INHERITANCE_RE: Lazy<Regex> = Lazy::new(|| {
50 Regex::new(r"(?:class|struct|interface|record)\s+\w+\s*(?:<[^>]*>)?\s*:\s*(.+)").unwrap()
51});
52static ATTRIBUTE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[(\w+)(?:\(|])").unwrap());
53static PARTIAL_RE: Lazy<Regex> = Lazy::new(|| {
54 Regex::new(r"(?m)^\s*(?:public|internal|private|protected)?\s*partial\s+(?:class|struct|interface|record)\s+(\w+)").unwrap()
55});
56static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w+)\b").unwrap());
57
58static DOTNET_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
59 [
60 "String",
61 "Int32",
62 "Boolean",
63 "Object",
64 "Void",
65 "Task",
66 "Action",
67 "Func",
68 "List",
69 "Dictionary",
70 "IEnumerable",
71 "IList",
72 "ICollection",
73 "Exception",
74 "Console",
75 "Math",
76 "Convert",
77 "Type",
78 "Attribute",
79 "Nullable",
80 "if",
81 "else",
82 "for",
83 "while",
84 "do",
85 "switch",
86 "case",
87 "break",
88 "continue",
89 "return",
90 "new",
91 "this",
92 "base",
93 "null",
94 "true",
95 "false",
96 "var",
97 "dynamic",
98 "async",
99 "await",
100 "try",
101 "catch",
102 "finally",
103 "throw",
104 "using",
105 "namespace",
106 "class",
107 "struct",
108 "interface",
109 "enum",
110 "record",
111 "delegate",
112 "event",
113 "public",
114 "private",
115 "protected",
116 "internal",
117 "static",
118 "abstract",
119 "sealed",
120 "virtual",
121 "override",
122 "partial",
123 "readonly",
124 "const",
125 "ref",
126 "out",
127 "in",
128 ]
129 .iter()
130 .copied()
131 .collect()
132});
133
134fn extract_usings(content: &str, path: &Path) -> FxHashSet<String> {
135 let mut refs = FxHashSet::default();
136 if is_cs_file(path) {
137 for cap in CS_USING_RE.captures_iter(content) {
138 refs.insert(cap[1].to_string());
139 }
140 }
141 if is_fs_file(path) {
142 for cap in FS_OPEN_RE.captures_iter(content) {
143 refs.insert(cap[1].to_string());
144 }
145 }
146 refs
147}
148
149fn extract_namespaces(content: &str) -> FxHashSet<String> {
150 NAMESPACE_RE
151 .captures_iter(content)
152 .map(|c| c[1].to_string())
153 .collect()
154}
155
156fn extract_defines(content: &str) -> FxHashSet<String> {
157 let mut defs = FxHashSet::default();
158 for cap in TYPE_DEF_RE.captures_iter(content) {
159 defs.insert(cap[1].to_string());
160 }
161 defs
162}
163
164fn extract_partials(content: &str) -> FxHashSet<String> {
165 PARTIAL_RE
166 .captures_iter(content)
167 .map(|c| c[1].to_string())
168 .collect()
169}
170
171fn extract_base_types(content: &str) -> FxHashSet<String> {
172 let mut bases = FxHashSet::default();
173 for cap in INHERITANCE_RE.captures_iter(content) {
174 for part in cap[1].split(',') {
175 let trimmed = part.trim().split('<').next().unwrap_or("").trim();
176 if !trimmed.is_empty() && trimmed.chars().next().map_or(false, |c| c.is_uppercase()) {
177 bases.insert(trimmed.to_string());
178 }
179 }
180 }
181 bases
182}
183
184fn extract_attributes(content: &str) -> FxHashSet<String> {
185 ATTRIBUTE_RE
186 .captures_iter(content)
187 .map(|c| c[1].to_string())
188 .filter(|n| !DOTNET_KEYWORDS.contains(n.as_str()))
189 .collect()
190}
191
192fn extract_type_refs(content: &str) -> FxHashSet<String> {
193 TYPE_REF_RE
194 .captures_iter(content)
195 .map(|c| c[1].to_string())
196 .filter(|n| !DOTNET_KEYWORDS.contains(n.as_str()))
197 .collect()
198}
199
200pub struct DotNetEdgeBuilder;
201
202impl EdgeBuilder for DotNetEdgeBuilder {
203 fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
204 let dn_frags: Vec<&Fragment> = fragments
205 .iter()
206 .filter(|f| is_dotnet_file(Path::new(f.path())))
207 .collect();
208 if dn_frags.is_empty() {
209 return FxHashMap::default();
210 }
211
212 let using_weight = EDGE_WEIGHTS["dotnet_using"].forward;
213 let inheritance_weight = EDGE_WEIGHTS["dotnet_inheritance"].forward;
214 let type_weight = EDGE_WEIGHTS["dotnet_type"].forward;
215 let same_ns_weight = EDGE_WEIGHTS["dotnet_same_namespace"].forward;
216 let attribute_weight = EDGE_WEIGHTS["dotnet_attribute"].forward;
217 let partial_weight = EDGE_WEIGHTS["dotnet_partial"].forward;
218 let reverse_factor = EDGE_WEIGHTS["dotnet_using"].reverse_factor;
219
220 let idx = FragmentIndex::new(fragments, repo_root);
221
222 let mut name_to_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
223 let mut frag_defines: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
224 let mut ns_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
225 let mut frag_namespaces: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
226 let mut partial_to_frags: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
227
228 for f in &dn_frags {
229 let defs = extract_defines(&f.content);
230 for name in &defs {
231 name_to_defs
232 .entry(name.clone())
233 .or_default()
234 .push(f.id.clone());
235 }
236 frag_defines.insert(f.id.clone(), defs);
237
238 let namespaces = extract_namespaces(&f.content);
239 for ns in &namespaces {
240 ns_to_frags
241 .entry(ns.clone())
242 .or_default()
243 .push(f.id.clone());
244 }
245 frag_namespaces.insert(f.id.clone(), namespaces);
246
247 let partials = extract_partials(&f.content);
248 for p in &partials {
249 partial_to_frags
250 .entry(p.clone())
251 .or_default()
252 .push(f.id.clone());
253 }
254 }
255
256 let mut edges: EdgeDict = FxHashMap::default();
257
258 for f in &dn_frags {
259 let self_defs = frag_defines.get(&f.id).cloned().unwrap_or_default();
260 let self_ns = frag_namespaces.get(&f.id).cloned().unwrap_or_default();
261
262 let usings = extract_usings(&f.content, Path::new(f.path()));
263 for u in &usings {
264 if let Some(targets) = ns_to_frags.get(u) {
265 for tgt in targets {
266 if tgt != &f.id {
267 add_edge(&mut edges, &f.id, tgt, using_weight, reverse_factor);
268 }
269 }
270 }
271 link_by_name(&f.id, u, &idx, &mut edges, using_weight, reverse_factor);
272 }
273
274 let base_types = extract_base_types(&f.content);
275 for bt in &base_types {
276 if let Some(dst_ids) = name_to_defs.get(bt) {
277 for dst_id in dst_ids {
278 if dst_id != &f.id {
279 add_edge(
280 &mut edges,
281 &f.id,
282 dst_id,
283 inheritance_weight,
284 reverse_factor,
285 );
286 }
287 }
288 }
289 }
290
291 let type_refs = extract_type_refs(&f.content);
292 for name in &type_refs {
293 if self_defs.contains(name) {
294 continue;
295 }
296 if let Some(dst_ids) = name_to_defs.get(name) {
297 for dst_id in dst_ids {
298 if dst_id != &f.id {
299 add_edge(&mut edges, &f.id, dst_id, type_weight, reverse_factor);
300 }
301 }
302 }
303 }
304
305 let attrs = extract_attributes(&f.content);
306 for attr in &attrs {
307 if let Some(dst_ids) = name_to_defs.get(attr) {
308 for dst_id in dst_ids {
309 if dst_id != &f.id {
310 add_edge(&mut edges, &f.id, dst_id, attribute_weight, reverse_factor);
311 }
312 }
313 }
314 }
315
316 for ns in &self_ns {
317 if let Some(targets) = ns_to_frags.get(ns) {
318 for tgt in targets {
319 if tgt != &f.id {
320 add_edge(&mut edges, &f.id, tgt, same_ns_weight, reverse_factor);
321 }
322 }
323 }
324 }
325 }
326
327 for (_, frag_ids) in &partial_to_frags {
328 if frag_ids.len() < 2 {
329 continue;
330 }
331 for i in 0..frag_ids.len() {
332 for j in (i + 1)..frag_ids.len() {
333 add_edge(
334 &mut edges,
335 &frag_ids[i],
336 &frag_ids[j],
337 partial_weight,
338 reverse_factor,
339 );
340 add_edge(
341 &mut edges,
342 &frag_ids[j],
343 &frag_ids[i],
344 partial_weight,
345 reverse_factor,
346 );
347 }
348 }
349 }
350
351 edges
352 }
353
354 fn discover_related_files(
355 &self,
356 changed: &[PathBuf],
357 candidates: &[PathBuf],
358 repo_root: Option<&Path>,
359 file_cache: Option<&FxHashMap<PathBuf, String>>,
360 ) -> Vec<PathBuf> {
361 let dn_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_dotnet_file(f)).collect();
362 if dn_changed.is_empty() {
363 return vec![];
364 }
365
366 let mut all_refs = FxHashSet::default();
367 for f in &dn_changed {
368 let content = base::read_file_cached(f, file_cache);
369 if let Some(c) = content {
370 all_refs.extend(extract_usings(&c, f));
371 all_refs.extend(extract_namespaces(&c));
372 for bt in extract_base_types(&c) {
373 all_refs.insert(bt);
374 }
375 }
376 }
377
378 discover_files_by_refs(&all_refs, changed, candidates, repo_root)
379 }
380}