_diffctx/edges/semantic/
swift.rs1use 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}