Skip to main content

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(&current)? {
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(&current) {
840            if seen.insert(item.fq_name()) {
841                queue.push_back(item.clone());
842                result.push(item);
843            }
844        }
845    }
846
847    result
848}