brokk_bifrost_core/analyzer/
capabilities.rs1use crate::analyzer::code_unit_index::CodeUnitIndex;
2use crate::analyzer::model::{CodeUnit, ImportInfo, ProjectFile};
3use crate::analyzer::pool_memo::PoolSafeMemo;
4use crate::compact_graph::{CompactRows, CompactRowsBuilder};
5use crate::hash::{HashMap, HashSet};
6use std::any::Any;
7use std::collections::{BTreeSet, VecDeque};
8use std::sync::Arc;
9
10use rayon::prelude::*;
11
12pub trait CapabilityProvider: Any {
13 fn as_any(&self) -> &dyn Any;
14}
15
16impl<T: Any> CapabilityProvider for T {
17 fn as_any(&self) -> &dyn Any {
18 self
19 }
20}
21
22pub trait ImportAnalysisProvider: CapabilityProvider + Send + Sync {
23 fn imported_code_units_of(&self, file: &ProjectFile) -> Arc<HashSet<CodeUnit>>;
29 fn referencing_files_of(&self, file: &ProjectFile) -> HashSet<ProjectFile>;
30
31 fn import_infos_for_files(
35 &self,
36 _files: &[ProjectFile],
37 ) -> Option<HashMap<ProjectFile, Vec<ImportInfo>>> {
38 None
39 }
40
41 fn import_info_of(&self, _file: &ProjectFile) -> Vec<ImportInfo> {
42 Vec::new()
43 }
44
45 fn imported_code_units_from_infos(
48 &self,
49 _file: &ProjectFile,
50 _imports: &[ImportInfo],
51 ) -> Option<Arc<HashSet<CodeUnit>>> {
52 None
53 }
54
55 fn imported_files_from_infos(
59 &self,
60 _file: &ProjectFile,
61 _imports: &[ImportInfo],
62 ) -> Option<HashSet<ProjectFile>> {
63 None
64 }
65
66 fn relevant_imports_for(&self, _code_unit: &CodeUnit) -> HashSet<String> {
67 HashSet::default()
68 }
69
70 fn could_import_file(
76 &self,
77 _source_file: &ProjectFile,
78 _imports: &[ImportInfo],
79 _target: &ProjectFile,
80 ) -> bool {
81 false
82 }
83
84 fn import_reachability(
97 &self,
98 source_file: &ProjectFile,
99 imports: &[ImportInfo],
100 target: &ProjectFile,
101 ) -> ImportReachability {
102 if self.could_import_file(source_file, imports, target) {
103 ImportReachability::Reaches
104 } else {
105 ImportReachability::Unknown
106 }
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ImportReachability {
126 Reaches,
128 DoesNotReach,
130 Unknown,
133}
134
135pub fn resolve_imported_files_from_infos(
140 provider: &dyn ImportAnalysisProvider,
141 file: &ProjectFile,
142 imports: &[ImportInfo],
143) -> HashSet<ProjectFile> {
144 provider
145 .imported_files_from_infos(file, imports)
146 .unwrap_or_else(|| {
147 provider
148 .imported_code_units_from_infos(file, imports)
149 .unwrap_or_else(|| provider.imported_code_units_of(file))
150 .iter()
151 .map(|unit| unit.source().clone())
152 .collect()
153 })
154}
155
156pub fn build_reverse_import_index<F>(
157 files: &[ProjectFile],
158 resolve_imported: F,
159 parallel: bool,
160) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
161where
162 F: Fn(&ProjectFile) -> Arc<HashSet<CodeUnit>> + Sync,
163{
164 build_reverse_file_index(
165 files,
166 |file| {
167 resolve_imported(file)
168 .iter()
169 .map(|code_unit| code_unit.source().clone())
170 .collect::<Vec<_>>()
171 },
172 parallel,
173 )
174}
175
176pub type ReverseFileIndex = HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>;
177
178pub fn memoized_reverse_import_index<F, Files>(
179 memo: &PoolSafeMemo<ReverseFileIndex>,
180 files: Files,
181 resolve_imported: F,
182) -> Arc<ReverseFileIndex>
183where
184 F: Fn(&ProjectFile) -> Arc<HashSet<CodeUnit>> + Sync + Copy,
185 Files: Fn() -> Vec<ProjectFile> + Copy,
186{
187 memoized_reverse_file_index(memo, files, |file| {
188 resolve_imported(file)
189 .iter()
190 .map(|code_unit| code_unit.source().clone())
191 .collect::<Vec<_>>()
192 })
193}
194
195pub fn memoized_reverse_file_index<F, I, Files>(
196 memo: &PoolSafeMemo<ReverseFileIndex>,
197 files: Files,
198 resolve_targets: F,
199) -> Arc<ReverseFileIndex>
200where
201 F: Fn(&ProjectFile) -> I + Sync + Copy,
202 I: IntoIterator<Item = ProjectFile>,
203 Files: Fn() -> Vec<ProjectFile> + Copy,
204{
205 memo.get_or_build(
206 || {
207 let files = files();
208 build_reverse_file_index(&files, resolve_targets, true)
209 },
210 || {
211 let files = files();
212 build_reverse_file_index(&files, resolve_targets, false)
213 },
214 )
215}
216
217pub fn build_reverse_file_index<F, I>(
218 files: &[ProjectFile],
219 resolve_targets: F,
220 parallel: bool,
221) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
222where
223 F: Fn(&ProjectFile) -> I + Sync,
224 I: IntoIterator<Item = ProjectFile>,
225{
226 let collect_edges = |file: &ProjectFile| {
227 let source = file.clone();
228 resolve_targets(file)
229 .into_iter()
230 .filter_map(move |target| (target != source).then(|| (target, source.clone())))
231 .collect::<Vec<_>>()
232 };
233 let edges: Vec<_> = if parallel {
234 files.par_iter().flat_map(collect_edges).collect()
235 } else {
236 files.iter().flat_map(collect_edges).collect()
237 };
238
239 let mut reverse: HashMap<ProjectFile, HashSet<ProjectFile>> = HashMap::default();
240 for (target, source) in edges {
241 reverse.entry(target).or_default().insert(source);
242 }
243 reverse
244 .into_iter()
245 .map(|(file, refs)| (file, Arc::new(refs)))
246 .collect()
247}
248
249pub trait TypeAliasProvider: CapabilityProvider + Send + Sync {
253 fn is_type_alias(&self, _code_unit: &CodeUnit) -> bool {
254 false
255 }
256}
257
258pub trait TestDetectionProvider: CapabilityProvider {}
259
260pub trait TypeHierarchyProvider: CapabilityProvider + Send + Sync {
261 fn get_direct_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit>;
262 fn get_direct_descendants(&self, code_unit: &CodeUnit) -> HashSet<CodeUnit>;
263
264 fn supports_type_hierarchy(&self, code_unit: &CodeUnit) -> bool {
265 code_unit.is_class()
266 }
267
268 fn get_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
269 traverse_hierarchy(code_unit, |next| self.get_direct_ancestors(next))
270 }
271
272 fn get_descendants(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
273 traverse_hierarchy(code_unit, |next| {
274 self.get_direct_descendants(next).into_iter().collect()
275 })
276 }
277
278 fn get_polymorphic_matches<T: CodeUnitIndex>(
279 &self,
280 target: &CodeUnit,
281 analyzer: &T,
282 ) -> Vec<CodeUnit>
283 where
284 Self: Sized,
285 {
286 if !target.is_function() {
287 return Vec::new();
288 }
289
290 let Some(parent) = analyzer.parent_of(target) else {
291 return Vec::new();
292 };
293
294 self.get_descendants(&parent)
295 }
296}
297
298pub struct DirectDescendantIndex {
300 nodes: Box<[CodeUnit]>,
301 row_by_ancestor: HashMap<CodeUnit, u32>,
302 descendants: CompactRows<u32>,
303}
304
305impl DirectDescendantIndex {
306 pub fn from_indexed_nodes(
307 nodes: Vec<CodeUnit>,
308 index_by_node: HashMap<CodeUnit, u32>,
309 mut edges: Vec<(u32, u32)>,
310 ) -> Self {
311 assert_eq!(nodes.len(), index_by_node.len());
312 assert!(nodes.iter().enumerate().all(|(index, node)| {
313 index_by_node.get(node).copied()
314 == Some(
315 u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
316 )
317 }));
318 assert!(edges.iter().all(|(ancestor, descendant)| {
319 (*ancestor as usize) < nodes.len() && (*descendant as usize) < nodes.len()
320 }));
321 edges.sort_unstable();
322 edges.dedup();
323
324 let row_count = usize::from(!edges.is_empty())
325 + edges
326 .windows(2)
327 .filter(|pair| pair[0].0 != pair[1].0)
328 .count();
329 let mut row_by_ancestor = HashMap::default();
330 let mut descendants = CompactRowsBuilder::with_capacity(row_count, edges.len());
331 let mut cursor = 0;
332 while cursor < edges.len() {
333 let ancestor = edges[cursor].0;
334 let start = cursor;
335 while cursor < edges.len() && edges[cursor].0 == ancestor {
336 cursor += 1;
337 }
338 let row =
339 u32::try_from(descendants.rows()).expect("hierarchy index rows must fit in a u32");
340 row_by_ancestor.insert(nodes[ancestor as usize].clone(), row);
341 descendants.push_row(
342 edges[start..cursor]
343 .iter()
344 .map(|(_, descendant)| *descendant),
345 );
346 }
347 Self {
348 nodes: nodes.into_boxed_slice(),
349 row_by_ancestor,
350 descendants: descendants.finish(),
351 }
352 }
353
354 pub fn descendants(&self, ancestor: &CodeUnit) -> HashSet<CodeUnit> {
355 let Some(row) = self.row_by_ancestor.get(ancestor).copied() else {
356 return HashSet::default();
357 };
358 self.descendants
359 .row(row as usize)
360 .iter()
361 .map(|descendant| self.nodes[*descendant as usize].clone())
362 .collect()
363 }
364}
365
366pub fn build_direct_descendant_index<A, P>(analyzer: &A, provider: &P) -> DirectDescendantIndex
367where
368 A: CodeUnitIndex,
369 P: TypeHierarchyProvider + ?Sized,
370{
371 build_direct_descendant_index_from_candidates(
372 analyzer
373 .all_declarations()
374 .filter(|candidate| candidate.is_class())
375 .collect(),
376 |candidate| provider.get_direct_ancestors(candidate),
377 )
378}
379
380pub fn build_direct_descendant_index_from_candidates<F>(
381 mut candidates: Vec<CodeUnit>,
382 mut direct_ancestors: F,
383) -> DirectDescendantIndex
384where
385 F: FnMut(&CodeUnit) -> Vec<CodeUnit>,
386{
387 candidates.sort();
388 candidates.dedup();
389 let mut types_by_fq_name: HashMap<String, Vec<CodeUnit>> = HashMap::default();
390 for candidate in &candidates {
391 types_by_fq_name
392 .entry(candidate.fq_name())
393 .or_default()
394 .push(candidate.clone());
395 }
396 let mut nodes = candidates.clone();
397 let mut index_by_node: HashMap<_, _> = nodes
398 .iter()
399 .enumerate()
400 .map(|(index, node)| {
401 (
402 node.clone(),
403 u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
404 )
405 })
406 .collect();
407 let mut edges = Vec::new();
408 for candidate in candidates {
409 let descendant = index_by_node[&candidate];
410 for ancestor in direct_ancestors(&candidate) {
411 let ancestor = types_by_fq_name
412 .get(&ancestor.fq_name())
413 .and_then(|same_name| {
414 let mut same_source = same_name
415 .iter()
416 .filter(|unit| unit.source() == candidate.source());
417 let exact = same_source.next()?;
418 same_source.next().is_none().then(|| exact.clone())
419 })
420 .unwrap_or(ancestor);
421 let ancestor = *index_by_node.entry(ancestor.clone()).or_insert_with(|| {
422 let index = u32::try_from(nodes.len())
423 .expect("hierarchy index declarations must fit in a u32");
424 nodes.push(ancestor);
425 index
426 });
427 edges.push((ancestor, descendant));
428 }
429 }
430 DirectDescendantIndex::from_indexed_nodes(nodes, index_by_node, edges)
431}
432
433fn traverse_hierarchy<F>(root: &CodeUnit, mut next: F) -> Vec<CodeUnit>
434where
435 F: FnMut(&CodeUnit) -> Vec<CodeUnit>,
436{
437 let direct = next(root);
438 if direct.is_empty() {
439 return Vec::new();
440 }
441
442 let mut seen = BTreeSet::new();
443 let mut result = Vec::new();
444 let mut queue = VecDeque::new();
445
446 for item in direct {
447 if seen.insert(item.fq_name()) {
448 queue.push_back(item.clone());
449 result.push(item);
450 }
451 }
452
453 while let Some(current) = queue.pop_front() {
454 for item in next(¤t) {
455 if seen.insert(item.fq_name()) {
456 queue.push_back(item.clone());
457 result.push(item);
458 }
459 }
460 }
461
462 result
463}