Skip to main content

brokk_bifrost_cpp/graph/
mod.rs

1//! The C++ usage graph's language knowledge.
2//!
3//! The forward scan ([`extractor`] plus [`hits`]), the visibility/macro/include
4//! resolver ([`resolver`]) and the whole-workspace inverted per-file walk
5//! ([`inverted`]) are one body of code and crossed together: `extractor` glob
6//! imports `resolver`, `hits` reads `extractor`'s scan context, and `inverted`
7//! names forty items from the other two.
8//!
9//! No analyzer handle appears here. `brokk-bifrost-analysis` downcasts once and
10//! hands over a [`CppGraphSource`] -- the *dispatching* analyzer's side of a
11//! scan -- which carries the [`CppSource`] the memoized C++ products
12//! come from.
13
14pub mod extractor;
15pub mod hits;
16pub mod inverted;
17pub mod resolver;
18pub mod syntax;
19
20use crate::graph_support::CppSource;
21use brokk_bifrost_core::analyzer::capabilities::{TypeAliasProvider, TypeHierarchyProvider};
22use brokk_bifrost_core::analyzer::model::{CppFieldLinkage, SignatureMetadata};
23use brokk_bifrost_core::analyzer::query_token::QueryToken;
24use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile, Range};
25use std::collections::BTreeSet;
26
27/// The workspace-wide questions a C++ scan asks of the *dispatching* analyzer
28/// rather than of the C++ analyzer.
29///
30/// Two of them have no core capability to sit on: `import_statements` is
31/// `IAnalyzer`'s raw `#include` lines, and the workspace definition index is
32/// reached through an analysis-side `DefinitionIndexHandle` that is built per
33/// call and so cannot be borrowed as a `&dyn BoundedDefinitionLookup`. Both
34/// stay the dispatching analyzer's job -- in a mixed workspace the query is
35/// issued against a `MultiAnalyzer` whose shards span languages, and the C++
36/// owner resolution depends on that reach.
37pub trait CppWorkspaceSource {
38    /// The raw import (`#include`) lines recorded for `file`.
39    fn import_statements(&self, file: &ProjectFile) -> Vec<String>;
40
41    /// Declarations in the workspace usage-definition index whose fq name is
42    /// exactly `fqn`, across every shard.
43    ///
44    /// Borrows the shard-owned units rather than cloning them. Two constraints
45    /// make that the only workable shape. Every owner-resolution caller filters
46    /// the result and clones at most one survivor, so cloning every match per
47    /// reference was pure waste; and the global-field linkage walk returns its
48    /// matches to a caller that outlives the lookup, so they must borrow the
49    /// analyzer. Both are why the impls read the index shard-by-shard: the
50    /// per-call `DefinitionIndexHandle` dies with the call.
51    fn definitions_by_fqn(&self, token: QueryToken<'_>, fqn: &str) -> Vec<&CodeUnit>;
52}
53
54/// The workspace definition index, spelled so a call reads exactly as it did
55/// against `IAnalyzer::global_usage_definition_index`.
56///
57/// Carries the request scope's [`QueryToken`] alongside the source, so a
58/// lookup made through it is proof-carrying without every C++ call site
59/// re-threading the token (issue #2423 milestone B).
60#[derive(Clone, Copy)]
61pub struct CppWorkspaceDefinitions<'a>(&'a dyn CppWorkspaceSource, QueryToken<'a>);
62
63impl<'a> CppWorkspaceDefinitions<'a> {
64    // `self.0` is copied out rather than reborrowed through `&self`, so the
65    // returned borrows carry the source's `'a` and can outlive this call.
66    pub fn fqn(&self, fqn: &str) -> Vec<&'a CodeUnit> {
67        self.0.definitions_by_fqn(self.1, fqn)
68    }
69}
70
71/// The *dispatching* analyzer's side of a C++ usage-graph scan.
72///
73/// Deliberately not the C++ analyzer, for the reason recorded on
74/// `PythonGraphSource` and `CSharpGraphSource`: in a mixed workspace the query
75/// is issued against a `MultiAnalyzer`, whose `definitions` merges every
76/// language's shards and whose provider accessors cross language boundaries.
77/// The C++ analyzer that answers the C++-only questions rides along in
78/// [`Self::cpp`], resolved once by the shim's `resolve_analyzer::<CppAnalyzer>`
79/// downcast instead of once per call site as before the move; `None` is the
80/// same answer that downcast's `else` arm gave.
81#[derive(Clone, Copy)]
82pub struct CppGraphSource<'a> {
83    pub index: &'a dyn CodeUnitIndex,
84    pub cpp: Option<&'a dyn CppSource>,
85    pub aliases: Option<&'a dyn TypeAliasProvider>,
86    pub hierarchy: Option<&'a dyn TypeHierarchyProvider>,
87    pub workspace: &'a dyn CppWorkspaceSource,
88    /// Proof that the request scope this bundle serves is open. The bundle is
89    /// a per-query object built at a query boundary, so the syntax accessors
90    /// its resolution paths reach can take the proof from here rather than
91    /// from ninety extra parameters (issue #2414 step 3).
92    pub token: QueryToken<'a>,
93}
94
95impl<'a> CppGraphSource<'a> {
96    /// The C++ source standing in for the dispatching analyzer.
97    ///
98    /// For the four resolution paths that only ever had the concrete C++
99    /// analyzer in hand: they passed `&CppAnalyzer` where a `&dyn IAnalyzer`
100    /// was wanted, and its `type_alias_provider()`/`type_hierarchy_provider()`
101    /// both answered `Some(self)`, so every field is the same object here too.
102    pub fn from_source(source: &'a dyn CppSource, token: QueryToken<'a>) -> Self {
103        Self {
104            index: source,
105            cpp: Some(source),
106            aliases: Some(source),
107            hierarchy: Some(source),
108            workspace: source,
109            token,
110        }
111    }
112
113    pub fn type_alias_provider(&self) -> Option<&'a dyn TypeAliasProvider> {
114        self.aliases
115    }
116
117    pub fn type_hierarchy_provider(&self) -> Option<&'a dyn TypeHierarchyProvider> {
118        self.hierarchy
119    }
120
121    pub fn import_statements(&self, file: &ProjectFile) -> Vec<String> {
122        self.workspace.import_statements(file)
123    }
124
125    pub fn global_usage_definition_index(&self) -> CppWorkspaceDefinitions<'a> {
126        CppWorkspaceDefinitions(self.workspace, self.token)
127    }
128
129    pub fn parent_of(&self, code_unit: &CodeUnit) -> Option<CodeUnit> {
130        self.index.parent_of(code_unit)
131    }
132
133    pub fn ranges(&self, code_unit: &CodeUnit) -> Vec<Range> {
134        self.index.ranges(code_unit)
135    }
136
137    pub fn enclosing_code_unit(&self, file: &ProjectFile, range: &Range) -> Option<CodeUnit> {
138        self.index.enclosing_code_unit(file, range)
139    }
140
141    pub fn signature_metadata(&self, code_unit: &CodeUnit) -> Vec<SignatureMetadata> {
142        self.index.signature_metadata(code_unit)
143    }
144
145    pub fn cpp_field_linkage(&self, code_unit: &CodeUnit) -> Option<CppFieldLinkage> {
146        self.cpp?.cpp_field_linkage(code_unit)
147    }
148
149    pub fn signatures(&self, code_unit: &CodeUnit) -> Vec<String> {
150        self.index.signatures(code_unit)
151    }
152
153    pub fn get_source(&self, code_unit: &CodeUnit, include_comments: bool) -> Option<String> {
154        self.index.get_source(code_unit, include_comments)
155    }
156
157    pub fn indexed_source(&self, file: &ProjectFile) -> Option<String> {
158        self.index.indexed_source(file)
159    }
160
161    pub fn declarations(&self, file: &ProjectFile) -> BTreeSet<CodeUnit> {
162        self.index.declarations(file)
163    }
164
165    /// Whether a reference written in `file` reads C++ source with C semantics
166    /// (issue #1970). Without a C++ analyzer behind this source only the path
167    /// evidence is available, which is exactly what
168    /// [`resolver::is_c_source_file`] answered before headers gained a second
169    /// reading.
170    pub fn reference_uses_c_semantics(&self, file: &ProjectFile) -> bool {
171        match self.cpp {
172            Some(cpp) => resolver::reference_uses_c_semantics(cpp, file),
173            None => resolver::is_c_source_file(file),
174        }
175    }
176
177    /// [`crate::graph_support::CppSource::declarations_in_reading`], falling
178    /// back to the single reading a bare index can serve.
179    pub fn declarations_in_reading(
180        &self,
181        file: &ProjectFile,
182        c_semantics: bool,
183    ) -> BTreeSet<CodeUnit> {
184        match self.cpp {
185            Some(cpp) if c_semantics => cpp.declarations_in_reading(file, true),
186            _ => self.index.declarations(file),
187        }
188    }
189
190    /// [`crate::graph_support::CppSource::site_equivalent_units`], empty
191    /// without a C++ analyzer.
192    pub fn site_equivalent_units(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
193        match self.cpp {
194            Some(cpp) => cpp.site_equivalent_units(code_unit),
195            None => Vec::new(),
196        }
197    }
198
199    pub fn direct_children(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
200        self.index.direct_children(code_unit)
201    }
202
203    pub fn definitions(&self, fq_name: &str) -> Box<dyn Iterator<Item = CodeUnit> + '_> {
204        self.index.definitions(fq_name)
205    }
206}
207
208/// [`crate::identity::cpp_callable_definitions_share_identity_evidence`] with
209/// its header/implementation evidence root supplied from the graph source.
210///
211/// The searchtools consumers reach the same predicate through the shim wrapper
212/// that owns the `resolve_analyzer` downcast; the scan already holds the
213/// resolved C++ source, so it passes the include index in directly. A source
214/// without a C++ analyzer answers `false`, exactly as the downcast's `else` arm
215/// did.
216pub fn callable_definitions_share_identity_evidence(
217    analyzer: &CppGraphSource<'_>,
218    left: &CodeUnit,
219    right: &CodeUnit,
220) -> bool {
221    crate::identity::cpp_callable_definitions_share_identity_evidence(
222        analyzer.index,
223        left,
224        right,
225        |left_source, right_source| {
226            let Some(implementation) =
227                crate::identity::cpp_header_body_implementation_file(left_source, right_source)
228            else {
229                return false;
230            };
231            let Some(cpp) = analyzer.cpp else {
232                return false;
233            };
234            crate::identity::cpp_header_body_files_are_related(
235                left_source,
236                right_source,
237                &analyzer.import_statements(implementation),
238                cpp.include_target_index(),
239            )
240        },
241    )
242}