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, normalize_indexed_edges};
7use crate::hash::{HashMap, HashSet};
8use std::any::Any;
9use std::collections::{BTreeSet, VecDeque};
10use std::sync::Arc;
11
12use rayon::prelude::*;
13
14use serde_json::Value;
15
16pub trait CapabilityProvider: Any {
17    fn as_any(&self) -> &dyn Any;
18}
19
20/// File-local facts consumed together while constructing the coarse direct
21/// dependency graph.
22///
23/// `contains_tests` is optional so providers without a bulk test-classification
24/// projection can retain their existing per-file fallback. Tree-sitter-backed
25/// providers populate it from the same persisted blob metadata read that
26/// supplies their import facts.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct FileDependencyFacts {
29    pub imports: Vec<ImportInfo>,
30    pub contains_tests: Option<bool>,
31}
32
33/// Structured direct file dependencies contributed outside ordinary imports.
34///
35/// `complete` belongs to the relation set rather than to cancellation. A
36/// provider may know useful edges while lacking enough build-model evidence to
37/// prove that it found every edge; callers must preserve those edges without
38/// publishing the result as a complete cached graph.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct AdditionalFileDependencies {
41    pub dependencies: HashMap<ProjectFile, HashSet<ProjectFile>>,
42    pub complete: bool,
43}
44
45/// One structured name lookup performed while resolving a file's imports.
46/// Candidate invalidation is indexed by the consulted file and looked-up name
47/// so a content edit can invalidate only importers whose resolution consulted
48/// the changed binding. The name `*` is reserved for a glob-surface lookup.
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub struct ImportLookupDependency {
51    pub import_ordinal: u32,
52    pub consulted_file: ProjectFile,
53    pub name: String,
54}
55
56/// Positive and negative structured lookups used to resolve one importer's
57/// file edges. Missing module keys let a newly appearing module invalidate
58/// imports that could not resolve before it existed.
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct ImportResolutionDependencies {
61    pub lookups: Vec<ImportLookupDependency>,
62    pub missing_module_path_keys: Vec<(u32, String)>,
63}
64
65/// One effective Rust name binding in a source file's module surface.
66///
67/// These rows are computed only for changed files and persisted relationally
68/// so the import graph can compare old and new lookup surfaces without
69/// retaining a workspace-sized Rust index in memory.
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct ImportGraphBindingSurface {
72    pub module_path: String,
73    pub name: String,
74    pub namespace: String,
75    pub visibility_kind: String,
76    pub visibility_scope: String,
77    pub origin_file: ProjectFile,
78    pub origin_module_path: String,
79    pub origin_name: String,
80    pub route_files: Vec<ProjectFile>,
81}
82
83/// Fixed-width fingerprints of the structured inputs to one file's effective
84/// import bindings. These are cache identities, not serialized fact bundles.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct ImportGraphInputDigests {
87    pub binding_surface: [u8; 32],
88    pub import_rows: [u8; 32],
89}
90
91/// Target-file edges with structured provenance values for each target.
92pub type ImportedFileEdgeTargets = HashMap<ProjectFile, Vec<Value>>;
93
94/// File targets resolved for one import statement. Providers with complete
95/// candidate dependencies emit these separately so an edit can replace only
96/// the statements whose lookup routes consulted the changed file.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ImportedFileEdgeFacts {
99    pub import_ordinal: u32,
100    pub edges: ImportedFileEdgeTargets,
101}
102
103impl AdditionalFileDependencies {
104    pub fn complete(dependencies: HashMap<ProjectFile, HashSet<ProjectFile>>) -> Self {
105        Self {
106            dependencies,
107            complete: true,
108        }
109    }
110
111    pub fn incomplete(dependencies: HashMap<ProjectFile, HashSet<ProjectFile>>) -> Self {
112        Self {
113            dependencies,
114            complete: false,
115        }
116    }
117}
118
119impl<T: Any> CapabilityProvider for T {
120    fn as_any(&self) -> &dyn Any {
121        self
122    }
123}
124
125/// Wrap a freshly built imported set without charging it.
126pub fn uncharged_imported_code_units(
127    build: impl FnOnce() -> HashSet<CodeUnit>,
128) -> Arc<crate::retention_budget::Accounted<HashSet<CodeUnit>>> {
129    Arc::new(crate::retention_budget::Accounted::uncharged(build()))
130}
131
132pub trait ImportAnalysisProvider: Send + Sync {
133    /// Shared, not owned: every memoizing implementation already stores the set
134    /// behind an `Arc` in its per-file cache, and the hottest consumers only
135    /// read it (a membership test in candidate discovery, a projection to
136    /// source files in the reverse-import index). Returning the `Arc` removes a
137    /// whole-set clone per cache hit and per insert.
138    fn imported_code_units_of(
139        &self,
140        file: &ProjectFile,
141    ) -> Arc<crate::retention_budget::Accounted<HashSet<CodeUnit>>>;
142    fn referencing_files_of(&self, file: &ProjectFile) -> HashSet<ProjectFile>;
143
144    /// Return the union of workspace files that reference any `target`.
145    ///
146    /// A depth-bounded caller already knows both sets and must not be forced to
147    /// initialize a provider's whole-workspace reverse index once per target.
148    /// Providers with a structured batch representation can override this.
149    /// The default preserves the file-at-a-time result for every existing
150    /// provider.
151    fn referencing_files_of_targets(
152        &self,
153        targets: &HashSet<ProjectFile>,
154        cancellation: &CancellationToken,
155    ) -> HashSet<ProjectFile> {
156        let mut referencing = HashSet::default();
157        for target in targets {
158            if cancellation.is_cancelled() {
159                break;
160            }
161            referencing.extend(self.referencing_files_of(target));
162        }
163        referencing
164    }
165
166    /// Return import facts for a group of files without requiring each caller
167    /// to hydrate a complete per-file analyzer state. `None` preserves the
168    /// existing file-at-a-time behavior for providers without a bulk read model.
169    fn import_infos_for_files(
170        &self,
171        _files: &[ProjectFile],
172    ) -> Option<HashMap<ProjectFile, Vec<ImportInfo>>> {
173        None
174    }
175
176    /// Return the file-local facts needed by coarse dependency-graph
177    /// construction in one batch.
178    ///
179    /// The default preserves providers that only expose bulk imports. A
180    /// provider with persisted test classification should override this so a
181    /// reverse walk does not rehydrate one file state per reached graph node.
182    fn file_dependency_facts_for_files(
183        &self,
184        files: &[ProjectFile],
185    ) -> Option<HashMap<ProjectFile, FileDependencyFacts>> {
186        self.import_infos_for_files(files).map(|infos| {
187            infos
188                .into_iter()
189                .map(|(file, imports)| {
190                    (
191                        file,
192                        FileDependencyFacts {
193                            imports,
194                            contains_tests: None,
195                        },
196                    )
197                })
198                .collect()
199        })
200    }
201
202    /// Whether this provider can produce complete structured negative and
203    /// positive import lookup dependencies for every source it analyzes.
204    fn has_complete_import_resolution_dependencies(&self) -> bool {
205        false
206    }
207
208    /// Stable module-path keys backed or declared by this source. They are
209    /// used only to match persisted missing-module lookups after source-set
210    /// changes.
211    fn import_graph_module_path_keys_for_files(
212        &self,
213        _files: &[ProjectFile],
214    ) -> Option<HashMap<ProjectFile, Vec<String>>> {
215        None
216    }
217
218    /// Return the current structured module-level binding surface for the
219    /// selected files. Incremental providers use this to invalidate only the
220    /// names whose bindings changed after a source edit.
221    fn import_graph_binding_surfaces_for_files(
222        &self,
223        _files: &[ProjectFile],
224        _cancellation: Option<&CancellationToken>,
225    ) -> Option<HashMap<ProjectFile, Vec<ImportGraphBindingSurface>>> {
226        None
227    }
228
229    /// Return per-file hashes of the structured declarations/routes and import
230    /// binders that can affect the file's import-graph surface. A provider that
231    /// cannot prove completeness returns None, selecting conservative refresh.
232    fn import_graph_input_digests_for_files(
233        &self,
234        _files: &[ProjectFile],
235        _cancellation: Option<&CancellationToken>,
236    ) -> Option<HashMap<ProjectFile, ImportGraphInputDigests>> {
237        None
238    }
239
240    /// Return complete structured candidate dependencies for each importer.
241    /// Providers without this evidence return None; callers can still build
242    /// correct edges, but must use conservative full invalidation when those
243    /// providers' source contents or source sets change.
244    fn import_resolution_dependencies_for_files(
245        &self,
246        _files: &[ProjectFile],
247        _import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
248        _cancellation: Option<&CancellationToken>,
249    ) -> Option<HashMap<ProjectFile, ImportResolutionDependencies>> {
250        None
251    }
252
253    /// Resolve structured import edges while retaining the source import
254    /// ordinal. `selected_ordinals` lists imports to resolve for files whose
255    /// other import rows will be carried forward; files absent from that map
256    /// are resolved in full. Providers without per-import output return None
257    /// for a partial request, allowing the caller to use its conservative
258    /// source-level path.
259    fn for_each_import_edge_facts_from_infos(
260        &self,
261        files: &[ProjectFile],
262        import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
263        selected_ordinals: &HashMap<ProjectFile, Vec<u32>>,
264        cancellation: Option<&CancellationToken>,
265        visit: &mut dyn FnMut(&ProjectFile, Vec<ImportedFileEdgeFacts>) -> bool,
266    ) -> Option<()> {
267        if !selected_ordinals.is_empty() {
268            return None;
269        }
270        self.for_each_imported_file_edges_from_infos(
271            files,
272            import_infos,
273            cancellation,
274            &mut |file, edges| {
275                visit(
276                    file,
277                    vec![ImportedFileEdgeFacts {
278                        import_ordinal: u32::MAX,
279                        edges,
280                    }],
281                )
282            },
283        )
284    }
285
286    /// Recompute only the candidate dependencies for selected import
287    /// ordinals. The default is conservative: it can only provide all facts
288    /// when the caller did not select a subset.
289    fn import_resolution_dependencies_for_ordinals(
290        &self,
291        files: &[ProjectFile],
292        import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
293        selected_ordinals: &HashMap<ProjectFile, Vec<u32>>,
294        cancellation: Option<&CancellationToken>,
295    ) -> Option<HashMap<ProjectFile, ImportResolutionDependencies>> {
296        if !selected_ordinals.is_empty() {
297            return None;
298        }
299        self.import_resolution_dependencies_for_files(files, import_infos, cancellation)
300    }
301
302    /// Return structured direct file dependencies that are not represented by
303    /// ordinary import facts.
304    ///
305    /// Rust external-module declarations are the motivating case: `mod x;`
306    /// makes the declaring file depend on `x.rs`, but it is neither a `use`
307    /// declaration nor an `extern crate` declaration. Providers should return
308    /// `None` when cancellation prevents a complete answer; callers must not
309    /// cache that prefix as a complete graph.
310    fn additional_direct_file_dependencies(
311        &self,
312        _files: &[ProjectFile],
313        _cancellation: &CancellationToken,
314    ) -> Option<AdditionalFileDependencies> {
315        Some(AdditionalFileDependencies::complete(HashMap::default()))
316    }
317
318    /// The file's import facts, which fall through to the import-tier store
319    /// read when nothing retained answers. Crossing that tier is only cheap
320    /// under an open request scope, so the caller proves one is open by
321    /// passing a [`QueryToken`] (issue #2423).
322    fn import_info_of(&self, _token: QueryToken<'_>, _file: &ProjectFile) -> Vec<ImportInfo> {
323        Vec::new()
324    }
325
326    /// Return the complete, unbounded import set for `file` when the provider
327    /// can prove that its source-owned import publication is available.
328    /// `Some(Vec::new())` is an authoritative import-free file; `None` means
329    /// that the file's import facts are unavailable and must not be treated as
330    /// an empty set.  The legacy [`Self::import_info_of`] method intentionally
331    /// retains its recovery semantics for callers that cannot preserve this
332    /// distinction.
333    fn import_info_of_checked(
334        &self,
335        _token: QueryToken<'_>,
336        _file: &ProjectFile,
337    ) -> Option<Vec<ImportInfo>> {
338        None
339    }
340
341    /// Resolve imported source units from already-loaded import facts. Providers
342    /// that cannot do this cheaply return `None` and use `imported_code_units_of`.
343    fn imported_code_units_from_infos(
344        &self,
345        _file: &ProjectFile,
346        _imports: &[ImportInfo],
347    ) -> Option<Arc<HashSet<CodeUnit>>> {
348        None
349    }
350
351    /// Resolve directly imported project files from already-loaded import facts.
352    /// Providers that do not expose file-level edges return `None` and callers
353    /// can derive a conservative approximation from imported declarations.
354    fn imported_files_from_infos(
355        &self,
356        _file: &ProjectFile,
357        _imports: &[ImportInfo],
358    ) -> Option<HashSet<ProjectFile>> {
359        None
360    }
361
362    /// Provenance for file edges resolved from these structured import facts.
363    /// Providers with richer resolution routes can attach those routes to the
364    /// target file; `None` leaves the caller to use the general structured
365    /// import facts as provenance.
366    fn imported_file_edge_provenance_from_infos(
367        &self,
368        _file: &ProjectFile,
369        _imports: &[ImportInfo],
370    ) -> Option<ImportedFileEdgeTargets> {
371        None
372    }
373
374    /// Resolve each file's imports while streaming results to `visit`, so a
375    /// provider can reuse request-scoped resolver state without retaining a
376    /// workspace-sized result collection. Return `None` only when unsupported;
377    /// in that case `visit` must not have been called. Return `Some(())` after
378    /// handling files until the input ends or `visit` returns `false`.
379    fn for_each_imported_files_from_infos(
380        &self,
381        _files: &[ProjectFile],
382        _import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
383        _cancellation: Option<&CancellationToken>,
384        _visit: &mut dyn FnMut(&ProjectFile, HashSet<ProjectFile>) -> bool,
385    ) -> Option<()> {
386        None
387    }
388
389    /// Stream resolved file edges together with their structured provenance.
390    /// The default preserves providers that only expose targets and records
391    /// the parsed import facts that were supplied for each importer.
392    fn for_each_imported_file_edges_from_infos(
393        &self,
394        files: &[ProjectFile],
395        import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
396        cancellation: Option<&CancellationToken>,
397        visit: &mut dyn FnMut(&ProjectFile, ImportedFileEdgeTargets) -> bool,
398    ) -> Option<()> {
399        let mut visit_targets = |file: &ProjectFile, targets: HashSet<ProjectFile>| {
400            let facts = import_infos.and_then(|infos| infos.get(file)).map_or_else(
401                || serde_json::json!({ "kind": "provider_resolved_import_edge" }),
402                |imports| {
403                    serde_json::json!({
404                        "kind": "structured_import_facts",
405                        "facts": imports
406                    })
407                },
408            );
409            let edges = targets
410                .into_iter()
411                .map(|target| (target, vec![facts.clone()]))
412                .collect();
413            visit(file, edges)
414        };
415        self.for_each_imported_files_from_infos(
416            files,
417            import_infos,
418            cancellation,
419            &mut visit_targets,
420        )
421    }
422
423    fn relevant_imports_for(&self, _code_unit: &CodeUnit) -> HashSet<String> {
424        HashSet::default()
425    }
426
427    /// Whether `source_file` can reference a declaration of `target`. A `true`
428    /// is an answer; a `false` is only an absence of evidence, which is why
429    /// callers still run the expansion backstop after one. Providers that can
430    /// prove the negative override [`Self::import_reachability`] instead and
431    /// derive this from it.
432    fn could_import_file(
433        &self,
434        _source_file: &ProjectFile,
435        _imports: &[ImportInfo],
436        _target: &ProjectFile,
437    ) -> bool {
438        false
439    }
440
441    /// Resolve, in one batch, whatever [`Self::could_import_file`] would look
442    /// up once per candidate.
443    ///
444    /// The shared import-graph candidate walk visits every workspace file, so
445    /// a provider that answers each visit with its own store lookup pays that
446    /// lookup once per import statement in the workspace -- 397k to 662k
447    /// `definition_candidates` round trips inside a single `scan_usages`
448    /// query on a 35k-file Rust workspace (#1748). The walk knows its whole
449    /// candidate set before it inspects any of it, so this hook lets the
450    /// provider enumerate its keys up front and collapse them into batched
451    /// reads.
452    ///
453    /// Doing nothing is always correct: the per-candidate path still answers
454    /// exactly as before, just without the shared warm result. Providers whose
455    /// per-candidate answer is already file-local (Python, JS/TS, Go, C++)
456    /// keep the default.
457    fn prefetch_import_targets(
458        &self,
459        _files: &[ProjectFile],
460        _import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
461        _cancellation: &crate::cancellation::CancellationToken,
462    ) {
463    }
464
465    /// Batch the definition identities that file-dependency resolution will
466    /// consult after import facts have been loaded for a whole workspace.
467    ///
468    /// This is separate from [`Self::prefetch_import_targets`]: exact usage
469    /// candidate discovery and coarse file-edge construction do not
470    /// necessarily resolve the same names. Providers whose file resolver is
471    /// already path-local keep the no-op default.
472    fn prefetch_file_dependency_targets(
473        &self,
474        _files: &[ProjectFile],
475        _import_infos: Option<&HashMap<ProjectFile, Vec<ImportInfo>>>,
476        _cancellation: &crate::cancellation::CancellationToken,
477    ) {
478    }
479
480    /// The three-valued form of [`Self::could_import_file`], which lets a
481    /// provider that has a completeness proof retire the caller's backstop.
482    ///
483    /// The default preserves the historical contract exactly by mapping the
484    /// bool spelling's `true` to [`ImportReachability::Reaches`] and its
485    /// `false` to [`ImportReachability::Unknown`] -- never to `DoesNotReach`,
486    /// because the bool contract never distinguished "no" from "I did not
487    /// find one".
488    ///
489    /// Bridging runs in this direction only. A provider overrides this method
490    /// and defines `could_import_file` over it, so the two spellings cannot
491    /// drift apart.
492    fn import_reachability(
493        &self,
494        source_file: &ProjectFile,
495        imports: &[ImportInfo],
496        target: &ProjectFile,
497    ) -> ImportReachability {
498        if self.could_import_file(source_file, imports, target) {
499            ImportReachability::Reaches
500        } else {
501            ImportReachability::Unknown
502        }
503    }
504}
505
506/// How completely a provider can answer "can `source_file` reference a
507/// declaration of `target`?".
508///
509/// Candidate discovery asks this per (candidate, target) pair and, when it
510/// gets no positive answer, materializes every declaration the candidate
511/// imports as a recall backstop. For a namespace-import language that
512/// expansion is the whole workspace's top-level types per using directive,
513/// which is the shape that burned #1194. The backstop exists only because the
514/// bool contract could not distinguish a proven "no" from an unproven one.
515///
516/// A provider must return `DoesNotReach` only from a proof that covers every
517/// way its language can name a declaration without importing it. Anything less
518/// is `Unknown`: the cost of an unnecessary expansion is time, the cost of a
519/// wrong `DoesNotReach` is a missing usage.
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
521pub enum ImportReachability {
522    /// The candidate can reference the target. Accept it without expanding.
523    Reaches,
524    /// The candidate provably cannot. Reject it without expanding.
525    DoesNotReach,
526    /// Undecided. The caller runs its own backstop, exactly as before this
527    /// verdict existed.
528    Unknown,
529}
530
531/// Resolve direct project-file edges from structured import facts. Prefer a
532/// provider's file-level resolver so imports whose target has no declarations
533/// remain visible; otherwise conservatively project resolved declaration
534/// identities back to their source files.
535pub fn resolve_imported_files_from_infos(
536    provider: &dyn ImportAnalysisProvider,
537    file: &ProjectFile,
538    imports: &[ImportInfo],
539) -> HashSet<ProjectFile> {
540    provider
541        .imported_files_from_infos(file, imports)
542        .unwrap_or_else(|| {
543            if let Some(units) = provider.imported_code_units_from_infos(file, imports) {
544                units.iter().map(|unit| unit.source().clone()).collect()
545            } else {
546                provider
547                    .imported_code_units_of(file)
548                    .iter()
549                    .map(|unit| unit.source().clone())
550                    .collect()
551            }
552        })
553}
554
555pub fn build_reverse_import_index<F>(
556    files: &[ProjectFile],
557    resolve_imported: F,
558    parallel: bool,
559) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
560where
561    F: Fn(&ProjectFile) -> Arc<crate::retention_budget::Accounted<HashSet<CodeUnit>>> + Sync,
562{
563    build_reverse_file_index(
564        files,
565        |file| imported_source_files(&resolve_imported(file)),
566        parallel,
567    )
568}
569
570fn imported_source_files(imported: &HashSet<CodeUnit>) -> Vec<ProjectFile> {
571    imported
572        .iter()
573        .map(|code_unit| code_unit.source().clone())
574        .collect()
575}
576
577pub type ReverseFileIndex = HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>;
578
579pub fn memoized_reverse_import_index<F, Files>(
580    memo: &PoolSafeMemo<ReverseFileIndex>,
581    files: Files,
582    resolve_imported: F,
583) -> Arc<ReverseFileIndex>
584where
585    F: Fn(&ProjectFile) -> Arc<crate::retention_budget::Accounted<HashSet<CodeUnit>>> + Sync + Copy,
586    Files: Fn() -> Vec<ProjectFile> + Copy,
587{
588    memoized_reverse_file_index(memo, files, |file| {
589        imported_source_files(&resolve_imported(file))
590    })
591}
592
593pub fn memoized_reverse_file_index<F, I, Files>(
594    memo: &PoolSafeMemo<ReverseFileIndex>,
595    files: Files,
596    resolve_targets: F,
597) -> Arc<ReverseFileIndex>
598where
599    F: Fn(&ProjectFile) -> I + Sync + Copy,
600    I: IntoIterator<Item = ProjectFile>,
601    Files: Fn() -> Vec<ProjectFile> + Copy,
602{
603    memo.get_or_build(
604        || {
605            let files = files();
606            build_reverse_file_index(&files, resolve_targets, true)
607        },
608        || {
609            let files = files();
610            build_reverse_file_index(&files, resolve_targets, false)
611        },
612    )
613}
614
615pub fn build_reverse_file_index<F, I>(
616    files: &[ProjectFile],
617    resolve_targets: F,
618    parallel: bool,
619) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
620where
621    F: Fn(&ProjectFile) -> I + Sync,
622    I: IntoIterator<Item = ProjectFile>,
623{
624    let context = super::carried_context::CarriedContext::current();
625    let collect_edges = |file: &ProjectFile| {
626        context.run(|| {
627            let source = file.clone();
628            resolve_targets(file)
629                .into_iter()
630                .filter_map(move |target| (target != source).then(|| (target, source.clone())))
631                .collect::<Vec<_>>()
632        })
633    };
634    let edges: Vec<_> = if parallel {
635        files.par_iter().flat_map(collect_edges).collect()
636    } else {
637        files.iter().flat_map(collect_edges).collect()
638    };
639
640    let mut reverse: HashMap<ProjectFile, HashSet<ProjectFile>> = HashMap::default();
641    for (target, source) in edges {
642        reverse.entry(target).or_default().insert(source);
643    }
644    reverse
645        .into_iter()
646        .map(|(file, refs)| (file, Arc::new(refs)))
647        .collect()
648}
649
650/// `Send + Sync` like its three sibling capabilities: every implementor is an
651/// analyzer, and a parallel whole-workspace scan holds the provider across its
652/// fan-out (`kotlin_graph`'s edge builder does exactly that).
653pub trait TypeAliasProvider: CapabilityProvider + Send + Sync {
654    fn is_type_alias(&self, _code_unit: &CodeUnit) -> bool {
655        false
656    }
657}
658
659/// Whether a capability provider classifies `code_unit` as a type alias.
660pub fn provider_declares_type_alias(
661    provider: Option<&dyn TypeAliasProvider>,
662    code_unit: &CodeUnit,
663) -> bool {
664    provider.is_some_and(|provider| provider.is_type_alias(code_unit))
665}
666
667pub trait TestDetectionProvider: CapabilityProvider {}
668
669/// Which slice of the workspace a descendant index covers.
670///
671/// This is the memo key, and two values is the whole range: the excluded set is
672/// a pure function of the analyzer and the file, so every request that asks to
673/// leave test files out describes the same index.
674#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
675pub enum DescendantIndexVariant {
676    /// Every class-like declaration the analyzer indexes.
677    WholeWorkspace,
678    /// The declarations whose source file the caller's predicate rejected are
679    /// left out. In practice that is a `scan_usages` request with
680    /// `include_tests: false`, whose test files the answer would discard anyway.
681    ProductionOnly,
682}
683
684/// The request state a whole-workspace descendant-index build needs: the
685/// deadline it polls, and the slice of the workspace it covers.
686///
687/// The two travel together because one loop consumes both -- the per-class loop
688/// in [`build_direct_descendant_index_from_candidates`] and, for C++, the
689/// per-file include-closure walk beneath it -- and because the slice is the
690/// memo key that the deadline's complete-or-nothing rule is applied to.
691///
692/// The exclusion predicate is supplied by the caller rather than named here.
693/// Test classification needs an analyzer (`file_is_test_only` reads a language
694/// module index), which this crate must not know about; the closure crossing is
695/// the same one the Rust usage walks already use for their `keep_going`
696/// predicate. **The predicate must be a pure function of the analyzer and the
697/// file.** That is the condition that makes [`DescendantIndexVariant`] a
698/// complete key: two requests that both exclude test files must describe the
699/// same index, or one would be served the other's.
700#[derive(Clone, Copy)]
701pub struct DescendantIndexScope<'a> {
702    cancellation: &'a CancellationToken,
703    excluded_source: Option<&'a dyn Fn(&ProjectFile) -> bool>,
704}
705
706impl<'a> DescendantIndexScope<'a> {
707    /// Every class in the workspace, stopping when `cancellation` says to.
708    pub fn whole_workspace(cancellation: &'a CancellationToken) -> Self {
709        Self {
710            cancellation,
711            excluded_source: None,
712        }
713    }
714
715    /// Every class whose source file `excluded` rejects is left out of the
716    /// index entirely -- it is never handed to `get_direct_ancestors`, so the
717    /// per-file resolution work behind that call is never charged.
718    pub fn excluding_sources(
719        cancellation: &'a CancellationToken,
720        excluded: &'a dyn Fn(&ProjectFile) -> bool,
721    ) -> Self {
722        Self {
723            cancellation,
724            excluded_source: Some(excluded),
725        }
726    }
727
728    pub fn cancellation(&self) -> &'a CancellationToken {
729        self.cancellation
730    }
731
732    pub fn variant(&self) -> DescendantIndexVariant {
733        match self.excluded_source {
734            Some(_) => DescendantIndexVariant::ProductionOnly,
735            None => DescendantIndexVariant::WholeWorkspace,
736        }
737    }
738
739    /// The poll predicate for a builder that would rather take a plain
740    /// `Fn() -> bool` than a token.
741    pub fn keep_going(&self) -> impl Fn() -> bool + use<'_> {
742        || !self.cancellation.is_cancelled()
743    }
744
745    /// Whether `declaration` belongs in this index.
746    pub fn admits(&self, declaration: &CodeUnit) -> bool {
747        self.excluded_source
748            .is_none_or(|excluded| !excluded(declaration.source()))
749    }
750}
751
752/// A caller's hierarchy query was stopped. Shared by both traversal directions.
753#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub struct Cancelled;
755
756fn query_within_scope<T>(
757    scope: &DescendantIndexScope<'_>,
758    query: impl FnOnce() -> T,
759) -> Result<T, Cancelled> {
760    if scope.cancellation().is_cancelled() {
761        Err(Cancelled)
762    } else {
763        Ok(query())
764    }
765}
766
767/// One member shape fixed by a platform language specification.
768#[derive(Debug, Clone, Copy)]
769pub struct PlatformInventoryMethod {
770    pub name: &'static str,
771    pub parameters: &'static [&'static str],
772    pub returns: &'static str,
773}
774
775/// One declared parameter type of a nominal-family member, resolved and
776/// erased (JLS 4.6) from the declaration's own type node.
777///
778/// Override matching compares these per position (JLS 8.4.2, 8.4.8.1). A type
779/// variable of the member's *owner* keeps its index, because a subtype that
780/// names the owner with type arguments substitutes them before erasure
781/// (`compareTo(T)` in `Comparable<Box>` is `compareTo(Box)`). Any other type
782/// variable is replaced by the erasure of its leftmost bound.
783#[derive(Debug, Clone, PartialEq, Eq)]
784pub enum MemberFamilyParameterType {
785    /// A primitive type, by keyword.
786    Primitive(String),
787    /// A nominal type whose fully qualified name is known.
788    Nominal(String),
789    /// A nominal type whose identity the analyzer could not establish. It
790    /// carries every fully qualified name the language's name tiers could give
791    /// the spelling at its use site, strongest first, and the type is one of
792    /// them. Two equal lists are the same type, because the tiers pick from a
793    /// list by a rule of its inputs; two disjoint lists are different types.
794    /// An empty list is a type that could not be read at all.
795    Unresolved(Vec<String>),
796    Array(Box<MemberFamilyParameterType>),
797    /// The owner's type parameter at `index`, with the erasure of its bound.
798    OwnerTypeParameter {
799        index: usize,
800        erasure: Box<MemberFamilyParameterType>,
801    },
802}
803
804/// The type arguments a subtype's declaration passes to one direct supertype,
805/// each as a [`MemberFamilyParameterType`] in the subtype's scope.
806#[derive(Debug, Clone, PartialEq, Eq)]
807pub enum MemberFamilySupertypeArguments {
808    /// The supertype is named raw (or is not generic): its members are erased.
809    Raw,
810    Arguments(Vec<MemberFamilyParameterType>),
811    /// The clause naming the supertype could not be read or singled out.
812    Unknown,
813}
814
815/// An ancestor whose member declarations the workspace did not read.
816#[derive(Debug, Clone)]
817pub struct UnreadMemberFamilyAncestor {
818    pub name: String,
819    pub inventory: Option<&'static [PlatformInventoryMethod]>,
820}
821
822#[derive(Debug, Default)]
823pub struct MemberFamilyAncestors {
824    pub resolved: Vec<CodeUnit>,
825    pub ambiguous: Vec<Vec<CodeUnit>>,
826    pub unread: Vec<UnreadMemberFamilyAncestor>,
827}
828
829/// Evidence that a hierarchy query could not close its search boundary.
830#[derive(Debug, Clone)]
831pub enum HierarchyOpenReason {
832    /// Existing hierarchy diagnostics, including `HierarchyTruncated` and
833    /// `AncestorExternalUnindexed`, retain their consumer-side vocabulary.
834    MemberFamily(crate::analyzer::structural::resolution::MemberFamilyReason),
835    /// The descendant index could not acquire the evidence needed to fill.
836    DescendantIndexUnavailable,
837    /// Forward structural-satisfaction coverage (interfaces a type satisfies
838    /// without embedding them) is not closed.
839    ForwardSatisfactionIncomplete,
840    /// Preserve the unread name and any language-specified member inventory.
841    UnreadAncestor(UnreadMemberFamilyAncestor),
842}
843
844impl HierarchyOpenReason {
845    /// Stable reason label with the boundary name retained as data.
846    pub fn description(&self) -> String {
847        match self {
848            Self::UnreadAncestor(ancestor) => format!("{}: {}", self.as_str(), ancestor.name),
849            _ => self.as_str().to_string(),
850        }
851    }
852
853    /// Stable public reason label, shared by hierarchy presentation surfaces.
854    pub fn as_str(&self) -> &'static str {
855        match self {
856            Self::MemberFamily(reason) => reason.label(),
857            Self::DescendantIndexUnavailable => "descendant_index_unavailable",
858            Self::ForwardSatisfactionIncomplete => "forward_satisfaction_incomplete",
859            Self::UnreadAncestor(_) => "unread_ancestor",
860        }
861    }
862}
863
864/// A hierarchy answer owned by one request. Never retain it in a memo or cache.
865///
866/// Only `edges` are definite. Each ambiguous group contains alternatives for
867/// one unresolved edge; none of those alternatives is a definite ancestor or
868/// descendant. Ambiguity and open reasons both prevent an exhaustive answer.
869#[derive(Debug, Default)]
870pub struct HierarchyAnswer {
871    pub edges: Vec<CodeUnit>,
872    pub ambiguous: Vec<Vec<CodeUnit>>,
873    pub open: Vec<HierarchyOpenReason>,
874}
875
876impl HierarchyAnswer {
877    /// Construct a complete answer, preserving the provider's edge order.
878    pub fn complete(edges: Vec<CodeUnit>) -> Self {
879        Self {
880            edges,
881            ambiguous: Vec::new(),
882            open: Vec::new(),
883        }
884    }
885
886    /// Union definite edges in CodeUnit order while retaining all uncertainty.
887    pub fn merge(&mut self, other: Self) {
888        self.edges.extend(other.edges);
889        self.edges.sort();
890        self.edges.dedup();
891        self.ambiguous.extend(other.ambiguous);
892        self.open.extend(other.open);
893    }
894
895    pub fn is_complete(&self) -> bool {
896        self.ambiguous.is_empty() && self.open.is_empty()
897    }
898
899    /// Lossy: drops ambiguous groups and open reasons. Only for callers whose
900    /// language answers are complete by construction.
901    pub fn definite(&self) -> &[CodeUnit] {
902        &self.edges
903    }
904}
905
906/// One caller-owned hierarchy session, borrowed by every step of a walk.
907/// The handle has one borrower and is never shared between Rayon tasks.
908pub trait HierarchyQuery {
909    fn direct_ancestors(&mut self, unit: &CodeUnit) -> HierarchyAnswer;
910    fn direct_descendants(&mut self, unit: &CodeUnit) -> HierarchyAnswer;
911    fn member_family_ancestors(&mut self, unit: &CodeUnit) -> MemberFamilyAncestors;
912
913    fn direct_ancestors_within(
914        &mut self,
915        unit: &CodeUnit,
916        scope: &DescendantIndexScope<'_>,
917    ) -> Result<HierarchyAnswer, Cancelled> {
918        query_within_scope(scope, || self.direct_ancestors(unit))
919    }
920
921    fn direct_descendants_within(
922        &mut self,
923        unit: &CodeUnit,
924        scope: &DescendantIndexScope<'_>,
925    ) -> Result<HierarchyAnswer, Cancelled> {
926        query_within_scope(scope, || self.direct_descendants(unit))
927    }
928}
929
930/// Compatibility adapter for providers whose direct reads need no session.
931/// Each method forwards to the original provider, including scoped overrides.
932pub struct ForwardingHierarchyQuery<'a, P: ?Sized>(pub &'a P);
933
934impl<P: TypeHierarchyProvider + ?Sized> HierarchyQuery for ForwardingHierarchyQuery<'_, P> {
935    fn direct_ancestors(&mut self, unit: &CodeUnit) -> HierarchyAnswer {
936        self.0.get_direct_ancestors(unit)
937    }
938    fn direct_descendants(&mut self, unit: &CodeUnit) -> HierarchyAnswer {
939        self.0.get_direct_descendants(unit)
940    }
941    fn member_family_ancestors(&mut self, unit: &CodeUnit) -> MemberFamilyAncestors {
942        self.0.member_family_ancestors(unit)
943    }
944    fn direct_ancestors_within(
945        &mut self,
946        unit: &CodeUnit,
947        scope: &DescendantIndexScope<'_>,
948    ) -> Result<HierarchyAnswer, Cancelled> {
949        self.0.get_direct_ancestors_within(unit, scope)
950    }
951    fn direct_descendants_within(
952        &mut self,
953        unit: &CodeUnit,
954        scope: &DescendantIndexScope<'_>,
955    ) -> Result<HierarchyAnswer, Cancelled> {
956        self.0.get_direct_descendants_within(unit, scope)
957    }
958}
959
960pub trait TypeHierarchyProvider: Send + Sync {
961    /// Invoke the walk once with a request-local hierarchy handle.
962    fn with_hierarchy_query(&self, run: &mut dyn FnMut(&mut dyn HierarchyQuery)) {
963        run(&mut ForwardingHierarchyQuery(self));
964    }
965
966    /// Root-aware entry lets workspace dispatch exclude unrelated delegates.
967    /// Providers without workspace routing use their ordinary query boundary.
968    fn with_hierarchy_query_for(
969        &self,
970        _root: &CodeUnit,
971        run: &mut dyn FnMut(&mut dyn HierarchyQuery),
972    ) {
973        self.with_hierarchy_query(run);
974    }
975
976    fn get_direct_ancestors(&self, code_unit: &CodeUnit) -> HierarchyAnswer;
977    fn get_direct_descendants(&self, code_unit: &CodeUnit) -> HierarchyAnswer;
978
979    /// The nominal member walk also needs the ancestors not represented by
980    /// workspace declarations. Languages opt in without changing type lookup.
981    fn member_family_ancestors(&self, owner: &CodeUnit) -> MemberFamilyAncestors {
982        let answer = self.get_direct_ancestors(owner);
983        MemberFamilyAncestors {
984            resolved: answer.edges,
985            ambiguous: answer.ambiguous,
986            unread: Vec::new(),
987        }
988    }
989
990    /// Whether an unread ancestor's complete method inventory can declare
991    /// `member`. A method of another name never can. Parameter evidence from
992    /// the language narrows further; without it the side stays open.
993    fn member_family_inventory_matches(
994        &self,
995        member: &CodeUnit,
996        inventory: &[PlatformInventoryMethod],
997    ) -> bool {
998        let methods: Vec<_> = inventory
999            .iter()
1000            .filter(|method| method.name == member.identifier())
1001            .collect();
1002        if methods.is_empty() {
1003            return false;
1004        }
1005        let Some(arity) = self.member_family_parameter_arity(member) else {
1006            return true;
1007        };
1008        let methods: Vec<_> = methods
1009            .into_iter()
1010            .filter(|method| method.parameters.len() == arity)
1011            .collect();
1012        if methods.is_empty() {
1013            return false;
1014        }
1015        if methods.iter().any(|method| method.parameters.is_empty()) {
1016            return true;
1017        }
1018        let Some(parameters) = self.member_family_erased_parameters(member) else {
1019            return true;
1020        };
1021        methods.iter().any(|method| {
1022            parameters
1023                .iter()
1024                .map(String::as_str)
1025                .eq(method.parameters.iter().copied())
1026        })
1027    }
1028
1029    /// The member's declared parameter count, when the language knows it.
1030    fn member_family_parameter_arity(&self, _member: &CodeUnit) -> Option<usize> {
1031        None
1032    }
1033
1034    /// The member's parameter types resolved and erased the way the platform
1035    /// inventory spells them, when the language can resolve them.
1036    fn member_family_erased_parameters(&self, _member: &CodeUnit) -> Option<Vec<String>> {
1037        None
1038    }
1039
1040    /// The member's declared parameter types for override matching, or `None`
1041    /// when the language does not read them structurally. The shared walk then
1042    /// falls back to the recorded parameter spellings, which can confirm an
1043    /// override but never refute one.
1044    fn member_family_parameter_types(
1045        &self,
1046        _member: &CodeUnit,
1047    ) -> Option<Vec<MemberFamilyParameterType>> {
1048        None
1049    }
1050
1051    /// The type arguments `subtype` passes to its direct supertype `supertype`.
1052    fn member_family_supertype_arguments(
1053        &self,
1054        _subtype: &CodeUnit,
1055        _supertype: &CodeUnit,
1056    ) -> MemberFamilySupertypeArguments {
1057        MemberFamilySupertypeArguments::Unknown
1058    }
1059
1060    fn supports_type_hierarchy(&self, code_unit: &CodeUnit) -> bool {
1061        code_unit.is_class()
1062    }
1063
1064    fn get_ancestors(&self, code_unit: &CodeUnit) -> HierarchyAnswer {
1065        let mut result = None;
1066        self.with_hierarchy_query_for(code_unit, &mut |query| {
1067            result = Some(traverse_hierarchy(code_unit, query, |query, next| {
1068                query.direct_ancestors(next)
1069            }));
1070        });
1071        result.expect("hierarchy query callback runs once")
1072    }
1073
1074    fn get_descendants(&self, code_unit: &CodeUnit) -> HierarchyAnswer {
1075        let mut result = None;
1076        self.with_hierarchy_query_for(code_unit, &mut |query| {
1077            result = Some(traverse_hierarchy(code_unit, query, |query, next| {
1078                query.direct_descendants(next)
1079            }));
1080        });
1081        let mut answer = result.expect("hierarchy query callback runs once");
1082        answer.edges.sort();
1083        answer
1084    }
1085
1086    /// [`Self::get_direct_ancestors`] under a caller's deadline.
1087    ///
1088    /// `Err(Cancelled)` means the resolution stopped short; it is never an empty answer,
1089    /// which stays the honest way to say "no resolvable base types". The
1090    /// default checks the deadline once and delegates, which is right for a
1091    /// provider whose ancestor resolution is a lookup. C++ overrides it because
1092    /// resolving one class's bases means walking that file's whole transitive
1093    /// `#include` closure.
1094    fn get_direct_ancestors_within(
1095        &self,
1096        code_unit: &CodeUnit,
1097        scope: &DescendantIndexScope<'_>,
1098    ) -> Result<HierarchyAnswer, Cancelled> {
1099        query_within_scope(scope, || self.get_direct_ancestors(code_unit))
1100    }
1101
1102    /// [`Self::get_direct_descendants`] under a caller's deadline and workspace
1103    /// slice.
1104    ///
1105    /// `Incomplete` means required evidence was unavailable. `Cancelled` means
1106    /// the scope stopped. Neither is a complete empty answer; a failed build publishes
1107    /// nothing: a truncated index memoized as complete would be served to every
1108    /// later caller as the truth (the rule
1109    /// `cancelled_cold_candidate_discovery_does_not_publish_partial_index`
1110    /// pins for the Rust walk caches).
1111    ///
1112    /// The default checks the deadline once and delegates, which is the correct
1113    /// behaviour for a provider whose descendant answer is bounded by
1114    /// construction. Every provider that inverts the ancestor relation over the
1115    /// whole workspace overrides this; a provider that also honours
1116    /// [`DescendantIndexScope::admits`] builds one index per
1117    /// [`DescendantIndexVariant`]. Ignoring `admits` is sound -- the index is
1118    /// then a superset and the caller's own file filter still applies -- so
1119    /// only the providers where the prune pays for itself implement it.
1120    fn get_direct_descendants_within(
1121        &self,
1122        code_unit: &CodeUnit,
1123        scope: &DescendantIndexScope<'_>,
1124    ) -> Result<HierarchyAnswer, Cancelled> {
1125        query_within_scope(scope, || self.get_direct_descendants(code_unit))
1126    }
1127
1128    /// [`Self::get_descendants`] under a caller's deadline and workspace slice,
1129    /// polling once per node of the walk. Failures carry the same meaning as in
1130    /// [`Self::get_direct_descendants_within`].
1131    fn get_descendants_within(
1132        &self,
1133        code_unit: &CodeUnit,
1134        scope: &DescendantIndexScope<'_>,
1135    ) -> Result<HierarchyAnswer, Cancelled> {
1136        let mut result = None;
1137        self.with_hierarchy_query_for(code_unit, &mut |query| {
1138            result = Some(traverse_hierarchy_while(
1139                code_unit,
1140                query,
1141                scope.cancellation(),
1142                |query, next| query.direct_descendants_within(next, scope),
1143            ));
1144        });
1145        let mut answer = result.expect("hierarchy query callback runs once")?;
1146        answer.edges.sort();
1147        Ok(answer)
1148    }
1149
1150    fn get_polymorphic_matches<T: CodeUnitIndex>(
1151        &self,
1152        target: &CodeUnit,
1153        analyzer: &T,
1154    ) -> HierarchyAnswer
1155    where
1156        Self: Sized,
1157    {
1158        if !target.is_function() {
1159            return HierarchyAnswer::complete(Vec::new());
1160        }
1161
1162        let Some(parent) = analyzer.parent_of(target) else {
1163            return HierarchyAnswer::complete(Vec::new());
1164        };
1165
1166        self.get_descendants(&parent)
1167    }
1168}
1169
1170/// Exact declaration identities plus compact ancestor-to-descendant rows.
1171pub struct DirectDescendantIndex {
1172    nodes: Box<[CodeUnit]>,
1173    row_by_ancestor: HashMap<CodeUnit, u32>,
1174    descendants: CompactRows<u32>,
1175}
1176
1177impl DirectDescendantIndex {
1178    pub fn estimated_retained_bytes(&self) -> usize {
1179        let node_bytes = self
1180            .nodes
1181            .iter()
1182            .map(|unit| {
1183                std::mem::size_of::<CodeUnit>()
1184                    + unit.fq_name().len()
1185                    + unit.signature().map_or(0, str::len)
1186                    + unit.source().rel_path().as_os_str().len()
1187                    + unit.source().root().as_os_str().len()
1188            })
1189            .sum::<usize>();
1190        std::mem::size_of::<Self>()
1191            + node_bytes * 2
1192            + self.row_by_ancestor.capacity()
1193                * (std::mem::size_of::<CodeUnit>() + std::mem::size_of::<u32>() + 1)
1194            + self.descendants.estimated_bytes() as usize
1195    }
1196
1197    pub fn from_indexed_nodes(
1198        nodes: Vec<CodeUnit>,
1199        index_by_node: HashMap<CodeUnit, u32>,
1200        mut edges: Vec<(u32, u32)>,
1201    ) -> Self {
1202        normalize_indexed_edges(&nodes, &index_by_node, &mut edges);
1203
1204        let row_count = usize::from(!edges.is_empty())
1205            + edges
1206                .windows(2)
1207                .filter(|pair| pair[0].0 != pair[1].0)
1208                .count();
1209        let mut row_by_ancestor = HashMap::default();
1210        let mut descendants = CompactRowsBuilder::with_capacity(row_count, edges.len());
1211        let mut cursor = 0;
1212        while cursor < edges.len() {
1213            let ancestor = edges[cursor].0;
1214            let start = cursor;
1215            while cursor < edges.len() && edges[cursor].0 == ancestor {
1216                cursor += 1;
1217            }
1218            let row =
1219                u32::try_from(descendants.rows()).expect("hierarchy index rows must fit in a u32");
1220            row_by_ancestor.insert(nodes[ancestor as usize].clone(), row);
1221            descendants.push_row(
1222                edges[start..cursor]
1223                    .iter()
1224                    .map(|(_, descendant)| *descendant),
1225            );
1226        }
1227        Self {
1228            nodes: nodes.into_boxed_slice(),
1229            row_by_ancestor,
1230            descendants: descendants.finish(),
1231        }
1232    }
1233
1234    pub fn descendants(&self, ancestor: &CodeUnit) -> HashSet<CodeUnit> {
1235        let Some(row) = self.row_by_ancestor.get(ancestor).copied() else {
1236            return HashSet::default();
1237        };
1238        self.descendants
1239            .row(row as usize)
1240            .iter()
1241            .map(|descendant| self.nodes[*descendant as usize].clone())
1242            .collect()
1243    }
1244}
1245
1246/// Answer a descendant query from a variant-keyed index cell, building the
1247/// variant the caller asked for if it is missing.
1248///
1249/// Every analyzer that memoizes a whole-workspace descendant index repeats this
1250/// same dance: pick the cell for the scope's variant, build while the deadline
1251/// holds, publish nothing if it does not, project the ancestor's row. It lives
1252/// here so the complete-or-nothing rule is stated once instead of copied into
1253/// each language module.
1254pub fn descendants_from_variant_index(
1255    index: &KeyedPoolSafeMemo<DescendantIndexVariant, DirectDescendantIndex>,
1256    scope: &DescendantIndexScope<'_>,
1257    code_unit: &CodeUnit,
1258    build: impl Fn() -> Option<DirectDescendantIndex>,
1259) -> Option<HashSet<CodeUnit>> {
1260    Some(
1261        index
1262            .cell(&scope.variant())
1263            // The builders are serial, so the same closure serves both memo
1264            // arms; the memo's value here is the non-blocking claim protocol.
1265            .get_or_build_while(&scope.keep_going(), &build, &build)?
1266            .descendants(code_unit),
1267    )
1268}
1269
1270/// Invert the ancestor relation over every class the analyzer indexes.
1271///
1272/// `scope` decides two things. Its predicate drops declarations before they are
1273/// ever handed to `get_direct_ancestors`, which is where the per-declaration
1274/// resolution cost is charged (for C++ that is a whole transitive `#include`
1275/// closure per declaring file). Its token bounds the loop: `None` means the
1276/// build stopped short and must not be published.
1277pub fn build_direct_descendant_index<A, P>(
1278    analyzer: &A,
1279    provider: &P,
1280    scope: &DescendantIndexScope<'_>,
1281) -> Option<DirectDescendantIndex>
1282where
1283    A: CodeUnitIndex,
1284    P: TypeHierarchyProvider + ?Sized,
1285{
1286    let mut result = None;
1287    provider.with_hierarchy_query(&mut |query| {
1288        result = Some(build_direct_descendant_index_in_query(
1289            analyzer, query, scope,
1290        ));
1291    });
1292    result.expect("hierarchy query callback runs once")
1293}
1294
1295/// Invert ancestors inside an already-open query without opening another one.
1296pub fn build_direct_descendant_index_in_query<A: CodeUnitIndex>(
1297    analyzer: &A,
1298    query: &mut dyn HierarchyQuery,
1299    scope: &DescendantIndexScope<'_>,
1300) -> Option<DirectDescendantIndex> {
1301    build_direct_descendant_index_from_candidates(
1302        analyzer
1303            .all_declarations()
1304            .filter(|candidate| candidate.is_class() && scope.admits(candidate))
1305            .collect(),
1306        |candidate| {
1307            let answer = query.direct_ancestors_within(candidate, scope).ok()?;
1308            // Legacy indexes cannot represent uncertainty. Do not publish an
1309            // incomplete inverse relation as a complete cached index.
1310            answer.is_complete().then_some(answer.edges)
1311        },
1312        &scope.keep_going(),
1313    )
1314}
1315
1316/// The edge-building half of [`build_direct_descendant_index`], for providers
1317/// that assemble their candidate list some other way.
1318///
1319/// `keep_going` is polled once per candidate. That granularity is the natural
1320/// checkpoint: one candidate is one `direct_ancestors` call, which is the unit
1321/// of work whose tens of thousands of repetitions made this loop unbounded in
1322/// the first place (issue #1748). `direct_ancestors` answers `None` when its
1323/// own work stopped short, which stops this loop too -- a candidate whose bases
1324/// were half-resolved must not contribute a half-populated edge set.
1325pub fn build_direct_descendant_index_from_candidates<F>(
1326    mut candidates: Vec<CodeUnit>,
1327    mut direct_ancestors: F,
1328    keep_going: &dyn Fn() -> bool,
1329) -> Option<DirectDescendantIndex>
1330where
1331    F: FnMut(&CodeUnit) -> Option<Vec<CodeUnit>>,
1332{
1333    candidates.sort();
1334    candidates.dedup();
1335    let mut types_by_fq_name: HashMap<String, Vec<CodeUnit>> = HashMap::default();
1336    for candidate in &candidates {
1337        types_by_fq_name
1338            .entry(candidate.fq_name())
1339            .or_default()
1340            .push(candidate.clone());
1341    }
1342    let mut nodes = candidates.clone();
1343    let mut index_by_node: HashMap<_, _> = nodes
1344        .iter()
1345        .enumerate()
1346        .map(|(index, node)| {
1347            (
1348                node.clone(),
1349                u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
1350            )
1351        })
1352        .collect();
1353    let mut edges = Vec::new();
1354    for candidate in candidates {
1355        if !keep_going() {
1356            return None;
1357        }
1358        let descendant = index_by_node[&candidate];
1359        for ancestor in direct_ancestors(&candidate)? {
1360            let ancestor = types_by_fq_name
1361                .get(&ancestor.fq_name())
1362                .and_then(|same_name| {
1363                    let mut same_source = same_name
1364                        .iter()
1365                        .filter(|unit| unit.source() == candidate.source());
1366                    let exact = same_source.next()?;
1367                    same_source.next().is_none().then(|| exact.clone())
1368                })
1369                .unwrap_or(ancestor);
1370            let ancestor = *index_by_node.entry(ancestor.clone()).or_insert_with(|| {
1371                let index = u32::try_from(nodes.len())
1372                    .expect("hierarchy index declarations must fit in a u32");
1373                nodes.push(ancestor);
1374                index
1375            });
1376            edges.push((ancestor, descendant));
1377        }
1378    }
1379    Some(DirectDescendantIndex::from_indexed_nodes(
1380        nodes,
1381        index_by_node,
1382        edges,
1383    ))
1384}
1385
1386fn traverse_hierarchy(
1387    root: &CodeUnit,
1388    query: &mut dyn HierarchyQuery,
1389    mut next: impl FnMut(&mut dyn HierarchyQuery, &CodeUnit) -> HierarchyAnswer,
1390) -> HierarchyAnswer {
1391    traverse_hierarchy_edges(root, |unit| next(query, unit))
1392}
1393
1394fn traverse_hierarchy_while(
1395    root: &CodeUnit,
1396    query: &mut dyn HierarchyQuery,
1397    cancellation: &CancellationToken,
1398    mut next: impl FnMut(&mut dyn HierarchyQuery, &CodeUnit) -> Result<HierarchyAnswer, Cancelled>,
1399) -> Result<HierarchyAnswer, Cancelled> {
1400    traverse_hierarchy_edges_while(root, cancellation, |unit| next(query, unit))
1401}
1402
1403/// [`traverse_hierarchy`] under a deadline: polls once per node popped and
1404/// propagates a stopped step as `Err(Cancelled)` rather than returning the nodes it had
1405/// already collected. A partial subtype set read as complete would let a caller
1406/// conclude that a class has no further subclasses.
1407fn traverse_hierarchy_edges_while<F>(
1408    root: &CodeUnit,
1409    cancellation: &CancellationToken,
1410    mut next: F,
1411) -> Result<HierarchyAnswer, Cancelled>
1412where
1413    F: FnMut(&CodeUnit) -> Result<HierarchyAnswer, Cancelled>,
1414{
1415    let mut result = HierarchyAnswer::default();
1416    let mut seen = BTreeSet::new();
1417    let mut queue = VecDeque::from([root.clone()]);
1418    while let Some(current) = queue.pop_front() {
1419        let answer = next(&current)?;
1420        result.ambiguous.extend(answer.ambiguous);
1421        result.open.extend(answer.open);
1422        for item in answer.edges {
1423            if seen.insert(item.fq_name()) {
1424                queue.push_back(item.clone());
1425                result.edges.push(item);
1426            }
1427        }
1428        if !queue.is_empty() && cancellation.is_cancelled() {
1429            return Err(Cancelled);
1430        }
1431    }
1432    Ok(result)
1433}
1434
1435fn traverse_hierarchy_edges<F>(root: &CodeUnit, mut next: F) -> HierarchyAnswer
1436where
1437    F: FnMut(&CodeUnit) -> HierarchyAnswer,
1438{
1439    // Both variants share the traversal and completeness accounting. An owned
1440    // live token makes stopping impossible for this unscoped request.
1441    traverse_hierarchy_edges_while(root, &CancellationToken::new(), |unit| Ok(next(unit)))
1442        .expect("unscoped hierarchy traversal cannot stop")
1443}
1444
1445#[cfg(test)]
1446mod hierarchy_traversal_properties {
1447    use super::*;
1448    use crate::analyzer::model::CodeUnitType;
1449    use proptest::prelude::*;
1450    use std::collections::BTreeMap;
1451
1452    const NODES: usize = 8;
1453
1454    fn node(index: usize) -> CodeUnit {
1455        CodeUnit::new(
1456            ProjectFile::new(std::env::temp_dir(), "Hierarchy.java"),
1457            CodeUnitType::Class,
1458            "",
1459            format!("N{index}"),
1460        )
1461    }
1462
1463    #[test]
1464    fn transitive_walks_borrow_one_query_and_preserve_scoped_dispatch() {
1465        use std::sync::atomic::{AtomicUsize, Ordering};
1466        struct Provider(AtomicUsize);
1467        struct Query;
1468        impl HierarchyQuery for Query {
1469            fn direct_ancestors(&mut self, unit: &CodeUnit) -> HierarchyAnswer {
1470                let index = index_of(unit);
1471                HierarchyAnswer::complete(if index < 2 {
1472                    vec![node(index + 1)]
1473                } else {
1474                    Vec::new()
1475                })
1476            }
1477            fn direct_descendants(&mut self, unit: &CodeUnit) -> HierarchyAnswer {
1478                self.direct_ancestors(unit)
1479            }
1480            fn member_family_ancestors(&mut self, _unit: &CodeUnit) -> MemberFamilyAncestors {
1481                MemberFamilyAncestors::default()
1482            }
1483            fn direct_descendants_within(
1484                &mut self,
1485                _unit: &CodeUnit,
1486                _scope: &DescendantIndexScope<'_>,
1487            ) -> Result<HierarchyAnswer, Cancelled> {
1488                Err(Cancelled)
1489            }
1490        }
1491        impl TypeHierarchyProvider for Provider {
1492            fn get_direct_ancestors(&self, _unit: &CodeUnit) -> HierarchyAnswer {
1493                panic!("the walk must use its query")
1494            }
1495            fn get_direct_descendants(&self, _unit: &CodeUnit) -> HierarchyAnswer {
1496                panic!("the walk must use its query")
1497            }
1498            fn with_hierarchy_query(&self, run: &mut dyn FnMut(&mut dyn HierarchyQuery)) {
1499                self.0.fetch_add(1, Ordering::Relaxed);
1500                run(&mut Query);
1501            }
1502        }
1503        let provider = Provider(AtomicUsize::new(0));
1504        assert_eq!(
1505            provider.get_ancestors(&node(0)).edges,
1506            vec![node(1), node(2)]
1507        );
1508        assert_eq!(provider.0.load(Ordering::Relaxed), 1);
1509        assert_eq!(
1510            provider.get_descendants(&node(0)).edges,
1511            vec![node(1), node(2)]
1512        );
1513        assert_eq!(provider.0.load(Ordering::Relaxed), 2);
1514        let cancellation = CancellationToken::new();
1515        assert!(
1516            provider
1517                .get_descendants_within(
1518                    &node(0),
1519                    &DescendantIndexScope::whole_workspace(&cancellation)
1520                )
1521                .is_err()
1522        );
1523        assert_eq!(provider.0.load(Ordering::Relaxed), 3);
1524    }
1525
1526    #[test]
1527    fn default_query_forwards_scoped_provider_overrides() {
1528        struct Provider;
1529        impl TypeHierarchyProvider for Provider {
1530            fn get_direct_ancestors(&self, _unit: &CodeUnit) -> HierarchyAnswer {
1531                HierarchyAnswer::complete(vec![node(1)])
1532            }
1533            fn get_direct_descendants(&self, _unit: &CodeUnit) -> HierarchyAnswer {
1534                HierarchyAnswer::complete(vec![node(2)])
1535            }
1536            fn get_direct_ancestors_within(
1537                &self,
1538                _unit: &CodeUnit,
1539                _scope: &DescendantIndexScope<'_>,
1540            ) -> Result<HierarchyAnswer, Cancelled> {
1541                Err(Cancelled)
1542            }
1543            fn member_family_ancestors(&self, _unit: &CodeUnit) -> MemberFamilyAncestors {
1544                MemberFamilyAncestors {
1545                    resolved: vec![node(3)],
1546                    ..Default::default()
1547                }
1548            }
1549        }
1550        let cancellation = CancellationToken::new();
1551        Provider.with_hierarchy_query(&mut |query| {
1552            assert_eq!(query.direct_ancestors(&node(0)).edges, vec![node(1)]);
1553            assert_eq!(query.direct_descendants(&node(0)).edges, vec![node(2)]);
1554            assert_eq!(
1555                query.member_family_ancestors(&node(0)).resolved,
1556                vec![node(3)]
1557            );
1558            assert!(
1559                query
1560                    .direct_ancestors_within(
1561                        &node(0),
1562                        &DescendantIndexScope::whole_workspace(&cancellation)
1563                    )
1564                    .is_err()
1565            );
1566        });
1567    }
1568
1569    fn index_of(unit: &CodeUnit) -> usize {
1570        unit.fq_name()[1..].parse().expect("test node name")
1571    }
1572
1573    #[test]
1574    fn closure_preserves_uncertainty_without_traversing_alternatives() {
1575        let mut visited = Vec::new();
1576        let answer = traverse_hierarchy_edges(&node(0), |unit| {
1577            let id = index_of(unit);
1578            visited.push(id);
1579            match id {
1580                0 => HierarchyAnswer {
1581                    edges: vec![node(1), node(1)],
1582                    ambiguous: vec![vec![node(2), node(3)]],
1583                    open: Vec::new(),
1584                },
1585                1 => HierarchyAnswer {
1586                    edges: Vec::new(),
1587                    ambiguous: vec![vec![node(4), node(5)]],
1588                    open: vec![HierarchyOpenReason::UnreadAncestor(
1589                        UnreadMemberFamilyAncestor {
1590                            name: "External".into(),
1591                            inventory: None,
1592                        },
1593                    )],
1594                },
1595                _ => panic!("ambiguous alternatives must not be traversed"),
1596            }
1597        });
1598        assert_eq!(visited, vec![0, 1]);
1599        assert_eq!(answer.edges, vec![node(1)]);
1600        assert_eq!(
1601            answer.ambiguous,
1602            vec![vec![node(2), node(3)], vec![node(4), node(5)]]
1603        );
1604        assert!(
1605            matches!(answer.open.as_slice(), [HierarchyOpenReason::UnreadAncestor(ancestor)] if ancestor.name == "External")
1606        );
1607        assert!(!answer.is_complete());
1608    }
1609
1610    /// Edge lists may repeat a target, hold self loops and form cycles.
1611    fn graphs() -> impl Strategy<Value = Vec<Vec<usize>>> {
1612        proptest::collection::vec(proptest::collection::vec(0..NODES, 0..5), NODES)
1613    }
1614
1615    /// Reference by relaxation: `distance[n]` is the fewest edges on a path
1616    /// from `root` to `n` that uses at least one edge. This makes the root
1617    /// reachable only through a cycle, which is what the traversal reports.
1618    fn reference_distances(graph: &[Vec<usize>], root: usize) -> BTreeMap<usize, usize> {
1619        let mut distance: BTreeMap<usize, usize> = BTreeMap::new();
1620        for &next in &graph[root] {
1621            distance.insert(next, 1);
1622        }
1623        loop {
1624            let mut changed = false;
1625            for (from, edges) in graph.iter().enumerate() {
1626                let Some(&base) = distance.get(&from) else {
1627                    continue;
1628                };
1629                for &to in edges {
1630                    if distance.get(&to).is_none_or(|&known| known > base + 1) {
1631                        distance.insert(to, base + 1);
1632                        changed = true;
1633                    }
1634                }
1635            }
1636            if !changed {
1637                return distance;
1638            }
1639        }
1640    }
1641
1642    proptest! {
1643        #![proptest_config(ProptestConfig::with_cases(128))]
1644
1645        /// On any graph, cyclic or not, the traversal terminates, returns each
1646        /// reachable declaration exactly once in breadth-first order, and asks
1647        /// for each declaration's neighbours once (the root twice when a cycle
1648        /// returns to it).
1649        #[test]
1650        fn visits_exactly_the_reachable_set_once_in_breadth_first_order(
1651            graph in graphs(),
1652            root in 0..NODES,
1653        ) {
1654            let mut calls: BTreeMap<usize, usize> = BTreeMap::new();
1655            let result = traverse_hierarchy_edges(&node(root), |unit| {
1656                let id = index_of(unit);
1657                *calls.entry(id).or_default() += 1;
1658                HierarchyAnswer::complete(graph[id].iter().map(|&to| node(to)).collect())
1659            });
1660            let found: Vec<usize> = result.edges.iter().map(index_of).collect();
1661
1662            let distance = reference_distances(&graph, root);
1663            let unique: BTreeSet<usize> = found.iter().copied().collect();
1664            prop_assert_eq!(found.len(), unique.len(), "a declaration was returned twice: {:?}", found);
1665            prop_assert_eq!(unique, distance.keys().copied().collect::<BTreeSet<_>>());
1666            prop_assert!(
1667                found.windows(2).all(|pair| distance[&pair[0]] <= distance[&pair[1]]),
1668                "not breadth-first: {:?} with distances {:?}", found, distance
1669            );
1670            // The root is not in the seen set when the walk starts, so a cycle
1671            // back to it expands it a second time; every other node once.
1672            prop_assert!(
1673                calls.iter().all(|(&id, &count)| count == 1 || (id == root && count == 2)),
1674                "repeated expansion: {:?}", calls
1675            );
1676
1677            // The deadline-aware variant agrees when never cancelled.
1678            let live = CancellationToken::new();
1679            let guarded = traverse_hierarchy_edges_while(&node(root), &live, |unit| {
1680                Ok(HierarchyAnswer::complete(graph[index_of(unit)].iter().map(|&to| node(to)).collect()))
1681            });
1682            let guarded = guarded.expect("live traversal");
1683            prop_assert!(guarded.is_complete());
1684            prop_assert_eq!(guarded.edges, result.edges);
1685        }
1686
1687        /// A failed neighbour step is propagated, not hidden behind a partial
1688        /// answer; a cancelled token stops a traversal that has work left but
1689        /// does not disturb one whose root has no neighbours.
1690        #[test]
1691        fn guarded_traversal_never_returns_a_partial_answer(
1692            graph in graphs(),
1693            root in 0..NODES,
1694            fail_on in 0..NODES,
1695        ) {
1696            let reachable = reference_distances(&graph, root);
1697            let failing = traverse_hierarchy_edges_while(&node(root), &CancellationToken::new(), |unit| {
1698                let id = index_of(unit);
1699                if id == fail_on { Err(Cancelled) } else { Ok(HierarchyAnswer::complete(graph[id].iter().map(|&to| node(to)).collect())) }
1700            });
1701            // The failing node's neighbours are requested when it is the root or
1702            // is itself returned, so it is only skipped when neither holds.
1703            let requested = fail_on == root || reachable.contains_key(&fail_on);
1704            prop_assert_eq!(failing.is_err(), requested);
1705
1706            let cancelled = CancellationToken::new();
1707            cancelled.cancel();
1708            let stopped = traverse_hierarchy_edges_while(&node(root), &cancelled, |unit| {
1709                Ok(HierarchyAnswer::complete(graph[index_of(unit)].iter().map(|&to| node(to)).collect()))
1710            });
1711            prop_assert_eq!(stopped.is_err(), !graph[root].is_empty());
1712            if graph[root].is_empty() {
1713                let stopped = stopped.expect("empty traversal");
1714                prop_assert!(stopped.is_complete());
1715                prop_assert!(stopped.edges.is_empty());
1716            }
1717        }
1718    }
1719}