brokk_bifrost_core/analyzer/capabilities.rs
1use crate::analyzer::code_unit_index::CodeUnitIndex;
2use crate::analyzer::model::{CodeUnit, ImportInfo, ProjectFile};
3use crate::analyzer::pool_memo::{KeyedPoolSafeMemo, PoolSafeMemo};
4use crate::analyzer::query_token::QueryToken;
5use crate::cancellation::CancellationToken;
6use crate::compact_graph::{CompactRows, CompactRowsBuilder};
7use crate::hash::{HashMap, HashSet};
8use std::any::Any;
9use std::collections::{BTreeSet, VecDeque};
10use std::sync::Arc;
11
12use rayon::prelude::*;
13
14pub trait CapabilityProvider: Any {
15 fn as_any(&self) -> &dyn Any;
16}
17
18/// File-local facts consumed together while constructing the coarse direct
19/// dependency graph.
20///
21/// `contains_tests` is optional so providers without a bulk test-classification
22/// projection can retain their existing per-file fallback. Tree-sitter-backed
23/// providers populate it from the same persisted blob metadata read that
24/// supplies their import facts.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct FileDependencyFacts {
27 pub imports: Vec<ImportInfo>,
28 pub contains_tests: Option<bool>,
29}
30
31/// Structured direct file dependencies contributed outside ordinary imports.
32///
33/// `complete` belongs to the relation set rather than to cancellation. A
34/// provider may know useful edges while lacking enough build-model evidence to
35/// prove that it found every edge; callers must preserve those edges without
36/// publishing the result as a complete cached graph.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct AdditionalFileDependencies {
39 pub dependencies: HashMap<ProjectFile, HashSet<ProjectFile>>,
40 pub complete: bool,
41}
42
43impl AdditionalFileDependencies {
44 pub fn complete(dependencies: HashMap<ProjectFile, HashSet<ProjectFile>>) -> Self {
45 Self {
46 dependencies,
47 complete: true,
48 }
49 }
50
51 pub fn incomplete(dependencies: HashMap<ProjectFile, HashSet<ProjectFile>>) -> Self {
52 Self {
53 dependencies,
54 complete: false,
55 }
56 }
57}
58
59impl<T: Any> CapabilityProvider for T {
60 fn as_any(&self) -> &dyn Any {
61 self
62 }
63}
64
65pub trait ImportAnalysisProvider: CapabilityProvider + Send + Sync {
66 /// Shared, not owned: every memoizing implementation already stores the set
67 /// behind an `Arc` in its per-file cache, and the hottest consumers only
68 /// read it (a membership test in candidate discovery, a projection to
69 /// source files in the reverse-import index). Returning the `Arc` removes a
70 /// whole-set clone per cache hit and per insert.
71 fn imported_code_units_of(&self, file: &ProjectFile) -> Arc<HashSet<CodeUnit>>;
72 fn referencing_files_of(&self, file: &ProjectFile) -> HashSet<ProjectFile>;
73
74 /// Return the union of workspace files that reference any `target`.
75 ///
76 /// A depth-bounded caller already knows both sets and must not be forced to
77 /// initialize a provider's whole-workspace reverse index once per target.
78 /// Providers with a structured batch representation can override this.
79 /// The default preserves the file-at-a-time result for every existing
80 /// provider.
81 fn referencing_files_of_targets(
82 &self,
83 targets: &HashSet<ProjectFile>,
84 cancellation: &CancellationToken,
85 ) -> HashSet<ProjectFile> {
86 let mut referencing = HashSet::default();
87 for target in targets {
88 if cancellation.is_cancelled() {
89 break;
90 }
91 referencing.extend(self.referencing_files_of(target));
92 }
93 referencing
94 }
95
96 /// Return import facts for a group of files without requiring each caller
97 /// to hydrate a complete per-file analyzer state. `None` preserves the
98 /// existing file-at-a-time behavior for providers without a bulk read model.
99 fn import_infos_for_files(
100 &self,
101 _files: &[ProjectFile],
102 ) -> Option<HashMap<ProjectFile, Vec<ImportInfo>>> {
103 None
104 }
105
106 /// Return the file-local facts needed by coarse dependency-graph
107 /// construction in one batch.
108 ///
109 /// The default preserves providers that only expose bulk imports. A
110 /// provider with persisted test classification should override this so a
111 /// reverse walk does not rehydrate one file state per reached graph node.
112 fn file_dependency_facts_for_files(
113 &self,
114 files: &[ProjectFile],
115 ) -> Option<HashMap<ProjectFile, FileDependencyFacts>> {
116 self.import_infos_for_files(files).map(|infos| {
117 infos
118 .into_iter()
119 .map(|(file, imports)| {
120 (
121 file,
122 FileDependencyFacts {
123 imports,
124 contains_tests: None,
125 },
126 )
127 })
128 .collect()
129 })
130 }
131
132 /// Return structured direct file dependencies that are not represented by
133 /// ordinary import facts.
134 ///
135 /// Rust external-module declarations are the motivating case: `mod x;`
136 /// makes the declaring file depend on `x.rs`, but it is neither a `use`
137 /// declaration nor an `extern crate` declaration. Providers should return
138 /// `None` when cancellation prevents a complete answer; callers must not
139 /// cache that prefix as a complete graph.
140 fn additional_direct_file_dependencies(
141 &self,
142 _files: &[ProjectFile],
143 _cancellation: &CancellationToken,
144 ) -> Option<AdditionalFileDependencies> {
145 Some(AdditionalFileDependencies::complete(HashMap::default()))
146 }
147
148 /// The file's import facts, which fall through to the import-tier store
149 /// read when nothing retained answers. Crossing that tier is only cheap
150 /// under an open request scope, so the caller proves one is open by
151 /// passing a [`QueryToken`] (issue #2423).
152 fn import_info_of(&self, _token: QueryToken<'_>, _file: &ProjectFile) -> Vec<ImportInfo> {
153 Vec::new()
154 }
155
156 /// Resolve imported source units from already-loaded import facts. Providers
157 /// that cannot do this cheaply return `None` and use `imported_code_units_of`.
158 fn imported_code_units_from_infos(
159 &self,
160 _file: &ProjectFile,
161 _imports: &[ImportInfo],
162 ) -> Option<Arc<HashSet<CodeUnit>>> {
163 None
164 }
165
166 /// Resolve directly imported project files from already-loaded import facts.
167 /// Providers that do not expose file-level edges return `None` and callers
168 /// can derive a conservative approximation from imported declarations.
169 fn imported_files_from_infos(
170 &self,
171 _file: &ProjectFile,
172 _imports: &[ImportInfo],
173 ) -> Option<HashSet<ProjectFile>> {
174 None
175 }
176
177 fn relevant_imports_for(&self, _code_unit: &CodeUnit) -> HashSet<String> {
178 HashSet::default()
179 }
180
181 /// Whether `source_file` can reference a declaration of `target`. A `true`
182 /// is an answer; a `false` is only an absence of evidence, which is why
183 /// callers still run the expansion backstop after one. Providers that can
184 /// prove the negative override [`Self::import_reachability`] instead and
185 /// derive this from it.
186 fn could_import_file(
187 &self,
188 _source_file: &ProjectFile,
189 _imports: &[ImportInfo],
190 _target: &ProjectFile,
191 ) -> bool {
192 false
193 }
194
195 /// Resolve, in one batch, whatever [`Self::could_import_file`] would look
196 /// up once per candidate.
197 ///
198 /// The shared import-graph candidate walk visits every workspace file, so
199 /// a provider that answers each visit with its own store lookup pays that
200 /// lookup once per import statement in the workspace -- 397k to 662k
201 /// `definition_candidates` round trips inside a single `scan_usages`
202 /// query on a 35k-file Rust workspace (#1748). The walk knows its whole
203 /// candidate set before it inspects any of it, so this hook lets the
204 /// provider enumerate its keys up front and collapse them into batched
205 /// reads.
206 ///
207 /// Doing nothing is always correct: the per-candidate path still answers
208 /// exactly as before, just without the shared warm result. Providers whose
209 /// per-candidate answer is already file-local (Python, JS/TS, Go, C++)
210 /// keep the default.
211 fn prefetch_import_targets(
212 &self,
213 _files: &[ProjectFile],
214 _import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
215 _cancellation: &crate::cancellation::CancellationToken,
216 ) {
217 }
218
219 /// Batch the definition identities that file-dependency resolution will
220 /// consult after import facts have been loaded for a whole workspace.
221 ///
222 /// This is separate from [`Self::prefetch_import_targets`]: exact usage
223 /// candidate discovery and coarse file-edge construction do not
224 /// necessarily resolve the same names. Providers whose file resolver is
225 /// already path-local keep the no-op default.
226 fn prefetch_file_dependency_targets(
227 &self,
228 _files: &[ProjectFile],
229 _import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
230 _cancellation: &crate::cancellation::CancellationToken,
231 ) {
232 }
233
234 /// The three-valued form of [`Self::could_import_file`], which lets a
235 /// provider that has a completeness proof retire the caller's backstop.
236 ///
237 /// The default preserves the historical contract exactly by mapping the
238 /// bool spelling's `true` to [`ImportReachability::Reaches`] and its
239 /// `false` to [`ImportReachability::Unknown`] -- never to `DoesNotReach`,
240 /// because the bool contract never distinguished "no" from "I did not
241 /// find one".
242 ///
243 /// Bridging runs in this direction only. A provider overrides this method
244 /// and defines `could_import_file` over it, so the two spellings cannot
245 /// drift apart.
246 fn import_reachability(
247 &self,
248 source_file: &ProjectFile,
249 imports: &[ImportInfo],
250 target: &ProjectFile,
251 ) -> ImportReachability {
252 if self.could_import_file(source_file, imports, target) {
253 ImportReachability::Reaches
254 } else {
255 ImportReachability::Unknown
256 }
257 }
258}
259
260/// How completely a provider can answer "can `source_file` reference a
261/// declaration of `target`?".
262///
263/// Candidate discovery asks this per (candidate, target) pair and, when it
264/// gets no positive answer, materializes every declaration the candidate
265/// imports as a recall backstop. For a namespace-import language that
266/// expansion is the whole workspace's top-level types per using directive,
267/// which is the shape that burned #1194. The backstop exists only because the
268/// bool contract could not distinguish a proven "no" from an unproven one.
269///
270/// A provider must return `DoesNotReach` only from a proof that covers every
271/// way its language can name a declaration without importing it. Anything less
272/// is `Unknown`: the cost of an unnecessary expansion is time, the cost of a
273/// wrong `DoesNotReach` is a missing usage.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum ImportReachability {
276 /// The candidate can reference the target. Accept it without expanding.
277 Reaches,
278 /// The candidate provably cannot. Reject it without expanding.
279 DoesNotReach,
280 /// Undecided. The caller runs its own backstop, exactly as before this
281 /// verdict existed.
282 Unknown,
283}
284
285/// Resolve direct project-file edges from structured import facts. Prefer a
286/// provider's file-level resolver so imports whose target has no declarations
287/// remain visible; otherwise conservatively project resolved declaration
288/// identities back to their source files.
289pub fn resolve_imported_files_from_infos(
290 provider: &dyn ImportAnalysisProvider,
291 file: &ProjectFile,
292 imports: &[ImportInfo],
293) -> HashSet<ProjectFile> {
294 provider
295 .imported_files_from_infos(file, imports)
296 .unwrap_or_else(|| {
297 provider
298 .imported_code_units_from_infos(file, imports)
299 .unwrap_or_else(|| provider.imported_code_units_of(file))
300 .iter()
301 .map(|unit| unit.source().clone())
302 .collect()
303 })
304}
305
306pub fn build_reverse_import_index<F>(
307 files: &[ProjectFile],
308 resolve_imported: F,
309 parallel: bool,
310) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
311where
312 F: Fn(&ProjectFile) -> Arc<HashSet<CodeUnit>> + Sync,
313{
314 build_reverse_file_index(
315 files,
316 |file| {
317 resolve_imported(file)
318 .iter()
319 .map(|code_unit| code_unit.source().clone())
320 .collect::<Vec<_>>()
321 },
322 parallel,
323 )
324}
325
326pub type ReverseFileIndex = HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>;
327
328pub fn memoized_reverse_import_index<F, Files>(
329 memo: &PoolSafeMemo<ReverseFileIndex>,
330 files: Files,
331 resolve_imported: F,
332) -> Arc<ReverseFileIndex>
333where
334 F: Fn(&ProjectFile) -> Arc<HashSet<CodeUnit>> + Sync + Copy,
335 Files: Fn() -> Vec<ProjectFile> + Copy,
336{
337 memoized_reverse_file_index(memo, files, |file| {
338 resolve_imported(file)
339 .iter()
340 .map(|code_unit| code_unit.source().clone())
341 .collect::<Vec<_>>()
342 })
343}
344
345pub fn memoized_reverse_file_index<F, I, Files>(
346 memo: &PoolSafeMemo<ReverseFileIndex>,
347 files: Files,
348 resolve_targets: F,
349) -> Arc<ReverseFileIndex>
350where
351 F: Fn(&ProjectFile) -> I + Sync + Copy,
352 I: IntoIterator<Item = ProjectFile>,
353 Files: Fn() -> Vec<ProjectFile> + Copy,
354{
355 memo.get_or_build(
356 || {
357 let files = files();
358 build_reverse_file_index(&files, resolve_targets, true)
359 },
360 || {
361 let files = files();
362 build_reverse_file_index(&files, resolve_targets, false)
363 },
364 )
365}
366
367pub fn build_reverse_file_index<F, I>(
368 files: &[ProjectFile],
369 resolve_targets: F,
370 parallel: bool,
371) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
372where
373 F: Fn(&ProjectFile) -> I + Sync,
374 I: IntoIterator<Item = ProjectFile>,
375{
376 let collect_edges = |file: &ProjectFile| {
377 let source = file.clone();
378 resolve_targets(file)
379 .into_iter()
380 .filter_map(move |target| (target != source).then(|| (target, source.clone())))
381 .collect::<Vec<_>>()
382 };
383 let edges: Vec<_> = if parallel {
384 files.par_iter().flat_map(collect_edges).collect()
385 } else {
386 files.iter().flat_map(collect_edges).collect()
387 };
388
389 let mut reverse: HashMap<ProjectFile, HashSet<ProjectFile>> = HashMap::default();
390 for (target, source) in edges {
391 reverse.entry(target).or_default().insert(source);
392 }
393 reverse
394 .into_iter()
395 .map(|(file, refs)| (file, Arc::new(refs)))
396 .collect()
397}
398
399/// `Send + Sync` like its three sibling capabilities: every implementor is an
400/// analyzer, and a parallel whole-workspace scan holds the provider across its
401/// fan-out (`kotlin_graph`'s edge builder does exactly that).
402pub trait TypeAliasProvider: CapabilityProvider + Send + Sync {
403 fn is_type_alias(&self, _code_unit: &CodeUnit) -> bool {
404 false
405 }
406}
407
408pub trait TestDetectionProvider: CapabilityProvider {}
409
410/// Which slice of the workspace a descendant index covers.
411///
412/// This is the memo key, and two values is the whole range: the excluded set is
413/// a pure function of the analyzer and the file, so every request that asks to
414/// leave test files out describes the same index.
415#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
416pub enum DescendantIndexVariant {
417 /// Every class-like declaration the analyzer indexes.
418 WholeWorkspace,
419 /// The declarations whose source file the caller's predicate rejected are
420 /// left out. In practice that is a `scan_usages` request with
421 /// `include_tests: false`, whose test files the answer would discard anyway.
422 ProductionOnly,
423}
424
425/// The request state a whole-workspace descendant-index build needs: the
426/// deadline it polls, and the slice of the workspace it covers.
427///
428/// The two travel together because one loop consumes both -- the per-class loop
429/// in [`build_direct_descendant_index_from_candidates`] and, for C++, the
430/// per-file include-closure walk beneath it -- and because the slice is the
431/// memo key that the deadline's complete-or-nothing rule is applied to.
432///
433/// The exclusion predicate is supplied by the caller rather than named here.
434/// Test classification needs an analyzer (`file_is_test_only` reads a language
435/// module index), which this crate must not know about; the closure crossing is
436/// the same one the Rust usage walks already use for their `keep_going`
437/// predicate. **The predicate must be a pure function of the analyzer and the
438/// file.** That is the condition that makes [`DescendantIndexVariant`] a
439/// complete key: two requests that both exclude test files must describe the
440/// same index, or one would be served the other's.
441#[derive(Clone, Copy)]
442pub struct DescendantIndexScope<'a> {
443 cancellation: &'a CancellationToken,
444 excluded_source: Option<&'a dyn Fn(&ProjectFile) -> bool>,
445}
446
447impl<'a> DescendantIndexScope<'a> {
448 /// Every class in the workspace, stopping when `cancellation` says to.
449 pub fn whole_workspace(cancellation: &'a CancellationToken) -> Self {
450 Self {
451 cancellation,
452 excluded_source: None,
453 }
454 }
455
456 /// Every class whose source file `excluded` rejects is left out of the
457 /// index entirely -- it is never handed to `get_direct_ancestors`, so the
458 /// per-file resolution work behind that call is never charged.
459 pub fn excluding_sources(
460 cancellation: &'a CancellationToken,
461 excluded: &'a dyn Fn(&ProjectFile) -> bool,
462 ) -> Self {
463 Self {
464 cancellation,
465 excluded_source: Some(excluded),
466 }
467 }
468
469 pub fn cancellation(&self) -> &'a CancellationToken {
470 self.cancellation
471 }
472
473 pub fn variant(&self) -> DescendantIndexVariant {
474 match self.excluded_source {
475 Some(_) => DescendantIndexVariant::ProductionOnly,
476 None => DescendantIndexVariant::WholeWorkspace,
477 }
478 }
479
480 /// The poll predicate for a builder that would rather take a plain
481 /// `Fn() -> bool` than a token.
482 pub fn keep_going(&self) -> impl Fn() -> bool + use<'_> {
483 || !self.cancellation.is_cancelled()
484 }
485
486 /// Whether `declaration` belongs in this index.
487 pub fn admits(&self, declaration: &CodeUnit) -> bool {
488 self.excluded_source
489 .is_none_or(|excluded| !excluded(declaration.source()))
490 }
491}
492
493pub trait TypeHierarchyProvider: CapabilityProvider + Send + Sync {
494 fn get_direct_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit>;
495 fn get_direct_descendants(&self, code_unit: &CodeUnit) -> HashSet<CodeUnit>;
496
497 fn supports_type_hierarchy(&self, code_unit: &CodeUnit) -> bool {
498 code_unit.is_class()
499 }
500
501 fn get_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
502 traverse_hierarchy(code_unit, |next| self.get_direct_ancestors(next))
503 }
504
505 fn get_descendants(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
506 traverse_hierarchy(code_unit, |next| {
507 self.get_direct_descendants(next).into_iter().collect()
508 })
509 }
510
511 /// [`Self::get_direct_ancestors`] under a caller's deadline.
512 ///
513 /// `None` means the resolution stopped short; it is never an empty answer,
514 /// which stays the honest way to say "no resolvable base types". The
515 /// default checks the deadline once and delegates, which is right for a
516 /// provider whose ancestor resolution is a lookup. C++ overrides it because
517 /// resolving one class's bases means walking that file's whole transitive
518 /// `#include` closure.
519 fn get_direct_ancestors_within(
520 &self,
521 code_unit: &CodeUnit,
522 scope: &DescendantIndexScope<'_>,
523 ) -> Option<Vec<CodeUnit>> {
524 (!scope.cancellation().is_cancelled()).then(|| self.get_direct_ancestors(code_unit))
525 }
526
527 /// [`Self::get_direct_descendants`] under a caller's deadline and workspace
528 /// slice.
529 ///
530 /// `None` means the answer is not available because the build stopped
531 /// short. It is never a partial answer, and a stopped build publishes
532 /// nothing: a truncated index memoized as complete would be served to every
533 /// later caller as the truth (the rule
534 /// `cancelled_cold_candidate_discovery_does_not_publish_partial_index`
535 /// pins for the Rust walk caches).
536 ///
537 /// The default checks the deadline once and delegates, which is the correct
538 /// behaviour for a provider whose descendant answer is bounded by
539 /// construction. Every provider that inverts the ancestor relation over the
540 /// whole workspace overrides this; a provider that also honours
541 /// [`DescendantIndexScope::admits`] builds one index per
542 /// [`DescendantIndexVariant`]. Ignoring `admits` is sound -- the index is
543 /// then a superset and the caller's own file filter still applies -- so
544 /// only the providers where the prune pays for itself implement it.
545 fn get_direct_descendants_within(
546 &self,
547 code_unit: &CodeUnit,
548 scope: &DescendantIndexScope<'_>,
549 ) -> Option<HashSet<CodeUnit>> {
550 (!scope.cancellation().is_cancelled()).then(|| self.get_direct_descendants(code_unit))
551 }
552
553 /// [`Self::get_descendants`] under a caller's deadline and workspace slice,
554 /// polling once per node of the walk. `None` carries the same meaning as in
555 /// [`Self::get_direct_descendants_within`].
556 fn get_descendants_within(
557 &self,
558 code_unit: &CodeUnit,
559 scope: &DescendantIndexScope<'_>,
560 ) -> Option<Vec<CodeUnit>> {
561 traverse_hierarchy_while(code_unit, scope.cancellation(), |next| {
562 self.get_direct_descendants_within(next, scope)
563 .map(|descendants| descendants.into_iter().collect())
564 })
565 }
566
567 fn get_polymorphic_matches<T: CodeUnitIndex>(
568 &self,
569 target: &CodeUnit,
570 analyzer: &T,
571 ) -> Vec<CodeUnit>
572 where
573 Self: Sized,
574 {
575 if !target.is_function() {
576 return Vec::new();
577 }
578
579 let Some(parent) = analyzer.parent_of(target) else {
580 return Vec::new();
581 };
582
583 self.get_descendants(&parent)
584 }
585}
586
587/// Exact declaration identities plus compact ancestor-to-descendant rows.
588pub struct DirectDescendantIndex {
589 nodes: Box<[CodeUnit]>,
590 row_by_ancestor: HashMap<CodeUnit, u32>,
591 descendants: CompactRows<u32>,
592}
593
594impl DirectDescendantIndex {
595 pub fn from_indexed_nodes(
596 nodes: Vec<CodeUnit>,
597 index_by_node: HashMap<CodeUnit, u32>,
598 mut edges: Vec<(u32, u32)>,
599 ) -> Self {
600 assert_eq!(nodes.len(), index_by_node.len());
601 assert!(nodes.iter().enumerate().all(|(index, node)| {
602 index_by_node.get(node).copied()
603 == Some(
604 u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
605 )
606 }));
607 assert!(edges.iter().all(|(ancestor, descendant)| {
608 (*ancestor as usize) < nodes.len() && (*descendant as usize) < nodes.len()
609 }));
610 edges.sort_unstable();
611 edges.dedup();
612
613 let row_count = usize::from(!edges.is_empty())
614 + edges
615 .windows(2)
616 .filter(|pair| pair[0].0 != pair[1].0)
617 .count();
618 let mut row_by_ancestor = HashMap::default();
619 let mut descendants = CompactRowsBuilder::with_capacity(row_count, edges.len());
620 let mut cursor = 0;
621 while cursor < edges.len() {
622 let ancestor = edges[cursor].0;
623 let start = cursor;
624 while cursor < edges.len() && edges[cursor].0 == ancestor {
625 cursor += 1;
626 }
627 let row =
628 u32::try_from(descendants.rows()).expect("hierarchy index rows must fit in a u32");
629 row_by_ancestor.insert(nodes[ancestor as usize].clone(), row);
630 descendants.push_row(
631 edges[start..cursor]
632 .iter()
633 .map(|(_, descendant)| *descendant),
634 );
635 }
636 Self {
637 nodes: nodes.into_boxed_slice(),
638 row_by_ancestor,
639 descendants: descendants.finish(),
640 }
641 }
642
643 pub fn descendants(&self, ancestor: &CodeUnit) -> HashSet<CodeUnit> {
644 let Some(row) = self.row_by_ancestor.get(ancestor).copied() else {
645 return HashSet::default();
646 };
647 self.descendants
648 .row(row as usize)
649 .iter()
650 .map(|descendant| self.nodes[*descendant as usize].clone())
651 .collect()
652 }
653}
654
655/// Answer a descendant query from a variant-keyed index cell, building the
656/// variant the caller asked for if it is missing.
657///
658/// Every analyzer that memoizes a whole-workspace descendant index repeats this
659/// same dance: pick the cell for the scope's variant, build while the deadline
660/// holds, publish nothing if it does not, project the ancestor's row. It lives
661/// here so the complete-or-nothing rule is stated once instead of copied into
662/// each language module.
663pub fn descendants_from_variant_index(
664 index: &KeyedPoolSafeMemo<DescendantIndexVariant, DirectDescendantIndex>,
665 scope: &DescendantIndexScope<'_>,
666 code_unit: &CodeUnit,
667 build: impl Fn() -> Option<DirectDescendantIndex>,
668) -> Option<HashSet<CodeUnit>> {
669 Some(
670 index
671 .cell(&scope.variant())
672 // The builders are serial, so the same closure serves both memo
673 // arms; the memo's value here is the non-blocking claim protocol.
674 .get_or_build_while(&scope.keep_going(), &build, &build)?
675 .descendants(code_unit),
676 )
677}
678
679/// Invert the ancestor relation over every class the analyzer indexes.
680///
681/// `scope` decides two things. Its predicate drops declarations before they are
682/// ever handed to `get_direct_ancestors`, which is where the per-declaration
683/// resolution cost is charged (for C++ that is a whole transitive `#include`
684/// closure per declaring file). Its token bounds the loop: `None` means the
685/// build stopped short and must not be published.
686pub fn build_direct_descendant_index<A, P>(
687 analyzer: &A,
688 provider: &P,
689 scope: &DescendantIndexScope<'_>,
690) -> Option<DirectDescendantIndex>
691where
692 A: CodeUnitIndex,
693 P: TypeHierarchyProvider + ?Sized,
694{
695 build_direct_descendant_index_from_candidates(
696 analyzer
697 .all_declarations()
698 .filter(|candidate| candidate.is_class() && scope.admits(candidate))
699 .collect(),
700 |candidate| provider.get_direct_ancestors_within(candidate, scope),
701 &scope.keep_going(),
702 )
703}
704
705/// The edge-building half of [`build_direct_descendant_index`], for providers
706/// that assemble their candidate list some other way.
707///
708/// `keep_going` is polled once per candidate. That granularity is the natural
709/// checkpoint: one candidate is one `direct_ancestors` call, which is the unit
710/// of work whose tens of thousands of repetitions made this loop unbounded in
711/// the first place (issue #1748). `direct_ancestors` answers `None` when its
712/// own work stopped short, which stops this loop too -- a candidate whose bases
713/// were half-resolved must not contribute a half-populated edge set.
714pub fn build_direct_descendant_index_from_candidates<F>(
715 mut candidates: Vec<CodeUnit>,
716 mut direct_ancestors: F,
717 keep_going: &dyn Fn() -> bool,
718) -> Option<DirectDescendantIndex>
719where
720 F: FnMut(&CodeUnit) -> Option<Vec<CodeUnit>>,
721{
722 candidates.sort();
723 candidates.dedup();
724 let mut types_by_fq_name: HashMap<String, Vec<CodeUnit>> = HashMap::default();
725 for candidate in &candidates {
726 types_by_fq_name
727 .entry(candidate.fq_name())
728 .or_default()
729 .push(candidate.clone());
730 }
731 let mut nodes = candidates.clone();
732 let mut index_by_node: HashMap<_, _> = nodes
733 .iter()
734 .enumerate()
735 .map(|(index, node)| {
736 (
737 node.clone(),
738 u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
739 )
740 })
741 .collect();
742 let mut edges = Vec::new();
743 for candidate in candidates {
744 if !keep_going() {
745 return None;
746 }
747 let descendant = index_by_node[&candidate];
748 for ancestor in direct_ancestors(&candidate)? {
749 let ancestor = types_by_fq_name
750 .get(&ancestor.fq_name())
751 .and_then(|same_name| {
752 let mut same_source = same_name
753 .iter()
754 .filter(|unit| unit.source() == candidate.source());
755 let exact = same_source.next()?;
756 same_source.next().is_none().then(|| exact.clone())
757 })
758 .unwrap_or(ancestor);
759 let ancestor = *index_by_node.entry(ancestor.clone()).or_insert_with(|| {
760 let index = u32::try_from(nodes.len())
761 .expect("hierarchy index declarations must fit in a u32");
762 nodes.push(ancestor);
763 index
764 });
765 edges.push((ancestor, descendant));
766 }
767 }
768 Some(DirectDescendantIndex::from_indexed_nodes(
769 nodes,
770 index_by_node,
771 edges,
772 ))
773}
774
775/// [`traverse_hierarchy`] under a deadline: polls once per node popped and
776/// propagates a stopped step as `None` rather than returning the nodes it had
777/// already collected. A partial subtype set read as complete would let a caller
778/// conclude that a class has no further subclasses.
779fn traverse_hierarchy_while<F>(
780 root: &CodeUnit,
781 cancellation: &CancellationToken,
782 mut next: F,
783) -> Option<Vec<CodeUnit>>
784where
785 F: FnMut(&CodeUnit) -> Option<Vec<CodeUnit>>,
786{
787 let direct = next(root)?;
788 if direct.is_empty() {
789 return Some(Vec::new());
790 }
791
792 let mut seen = BTreeSet::new();
793 let mut result = Vec::new();
794 let mut queue = VecDeque::new();
795
796 for item in direct {
797 if seen.insert(item.fq_name()) {
798 queue.push_back(item.clone());
799 result.push(item);
800 }
801 }
802
803 while let Some(current) = queue.pop_front() {
804 if cancellation.is_cancelled() {
805 return None;
806 }
807 for item in next(¤t)? {
808 if seen.insert(item.fq_name()) {
809 queue.push_back(item.clone());
810 result.push(item);
811 }
812 }
813 }
814
815 Some(result)
816}
817
818fn traverse_hierarchy<F>(root: &CodeUnit, mut next: F) -> Vec<CodeUnit>
819where
820 F: FnMut(&CodeUnit) -> Vec<CodeUnit>,
821{
822 let direct = next(root);
823 if direct.is_empty() {
824 return Vec::new();
825 }
826
827 let mut seen = BTreeSet::new();
828 let mut result = Vec::new();
829 let mut queue = VecDeque::new();
830
831 for item in direct {
832 if seen.insert(item.fq_name()) {
833 queue.push_back(item.clone());
834 result.push(item);
835 }
836 }
837
838 while let Some(current) = queue.pop_front() {
839 for item in next(¤t) {
840 if seen.insert(item.fq_name()) {
841 queue.push_back(item.clone());
842 result.push(item);
843 }
844 }
845 }
846
847 result
848}