Skip to main content

_diffctx/edges/semantic/
dart.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, 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 DART_EXTENSIONS: Lazy<FxHashSet<&str>> = Lazy::new(|| [".dart"].iter().copied().collect());
16
17fn is_dart_file(path: &Path) -> bool {
18    let ext = base::file_ext(path);
19    DART_EXTENSIONS.contains(ext.as_str())
20}
21
22static IMPORT_RE: Lazy<Regex> =
23    Lazy::new(|| Regex::new(r#"(?m)^\s*import\s+['"]([^'"]+)['"]"#).unwrap());
24static EXPORT_RE: Lazy<Regex> =
25    Lazy::new(|| Regex::new(r#"(?m)^\s*export\s+['"]([^'"]+)['"]"#).unwrap());
26static PART_RE: Lazy<Regex> =
27    Lazy::new(|| Regex::new(r#"(?m)^\s*part\s+['"]([^'"]+)['"]"#).unwrap());
28static PART_OF_RE: Lazy<Regex> =
29    Lazy::new(|| Regex::new(r#"(?m)^\s*part\s+of\s+['"]([^'"]+)['"]"#).unwrap());
30static CLASS_RE: Lazy<Regex> =
31    Lazy::new(|| Regex::new(r"(?m)^\s*(?:abstract\s+)?class\s+(\w+)").unwrap());
32static MIXIN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*mixin\s+(\w+)").unwrap());
33static EXTENSION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*extension\s+(\w+)").unwrap());
34static FUNC_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*(?:\w+\s+)*(\w+)\s*[<(]").unwrap());
35static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w+)\b").unwrap());
36static CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([a-z_]\w+)\s*\(").unwrap());
37
38static DART_KEYWORDS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
39    [
40        "if",
41        "else",
42        "for",
43        "while",
44        "do",
45        "switch",
46        "case",
47        "break",
48        "continue",
49        "return",
50        "var",
51        "final",
52        "const",
53        "void",
54        "null",
55        "true",
56        "false",
57        "new",
58        "this",
59        "super",
60        "class",
61        "extends",
62        "implements",
63        "with",
64        "abstract",
65        "import",
66        "export",
67        "library",
68        "part",
69        "typedef",
70        "enum",
71        "mixin",
72        "extension",
73        "async",
74        "await",
75        "yield",
76        "try",
77        "catch",
78        "finally",
79        "throw",
80        "rethrow",
81        "assert",
82        "in",
83        "is",
84        "as",
85        "dynamic",
86        "Function",
87        "String",
88        "int",
89        "double",
90        "bool",
91        "List",
92        "Map",
93        "Set",
94        "Future",
95        "Stream",
96        "Iterable",
97        "Object",
98        "Null",
99        "Never",
100        "Type",
101        "print",
102    ]
103    .iter()
104    .copied()
105    .collect()
106});
107
108fn extract_refs(content: &str) -> FxHashSet<String> {
109    let mut refs = FxHashSet::default();
110    for cap in IMPORT_RE.captures_iter(content) {
111        refs.insert(cap[1].to_string());
112    }
113    for cap in EXPORT_RE.captures_iter(content) {
114        refs.insert(cap[1].to_string());
115    }
116    for cap in PART_RE.captures_iter(content) {
117        refs.insert(cap[1].to_string());
118    }
119    for cap in PART_OF_RE.captures_iter(content) {
120        refs.insert(cap[1].to_string());
121    }
122    refs
123}
124
125fn extract_defines(content: &str) -> FxHashSet<String> {
126    let mut defs = FxHashSet::default();
127    for cap in CLASS_RE.captures_iter(content) {
128        defs.insert(cap[1].to_string());
129    }
130    for cap in MIXIN_RE.captures_iter(content) {
131        defs.insert(cap[1].to_string());
132    }
133    for cap in EXTENSION_RE.captures_iter(content) {
134        defs.insert(cap[1].to_string());
135    }
136    for cap in FUNC_RE.captures_iter(content) {
137        let name = &cap[1];
138        if !DART_KEYWORDS.contains(name) {
139            defs.insert(name.to_string());
140        }
141    }
142    defs
143}
144
145fn extract_type_refs(content: &str) -> FxHashSet<String> {
146    TYPE_REF_RE
147        .captures_iter(content)
148        .map(|c| c[1].to_string())
149        .filter(|n| !DART_KEYWORDS.contains(n.as_str()))
150        .collect()
151}
152
153fn extract_calls(content: &str) -> FxHashSet<String> {
154    CALL_RE
155        .captures_iter(content)
156        .map(|c| c[1].to_string())
157        .filter(|n| !DART_KEYWORDS.contains(n.as_str()))
158        .collect()
159}
160
161pub struct DartEdgeBuilder;
162
163impl EdgeBuilder for DartEdgeBuilder {
164    fn build(&self, fragments: &[Fragment], _repo_root: Option<&Path>) -> EdgeDict {
165        let dart_frags: Vec<&Fragment> = fragments
166            .iter()
167            .filter(|f| is_dart_file(Path::new(f.path())))
168            .collect();
169        if dart_frags.is_empty() {
170            return FxHashMap::default();
171        }
172
173        let import_weight = EDGE_WEIGHTS["dart_import"].forward;
174        let type_weight = EDGE_WEIGHTS["dart_type"].forward;
175        let fn_weight = EDGE_WEIGHTS["dart_fn"].forward;
176        let inheritance_weight = EDGE_WEIGHTS["dart_inheritance"].forward;
177        let reverse_factor = EDGE_WEIGHTS["dart_import"].reverse_factor;
178
179        let idx = FragmentIndex::new(fragments, _repo_root);
180
181        let mut name_to_defs: FxHashMap<String, Vec<FragmentId>> = FxHashMap::default();
182        let mut frag_defines: FxHashMap<FragmentId, FxHashSet<String>> = FxHashMap::default();
183
184        for f in &dart_frags {
185            let defs = extract_defines(&f.content);
186            for name in &defs {
187                name_to_defs
188                    .entry(name.clone())
189                    .or_default()
190                    .push(f.id.clone());
191            }
192            frag_defines.insert(f.id.clone(), defs);
193        }
194
195        let mut edges: EdgeDict = FxHashMap::default();
196
197        for f in &dart_frags {
198            let self_defs = frag_defines.get(&f.id).cloned().unwrap_or_default();
199
200            let file_refs = extract_refs(&f.content);
201            for r in &file_refs {
202                link_by_name(&f.id, r, &idx, &mut edges, import_weight, reverse_factor);
203            }
204
205            let type_refs = extract_type_refs(&f.content);
206            for name in &type_refs {
207                if self_defs.contains(name) {
208                    continue;
209                }
210                if let Some(dst_ids) = name_to_defs.get(name) {
211                    for dst_id in dst_ids {
212                        if dst_id != &f.id {
213                            add_edge(&mut edges, &f.id, dst_id, type_weight, reverse_factor);
214                        }
215                    }
216                }
217            }
218
219            let calls = extract_calls(&f.content);
220            for name in &calls {
221                if self_defs.contains(name) {
222                    continue;
223                }
224                if let Some(dst_ids) = name_to_defs.get(name) {
225                    for dst_id in dst_ids {
226                        if dst_id != &f.id {
227                            add_edge(&mut edges, &f.id, dst_id, fn_weight, reverse_factor);
228                        }
229                    }
230                }
231            }
232
233            for ident in &f.identifiers {
234                if self_defs.contains(ident) {
235                    continue;
236                }
237                if let Some(dst_ids) = name_to_defs.get(ident) {
238                    for dst_id in dst_ids {
239                        if dst_id != &f.id {
240                            add_edge(
241                                &mut edges,
242                                &f.id,
243                                dst_id,
244                                inheritance_weight,
245                                reverse_factor,
246                            );
247                        }
248                    }
249                }
250            }
251        }
252
253        edges
254    }
255
256    fn discover_related_files(
257        &self,
258        changed: &[PathBuf],
259        candidates: &[PathBuf],
260        repo_root: Option<&Path>,
261        file_cache: Option<&FxHashMap<PathBuf, String>>,
262    ) -> Vec<PathBuf> {
263        let dart_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_dart_file(f)).collect();
264        if dart_changed.is_empty() {
265            return vec![];
266        }
267
268        let mut all_refs = FxHashSet::default();
269        for f in &dart_changed {
270            let content = base::read_file_cached(f, file_cache);
271            if let Some(c) = content {
272                all_refs.extend(extract_refs(&c));
273            }
274        }
275
276        discover_files_by_refs(&all_refs, changed, candidates, repo_root)
277    }
278}