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