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}