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: CapabilityProvider + 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 collect_edges = |file: &ProjectFile| {
612 let source = file.clone();
613 resolve_targets(file)
614 .into_iter()
615 .filter_map(move |target| (target != source).then(|| (target, source.clone())))
616 .collect::<Vec<_>>()
617 };
618 let edges: Vec<_> = if parallel {
619 files.par_iter().flat_map(collect_edges).collect()
620 } else {
621 files.iter().flat_map(collect_edges).collect()
622 };
623
624 let mut reverse: HashMap<ProjectFile, HashSet<ProjectFile>> = HashMap::default();
625 for (target, source) in edges {
626 reverse.entry(target).or_default().insert(source);
627 }
628 reverse
629 .into_iter()
630 .map(|(file, refs)| (file, Arc::new(refs)))
631 .collect()
632}
633
634/// `Send + Sync` like its three sibling capabilities: every implementor is an
635/// analyzer, and a parallel whole-workspace scan holds the provider across its
636/// fan-out (`kotlin_graph`'s edge builder does exactly that).
637pub trait TypeAliasProvider: CapabilityProvider + Send + Sync {
638 fn is_type_alias(&self, _code_unit: &CodeUnit) -> bool {
639 false
640 }
641}
642
643/// Whether a capability provider classifies `code_unit` as a type alias.
644pub fn provider_declares_type_alias(
645 provider: Option<&dyn TypeAliasProvider>,
646 code_unit: &CodeUnit,
647) -> bool {
648 provider.is_some_and(|provider| provider.is_type_alias(code_unit))
649}
650
651pub trait TestDetectionProvider: CapabilityProvider {}
652
653/// Which slice of the workspace a descendant index covers.
654///
655/// This is the memo key, and two values is the whole range: the excluded set is
656/// a pure function of the analyzer and the file, so every request that asks to
657/// leave test files out describes the same index.
658#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
659pub enum DescendantIndexVariant {
660 /// Every class-like declaration the analyzer indexes.
661 WholeWorkspace,
662 /// The declarations whose source file the caller's predicate rejected are
663 /// left out. In practice that is a `scan_usages` request with
664 /// `include_tests: false`, whose test files the answer would discard anyway.
665 ProductionOnly,
666}
667
668/// The request state a whole-workspace descendant-index build needs: the
669/// deadline it polls, and the slice of the workspace it covers.
670///
671/// The two travel together because one loop consumes both -- the per-class loop
672/// in [`build_direct_descendant_index_from_candidates`] and, for C++, the
673/// per-file include-closure walk beneath it -- and because the slice is the
674/// memo key that the deadline's complete-or-nothing rule is applied to.
675///
676/// The exclusion predicate is supplied by the caller rather than named here.
677/// Test classification needs an analyzer (`file_is_test_only` reads a language
678/// module index), which this crate must not know about; the closure crossing is
679/// the same one the Rust usage walks already use for their `keep_going`
680/// predicate. **The predicate must be a pure function of the analyzer and the
681/// file.** That is the condition that makes [`DescendantIndexVariant`] a
682/// complete key: two requests that both exclude test files must describe the
683/// same index, or one would be served the other's.
684#[derive(Clone, Copy)]
685pub struct DescendantIndexScope<'a> {
686 cancellation: &'a CancellationToken,
687 excluded_source: Option<&'a dyn Fn(&ProjectFile) -> bool>,
688}
689
690impl<'a> DescendantIndexScope<'a> {
691 /// Every class in the workspace, stopping when `cancellation` says to.
692 pub fn whole_workspace(cancellation: &'a CancellationToken) -> Self {
693 Self {
694 cancellation,
695 excluded_source: None,
696 }
697 }
698
699 /// Every class whose source file `excluded` rejects is left out of the
700 /// index entirely -- it is never handed to `get_direct_ancestors`, so the
701 /// per-file resolution work behind that call is never charged.
702 pub fn excluding_sources(
703 cancellation: &'a CancellationToken,
704 excluded: &'a dyn Fn(&ProjectFile) -> bool,
705 ) -> Self {
706 Self {
707 cancellation,
708 excluded_source: Some(excluded),
709 }
710 }
711
712 pub fn cancellation(&self) -> &'a CancellationToken {
713 self.cancellation
714 }
715
716 pub fn variant(&self) -> DescendantIndexVariant {
717 match self.excluded_source {
718 Some(_) => DescendantIndexVariant::ProductionOnly,
719 None => DescendantIndexVariant::WholeWorkspace,
720 }
721 }
722
723 /// The poll predicate for a builder that would rather take a plain
724 /// `Fn() -> bool` than a token.
725 pub fn keep_going(&self) -> impl Fn() -> bool + use<'_> {
726 || !self.cancellation.is_cancelled()
727 }
728
729 /// Whether `declaration` belongs in this index.
730 pub fn admits(&self, declaration: &CodeUnit) -> bool {
731 self.excluded_source
732 .is_none_or(|excluded| !excluded(declaration.source()))
733 }
734}
735
736fn query_within_scope<T>(scope: &DescendantIndexScope<'_>, query: impl FnOnce() -> T) -> Option<T> {
737 (!scope.cancellation().is_cancelled()).then(query)
738}
739
740pub trait TypeHierarchyProvider: CapabilityProvider + Send + Sync {
741 fn get_direct_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit>;
742 fn get_direct_descendants(&self, code_unit: &CodeUnit) -> HashSet<CodeUnit>;
743
744 fn supports_type_hierarchy(&self, code_unit: &CodeUnit) -> bool {
745 code_unit.is_class()
746 }
747
748 fn get_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
749 traverse_hierarchy(code_unit, |next| self.get_direct_ancestors(next))
750 }
751
752 fn get_descendants(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
753 traverse_hierarchy(code_unit, |next| {
754 self.get_direct_descendants(next).into_iter().collect()
755 })
756 }
757
758 /// [`Self::get_direct_ancestors`] under a caller's deadline.
759 ///
760 /// `None` means the resolution stopped short; it is never an empty answer,
761 /// which stays the honest way to say "no resolvable base types". The
762 /// default checks the deadline once and delegates, which is right for a
763 /// provider whose ancestor resolution is a lookup. C++ overrides it because
764 /// resolving one class's bases means walking that file's whole transitive
765 /// `#include` closure.
766 fn get_direct_ancestors_within(
767 &self,
768 code_unit: &CodeUnit,
769 scope: &DescendantIndexScope<'_>,
770 ) -> Option<Vec<CodeUnit>> {
771 query_within_scope(scope, || self.get_direct_ancestors(code_unit))
772 }
773
774 /// [`Self::get_direct_descendants`] under a caller's deadline and workspace
775 /// slice.
776 ///
777 /// `None` means the answer is not available because the build stopped
778 /// short. It is never a partial answer, and a stopped build publishes
779 /// nothing: a truncated index memoized as complete would be served to every
780 /// later caller as the truth (the rule
781 /// `cancelled_cold_candidate_discovery_does_not_publish_partial_index`
782 /// pins for the Rust walk caches).
783 ///
784 /// The default checks the deadline once and delegates, which is the correct
785 /// behaviour for a provider whose descendant answer is bounded by
786 /// construction. Every provider that inverts the ancestor relation over the
787 /// whole workspace overrides this; a provider that also honours
788 /// [`DescendantIndexScope::admits`] builds one index per
789 /// [`DescendantIndexVariant`]. Ignoring `admits` is sound -- the index is
790 /// then a superset and the caller's own file filter still applies -- so
791 /// only the providers where the prune pays for itself implement it.
792 fn get_direct_descendants_within(
793 &self,
794 code_unit: &CodeUnit,
795 scope: &DescendantIndexScope<'_>,
796 ) -> Option<HashSet<CodeUnit>> {
797 query_within_scope(scope, || self.get_direct_descendants(code_unit))
798 }
799
800 /// [`Self::get_descendants`] under a caller's deadline and workspace slice,
801 /// polling once per node of the walk. `None` carries the same meaning as in
802 /// [`Self::get_direct_descendants_within`].
803 fn get_descendants_within(
804 &self,
805 code_unit: &CodeUnit,
806 scope: &DescendantIndexScope<'_>,
807 ) -> Option<Vec<CodeUnit>> {
808 traverse_hierarchy_while(code_unit, scope.cancellation(), |next| {
809 self.get_direct_descendants_within(next, scope)
810 .map(|descendants| descendants.into_iter().collect())
811 })
812 }
813
814 fn get_polymorphic_matches<T: CodeUnitIndex>(
815 &self,
816 target: &CodeUnit,
817 analyzer: &T,
818 ) -> Vec<CodeUnit>
819 where
820 Self: Sized,
821 {
822 if !target.is_function() {
823 return Vec::new();
824 }
825
826 let Some(parent) = analyzer.parent_of(target) else {
827 return Vec::new();
828 };
829
830 self.get_descendants(&parent)
831 }
832}
833
834/// Exact declaration identities plus compact ancestor-to-descendant rows.
835pub struct DirectDescendantIndex {
836 nodes: Box<[CodeUnit]>,
837 row_by_ancestor: HashMap<CodeUnit, u32>,
838 descendants: CompactRows<u32>,
839}
840
841impl DirectDescendantIndex {
842 pub fn from_indexed_nodes(
843 nodes: Vec<CodeUnit>,
844 index_by_node: HashMap<CodeUnit, u32>,
845 mut edges: Vec<(u32, u32)>,
846 ) -> Self {
847 normalize_indexed_edges(&nodes, &index_by_node, &mut edges);
848
849 let row_count = usize::from(!edges.is_empty())
850 + edges
851 .windows(2)
852 .filter(|pair| pair[0].0 != pair[1].0)
853 .count();
854 let mut row_by_ancestor = HashMap::default();
855 let mut descendants = CompactRowsBuilder::with_capacity(row_count, edges.len());
856 let mut cursor = 0;
857 while cursor < edges.len() {
858 let ancestor = edges[cursor].0;
859 let start = cursor;
860 while cursor < edges.len() && edges[cursor].0 == ancestor {
861 cursor += 1;
862 }
863 let row =
864 u32::try_from(descendants.rows()).expect("hierarchy index rows must fit in a u32");
865 row_by_ancestor.insert(nodes[ancestor as usize].clone(), row);
866 descendants.push_row(
867 edges[start..cursor]
868 .iter()
869 .map(|(_, descendant)| *descendant),
870 );
871 }
872 Self {
873 nodes: nodes.into_boxed_slice(),
874 row_by_ancestor,
875 descendants: descendants.finish(),
876 }
877 }
878
879 pub fn descendants(&self, ancestor: &CodeUnit) -> HashSet<CodeUnit> {
880 let Some(row) = self.row_by_ancestor.get(ancestor).copied() else {
881 return HashSet::default();
882 };
883 self.descendants
884 .row(row as usize)
885 .iter()
886 .map(|descendant| self.nodes[*descendant as usize].clone())
887 .collect()
888 }
889}
890
891/// Answer a descendant query from a variant-keyed index cell, building the
892/// variant the caller asked for if it is missing.
893///
894/// Every analyzer that memoizes a whole-workspace descendant index repeats this
895/// same dance: pick the cell for the scope's variant, build while the deadline
896/// holds, publish nothing if it does not, project the ancestor's row. It lives
897/// here so the complete-or-nothing rule is stated once instead of copied into
898/// each language module.
899pub fn descendants_from_variant_index(
900 index: &KeyedPoolSafeMemo<DescendantIndexVariant, DirectDescendantIndex>,
901 scope: &DescendantIndexScope<'_>,
902 code_unit: &CodeUnit,
903 build: impl Fn() -> Option<DirectDescendantIndex>,
904) -> Option<HashSet<CodeUnit>> {
905 Some(
906 index
907 .cell(&scope.variant())
908 // The builders are serial, so the same closure serves both memo
909 // arms; the memo's value here is the non-blocking claim protocol.
910 .get_or_build_while(&scope.keep_going(), &build, &build)?
911 .descendants(code_unit),
912 )
913}
914
915/// Invert the ancestor relation over every class the analyzer indexes.
916///
917/// `scope` decides two things. Its predicate drops declarations before they are
918/// ever handed to `get_direct_ancestors`, which is where the per-declaration
919/// resolution cost is charged (for C++ that is a whole transitive `#include`
920/// closure per declaring file). Its token bounds the loop: `None` means the
921/// build stopped short and must not be published.
922pub fn build_direct_descendant_index<A, P>(
923 analyzer: &A,
924 provider: &P,
925 scope: &DescendantIndexScope<'_>,
926) -> Option<DirectDescendantIndex>
927where
928 A: CodeUnitIndex,
929 P: TypeHierarchyProvider + ?Sized,
930{
931 build_direct_descendant_index_from_candidates(
932 analyzer
933 .all_declarations()
934 .filter(|candidate| candidate.is_class() && scope.admits(candidate))
935 .collect(),
936 |candidate| provider.get_direct_ancestors_within(candidate, scope),
937 &scope.keep_going(),
938 )
939}
940
941/// The edge-building half of [`build_direct_descendant_index`], for providers
942/// that assemble their candidate list some other way.
943///
944/// `keep_going` is polled once per candidate. That granularity is the natural
945/// checkpoint: one candidate is one `direct_ancestors` call, which is the unit
946/// of work whose tens of thousands of repetitions made this loop unbounded in
947/// the first place (issue #1748). `direct_ancestors` answers `None` when its
948/// own work stopped short, which stops this loop too -- a candidate whose bases
949/// were half-resolved must not contribute a half-populated edge set.
950pub fn build_direct_descendant_index_from_candidates<F>(
951 mut candidates: Vec<CodeUnit>,
952 mut direct_ancestors: F,
953 keep_going: &dyn Fn() -> bool,
954) -> Option<DirectDescendantIndex>
955where
956 F: FnMut(&CodeUnit) -> Option<Vec<CodeUnit>>,
957{
958 candidates.sort();
959 candidates.dedup();
960 let mut types_by_fq_name: HashMap<String, Vec<CodeUnit>> = HashMap::default();
961 for candidate in &candidates {
962 types_by_fq_name
963 .entry(candidate.fq_name())
964 .or_default()
965 .push(candidate.clone());
966 }
967 let mut nodes = candidates.clone();
968 let mut index_by_node: HashMap<_, _> = nodes
969 .iter()
970 .enumerate()
971 .map(|(index, node)| {
972 (
973 node.clone(),
974 u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
975 )
976 })
977 .collect();
978 let mut edges = Vec::new();
979 for candidate in candidates {
980 if !keep_going() {
981 return None;
982 }
983 let descendant = index_by_node[&candidate];
984 for ancestor in direct_ancestors(&candidate)? {
985 let ancestor = types_by_fq_name
986 .get(&ancestor.fq_name())
987 .and_then(|same_name| {
988 let mut same_source = same_name
989 .iter()
990 .filter(|unit| unit.source() == candidate.source());
991 let exact = same_source.next()?;
992 same_source.next().is_none().then(|| exact.clone())
993 })
994 .unwrap_or(ancestor);
995 let ancestor = *index_by_node.entry(ancestor.clone()).or_insert_with(|| {
996 let index = u32::try_from(nodes.len())
997 .expect("hierarchy index declarations must fit in a u32");
998 nodes.push(ancestor);
999 index
1000 });
1001 edges.push((ancestor, descendant));
1002 }
1003 }
1004 Some(DirectDescendantIndex::from_indexed_nodes(
1005 nodes,
1006 index_by_node,
1007 edges,
1008 ))
1009}
1010
1011/// [`traverse_hierarchy`] under a deadline: polls once per node popped and
1012/// propagates a stopped step as `None` rather than returning the nodes it had
1013/// already collected. A partial subtype set read as complete would let a caller
1014/// conclude that a class has no further subclasses.
1015fn traverse_hierarchy_while<F>(
1016 root: &CodeUnit,
1017 cancellation: &CancellationToken,
1018 mut next: F,
1019) -> Option<Vec<CodeUnit>>
1020where
1021 F: FnMut(&CodeUnit) -> Option<Vec<CodeUnit>>,
1022{
1023 let direct = next(root)?;
1024 if direct.is_empty() {
1025 return Some(Vec::new());
1026 }
1027
1028 let mut seen = BTreeSet::new();
1029 let mut result = Vec::new();
1030 let mut queue = VecDeque::new();
1031
1032 for item in direct {
1033 if seen.insert(item.fq_name()) {
1034 queue.push_back(item.clone());
1035 result.push(item);
1036 }
1037 }
1038
1039 while let Some(current) = queue.pop_front() {
1040 if cancellation.is_cancelled() {
1041 return None;
1042 }
1043 for item in next(¤t)? {
1044 if seen.insert(item.fq_name()) {
1045 queue.push_back(item.clone());
1046 result.push(item);
1047 }
1048 }
1049 }
1050
1051 Some(result)
1052}
1053
1054fn traverse_hierarchy<F>(root: &CodeUnit, mut next: F) -> Vec<CodeUnit>
1055where
1056 F: FnMut(&CodeUnit) -> Vec<CodeUnit>,
1057{
1058 let direct = next(root);
1059 if direct.is_empty() {
1060 return Vec::new();
1061 }
1062
1063 let mut seen = BTreeSet::new();
1064 let mut result = Vec::new();
1065 let mut queue = VecDeque::new();
1066
1067 for item in direct {
1068 if seen.insert(item.fq_name()) {
1069 queue.push_back(item.clone());
1070 result.push(item);
1071 }
1072 }
1073
1074 while let Some(current) = queue.pop_front() {
1075 for item in next(¤t) {
1076 if seen.insert(item.fq_name()) {
1077 queue.push_back(item.clone());
1078 result.push(item);
1079 }
1080 }
1081 }
1082
1083 result
1084}