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