Skip to main content

brokk_bifrost_ruby/
graph_support.rs

1//! The analyzer-resident products Ruby's language logic resolves through.
2//!
3//! `RubyAnalyzer` owns three moka caches, nine `Arc<OnceLock<..>>` cells and one
4//! `PoolSafeMemo`; every one of them stays in `brokk-bifrost-analysis` because
5//! `IAnalyzer::update`/`update_all` rebuild the analyzer wholesale through
6//! `Self::from_inner`. What crosses the crate line is the *decision* that fills
7//! each cell -- the builders in [`crate::imports`], [`crate::mixins`] and
8//! [`crate::hierarchy`] -- plus this trait, which is how a free function reaches
9//! back for a memoized product without naming the analyzer type.
10//!
11//! Two members are load-bearing beyond their signature:
12//!
13//! * [`RubySource::forward_owner_relation_facts`] is the one accessor with no
14//!   landed precedent. Its body reads `TreeSitterAnalyzer::fetch_file_state`,
15//!   whose `Arc<FileState>` is crate-private to analysis, so it stays there and
16//!   hands the decoded facts across (the Py-2 `collect_bounded` precedent).
17//! * [`RubySource::all_files`] is `TreeSitterAnalyzer::all_files`, i.e. the
18//!   analyzed *live* file set. `CodeUnitIndex::analyzed_files` is a different
19//!   query, so this is spelled out rather than inferred from the supertrait.
20
21use crate::mixins::RubyOwnerRelationFact;
22use brokk_bifrost_core::analyzer::capabilities::{ImportAnalysisProvider, TypeHierarchyProvider};
23use brokk_bifrost_core::analyzer::model::RubyMethodDispatchMode;
24use brokk_bifrost_core::analyzer::type_relations::{TypeRelation, TypeRelationKind};
25use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
26use brokk_bifrost_core::hash::{HashMap, HashSet};
27use std::sync::Arc;
28
29pub trait RubySource: CodeUnitIndex + TypeHierarchyProvider + ImportAnalysisProvider {
30    /// The analyzed live file set (`TreeSitterAnalyzer::all_files`).
31    fn all_files(&self) -> Vec<ProjectFile>;
32
33    /// Workspace-wide `autoload :Const, "path"` edges, keyed by the `$`-joined
34    /// constant name. Built by [`crate::imports::build_autoload_constant_files`].
35    fn autoload_constant_files(&self) -> &HashMap<String, HashSet<ProjectFile>>;
36
37    /// Whether `Gemfile`/`Gemfile.lock` declare rails or zeitwerk. Built by
38    /// [`crate::imports::detect_zeitwerk_autoload_conventions`].
39    fn has_zeitwerk_autoload_conventions(&self) -> bool;
40
41    fn zeitwerk_autoload_files(&self) -> &HashSet<ProjectFile>;
42
43    fn zeitwerk_consumer_files(&self) -> &HashSet<ProjectFile>;
44
45    fn zeitwerk_autoload_code_units(&self) -> &HashSet<CodeUnit>;
46
47    /// `required_files` inverted over the whole workspace, memoized through the
48    /// analyzer's `PoolSafeMemo`.
49    fn reverse_import_index(&self) -> Arc<HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>>;
50
51    /// The resolved `include`/`prepend`/`extend` graph. Built by
52    /// [`crate::mixins::ruby_collect_mixin_relations`].
53    fn mixin_relations(&self) -> &[TypeRelation];
54
55    /// Built by [`build_ruby_semantic_facts`].
56    fn semantic_facts(&self) -> &RubySemanticFacts;
57
58    /// Class/module declarations indexed by trailing identifier. Built by
59    /// [`crate::hierarchy::build_ruby_types_by_identifier`].
60    fn types_by_identifier(&self) -> &HashMap<String, Vec<CodeUnit>>;
61
62    /// The persisted per-method dispatch mode, defaulting to `Instance`.
63    fn method_dispatch_mode(&self, unit: &CodeUnit) -> RubyMethodDispatchMode;
64
65    /// The decoded superclass and mixin relations declared by `owner`, read out
66    /// of the analyzer's persisted per-file state. See this module's note.
67    fn forward_owner_relation_facts(&self, owner: &CodeUnit) -> Vec<RubyOwnerRelationFact>;
68}
69
70/// The workspace-wide ancestry and mixin projection every proven method or
71/// constant resolution reads. Populated only for a target-scoped scan; the
72/// target-free lookup path resolves the same questions forward, per owner.
73pub struct RubySemanticFacts {
74    pub ancestors: HashMap<String, HashSet<String>>,
75    pub mixin_included_owners: HashMap<String, Vec<String>>,
76    pub mixin_prepended_owners: HashMap<String, Vec<String>>,
77    pub mixin_class_owners: HashMap<String, Vec<String>>,
78}
79
80pub fn build_ruby_semantic_facts(ruby: &dyn RubySource) -> RubySemanticFacts {
81    let mut ancestors = HashMap::default();
82    let mut mixin_included_owners: HashMap<String, Vec<String>> = HashMap::default();
83    let mut mixin_prepended_owners: HashMap<String, Vec<String>> = HashMap::default();
84    let mut mixin_class_owners: HashMap<String, Vec<String>> = HashMap::default();
85
86    for unit in ruby
87        .all_declarations()
88        .filter(|unit| unit.is_class() || unit.is_module())
89    {
90        let direct = ruby
91            .get_direct_ancestors(&unit)
92            .into_iter()
93            .map(|ancestor| ancestor.fq_name())
94            .collect();
95        ancestors.insert(unit.fq_name(), direct);
96    }
97
98    for relation in ruby.mixin_relations() {
99        let entry = match relation.kind {
100            TypeRelationKind::MixinInclude => &mut mixin_included_owners,
101            TypeRelationKind::MixinPrepend => &mut mixin_prepended_owners,
102            TypeRelationKind::MixinExtend => &mut mixin_class_owners,
103            _ => continue,
104        };
105        push_ordered_mixin(entry, relation.from.fq_name(), relation.to.fq_name());
106    }
107
108    RubySemanticFacts {
109        ancestors,
110        mixin_included_owners,
111        mixin_prepended_owners,
112        mixin_class_owners,
113    }
114}
115
116fn push_ordered_mixin(index: &mut HashMap<String, Vec<String>>, from: String, to: String) {
117    let owners = index.entry(from).or_default();
118    if !owners.contains(&to) {
119        owners.push(to);
120    }
121}