Skip to main content

_diffctx/edges/semantic/
dotnet.rs

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}