Skip to main content

_diffctx/edges/semantic/
swift.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_swift_file(path: &Path) -> bool {
14    base::file_ext(path) == ".swift"
15}
16
17static IMPORT_RE: Lazy<Regex> = Lazy::new(|| {
18    Regex::new(r"(?m)^\s*import\s+(?:class|struct|enum|protocol|func\s+)?(\w+)").unwrap()
19});
20static TYPE_DEF_RE: Lazy<Regex> = Lazy::new(|| {
21    Regex::new(r"(?m)^\s*(?:public|open|internal|private|fileprivate)?\s*(?:final\s+)?(?:class|struct|protocol|enum|actor)\s+([A-Z]\w*)").unwrap()
22});
23static FUNC_DEF_RE: Lazy<Regex> = Lazy::new(|| {
24    Regex::new(r"(?m)^\s*(?:public|open|internal|private|fileprivate|static|class)?\s*func\s+([a-zA-Z_]\w*)").unwrap()
25});
26static CONFORMANCE_RE: Lazy<Regex> =
27    Lazy::new(|| Regex::new(r"(?:class|struct|enum|actor)\s+\w+\s*:\s*([\w\s,]+)").unwrap());
28static EXTENSION_RE: Lazy<Regex> =
29    Lazy::new(|| Regex::new(r"(?m)^\s*extension\s+([A-Z]\w*)").unwrap());
30static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
31
32fn extract_imports(content: &str) -> FxHashSet<String> {
33    IMPORT_RE
34        .captures_iter(content)
35        .map(|c| c[1].to_string())
36        .collect()
37}
38
39fn extract_type_defs(content: &str) -> FxHashSet<String> {
40    let mut defs: FxHashSet<String> = TYPE_DEF_RE
41        .captures_iter(content)
42        .map(|c| c[1].to_string())
43        .collect();
44    defs.extend(FUNC_DEF_RE.captures_iter(content).map(|c| c[1].to_string()));
45    defs
46}
47
48fn extract_conformances(content: &str) -> FxHashSet<String> {
49    let mut refs = FxHashSet::default();
50    for cap in CONFORMANCE_RE.captures_iter(content) {
51        for part in cap[1].split(',') {
52            let name = part.trim();
53            if !name.is_empty() && name.starts_with(|c: char| c.is_uppercase()) {
54                refs.insert(name.to_string());
55            }
56        }
57    }
58    refs
59}
60
61fn extract_extensions(content: &str) -> FxHashSet<String> {
62    EXTENSION_RE
63        .captures_iter(content)
64        .map(|c| c[1].to_string())
65        .collect()
66}
67
68pub struct SwiftEdgeBuilder;
69
70impl EdgeBuilder for SwiftEdgeBuilder {
71    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
72        let frags: Vec<&Fragment> = fragments
73            .iter()
74            .filter(|f| is_swift_file(Path::new(f.path())))
75            .collect();
76        if frags.is_empty() {
77            return FxHashMap::default();
78        }
79
80        let import_w = EDGE_WEIGHTS["swift_import"].forward;
81        let conform_w = EDGE_WEIGHTS["swift_conformance"].forward;
82        let ext_w = EDGE_WEIGHTS["swift_extension"].forward;
83        let type_w = EDGE_WEIGHTS["swift_type"].forward;
84        let reverse_factor = EDGE_WEIGHTS["swift_import"].reverse_factor;
85
86        let idx = base::FragmentIndex::new(fragments, repo_root);
87        let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
88        for f in &frags {
89            for name in extract_type_defs(&f.content) {
90                name_to_defs
91                    .entry(name.to_lowercase())
92                    .or_default()
93                    .push(f.id.clone());
94            }
95        }
96
97        let mut edges: EdgeDict = FxHashMap::default();
98
99        for f in &frags {
100            let self_defs = extract_type_defs(&f.content);
101            for imp in extract_imports(&f.content) {
102                base::link_by_name(&f.id, &imp, &idx, &mut edges, import_w, reverse_factor);
103            }
104            for conf in extract_conformances(&f.content) {
105                if let Some(targets) = name_to_defs.get(&conf.to_lowercase()) {
106                    add_edges_from_ids(&mut edges, &f.id, targets, conform_w, reverse_factor);
107                }
108            }
109            for ext_name in extract_extensions(&f.content) {
110                if let Some(targets) = name_to_defs.get(&ext_name.to_lowercase()) {
111                    add_edges_from_ids(&mut edges, &f.id, targets, ext_w, reverse_factor);
112                }
113            }
114            for cap in TYPE_REF_RE.captures_iter(&f.content) {
115                let name = &cap[1];
116                if self_defs.contains(name) {
117                    continue;
118                }
119                if let Some(targets) = name_to_defs.get(&name.to_lowercase()) {
120                    for t in targets {
121                        if t != &f.id {
122                            add_edge(&mut edges, &f.id, t, type_w, reverse_factor);
123                        }
124                    }
125                }
126            }
127        }
128        edges
129    }
130
131    fn discover_related_files(
132        &self,
133        changed: &[PathBuf],
134        candidates: &[PathBuf],
135        repo_root: Option<&Path>,
136        file_cache: Option<&FxHashMap<PathBuf, String>>,
137    ) -> Vec<PathBuf> {
138        let sw_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_swift_file(f)).collect();
139        if sw_changed.is_empty() {
140            return vec![];
141        }
142        let mut refs = FxHashSet::default();
143        for f in &sw_changed {
144            if let Some(content) = base::read_file_cached(f, file_cache) {
145                refs.extend(extract_imports(&content));
146            }
147        }
148        discover_files_by_refs(&refs, changed, candidates, repo_root)
149    }
150}