Skip to main content

ryo_analysis/discovery/
result.rs

1//! DiscoveryResult - Results from symbol discovery.
2
3use crate::symbol::{FileSpan, SymbolId, SymbolPath, Uuid, Visibility};
4use crate::SymbolKind;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Result of a discovery query.
9#[derive(Debug, Clone, Default)]
10pub struct DiscoveryResult {
11    /// Discovered symbols.
12    pub symbols: Vec<DiscoveredSymbol>,
13    /// Relation graph (if relations were requested).
14    pub relations: Option<RelationGraph>,
15    /// Total matches before limit was applied.
16    pub total_matches: usize,
17    /// Whether results were truncated by limit.
18    pub truncated: bool,
19}
20
21impl DiscoveryResult {
22    /// Create an empty result.
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Add a discovered symbol.
28    pub fn add(&mut self, symbol: DiscoveredSymbol) {
29        self.symbols.push(symbol);
30    }
31
32    /// Get the number of symbols found.
33    pub fn len(&self) -> usize {
34        self.symbols.len()
35    }
36
37    /// Check if no symbols were found.
38    pub fn is_empty(&self) -> bool {
39        self.symbols.is_empty()
40    }
41
42    /// Get the first symbol.
43    pub fn first(&self) -> Option<&DiscoveredSymbol> {
44        self.symbols.first()
45    }
46
47    /// Iterate over symbols.
48    pub fn iter(&self) -> impl Iterator<Item = &DiscoveredSymbol> {
49        self.symbols.iter()
50    }
51
52    /// Get symbols as paths.
53    pub fn paths(&self) -> impl Iterator<Item = &SymbolPath> {
54        self.symbols.iter().map(|s| &s.path)
55    }
56
57    /// Get symbols as IDs.
58    pub fn ids(&self) -> impl Iterator<Item = SymbolId> + '_ {
59        self.symbols.iter().map(|s| s.id)
60    }
61}
62
63/// A discovered symbol with metadata.
64#[derive(Debug, Clone, Serialize)]
65pub struct DiscoveredSymbol {
66    /// Symbol ID (session-volatile).
67    pub id: SymbolId,
68    /// Persistent UUID for cross-session tracking.
69    ///
70    /// This UUID survives server restarts and symbol renames.
71    /// Returns `None` if the symbol hasn't been assigned a persistent ID.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub uuid: Option<Uuid>,
74    /// Symbol path.
75    pub path: SymbolPath,
76    /// Symbol kind.
77    pub kind: SymbolKind,
78    /// File location (if available).
79    pub span: Option<FileSpan>,
80    /// Visibility (if available).
81    pub visibility: Option<Visibility>,
82    /// Match score (for ranking).
83    pub score: f32,
84    /// Reference count (how often this symbol is used).
85    pub ref_count: usize,
86    /// Impl count (number of implementations for traits).
87    pub impl_count: usize,
88}
89
90impl DiscoveredSymbol {
91    /// Create a new discovered symbol.
92    pub fn new(id: SymbolId, path: SymbolPath, kind: SymbolKind) -> Self {
93        Self {
94            id,
95            uuid: None,
96            path,
97            kind,
98            span: None,
99            visibility: None,
100            score: 1.0,
101            ref_count: 0,
102            impl_count: 0,
103        }
104    }
105
106    /// Set the persistent UUID.
107    pub fn with_uuid(mut self, uuid: Uuid) -> Self {
108        self.uuid = Some(uuid);
109        self
110    }
111
112    /// Set the file span.
113    pub fn with_span(mut self, span: FileSpan) -> Self {
114        self.span = Some(span);
115        self
116    }
117
118    /// Set the visibility.
119    pub fn with_visibility(mut self, visibility: Visibility) -> Self {
120        self.visibility = Some(visibility);
121        self
122    }
123
124    /// Set the match score.
125    pub fn with_score(mut self, score: f32) -> Self {
126        self.score = score;
127        self
128    }
129
130    /// Set the reference count.
131    pub fn with_ref_count(mut self, ref_count: usize) -> Self {
132        self.ref_count = ref_count;
133        self
134    }
135
136    /// Set the impl count.
137    pub fn with_impl_count(mut self, impl_count: usize) -> Self {
138        self.impl_count = impl_count;
139        self
140    }
141
142    /// Check if this symbol is public.
143    pub fn is_public(&self) -> bool {
144        self.visibility.as_ref().is_some_and(|v| v.is_public())
145    }
146}
147
148/// Graph of symbol relations.
149#[derive(Debug, Clone, Default)]
150pub struct RelationGraph {
151    /// Relations keyed by source symbol ID.
152    relations: HashMap<SymbolId, Vec<Relation>>,
153}
154
155/// A relation between symbols.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Relation {
158    /// Target symbol ID.
159    pub target: SymbolId,
160    /// Kind of relation.
161    pub kind: RelationKind,
162}
163
164/// Kind of relation between symbols.
165///
166/// Three axes of symbol usage:
167/// - **Call**: `Calls` / `CalledBy` — function call relationships (from CodeGraphV2)
168/// - **Type**: `TypeReferences` / `TypeReferencedBy` — type usage relationships (from TypeFlowGraphV2)
169/// - **Trait**: `Implements` / `ImplementedBy` — trait implementation relationships (from CodeGraphV2)
170///
171/// Plus structural containment: `Contains` / `ContainedBy`.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
173pub enum RelationKind {
174    /// Symbol calls target (function call).
175    Calls,
176    /// Symbol is called by target.
177    CalledBy,
178    /// Symbol references target as a type (field type, param type, return type, etc.).
179    TypeReferences,
180    /// Symbol's type is referenced by target.
181    TypeReferencedBy,
182    /// Symbol implements target (trait).
183    Implements,
184    /// Symbol is implemented by target.
185    ImplementedBy,
186    /// Symbol contains target (parent-child).
187    Contains,
188    /// Symbol is contained by target.
189    ContainedBy,
190}
191
192impl RelationGraph {
193    /// Create a new empty relation graph.
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// Add a relation.
199    pub fn add(&mut self, source: SymbolId, target: SymbolId, kind: RelationKind) {
200        self.relations
201            .entry(source)
202            .or_default()
203            .push(Relation { target, kind });
204    }
205
206    /// Get relations for a symbol.
207    pub fn get(&self, id: SymbolId) -> &[Relation] {
208        self.relations.get(&id).map_or(&[], |v| v.as_slice())
209    }
210
211    /// Get relations of a specific kind.
212    pub fn get_by_kind(&self, id: SymbolId, kind: RelationKind) -> Vec<SymbolId> {
213        self.get(id)
214            .iter()
215            .filter(|r| r.kind == kind)
216            .map(|r| r.target)
217            .collect()
218    }
219
220    /// Check if there are any relations.
221    pub fn is_empty(&self) -> bool {
222        self.relations.is_empty()
223    }
224
225    /// Get the number of symbols with relations.
226    pub fn len(&self) -> usize {
227        self.relations.len()
228    }
229
230    /// Iterate over all relations.
231    pub fn iter(&self) -> impl Iterator<Item = (SymbolId, &[Relation])> {
232        self.relations
233            .iter()
234            .map(|(id, rels)| (*id, rels.as_slice()))
235    }
236
237    /// Get all source symbol IDs.
238    pub fn sources(&self) -> impl Iterator<Item = SymbolId> + '_ {
239        self.relations.keys().copied()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_discovery_result() {
249        let result = DiscoveryResult::new();
250        assert!(result.is_empty());
251
252        // We need a SymbolId, but for testing we can't easily create one
253        // So we'll just test the empty case
254        assert_eq!(result.len(), 0);
255    }
256
257    #[test]
258    fn test_relation_graph() {
259        use slotmap::SlotMap;
260
261        let mut slots: SlotMap<SymbolId, ()> = SlotMap::with_key();
262        let id1 = slots.insert(());
263        let id2 = slots.insert(());
264
265        let mut graph = RelationGraph::new();
266        graph.add(id1, id2, RelationKind::Calls);
267
268        assert!(!graph.is_empty());
269        assert_eq!(graph.get(id1).len(), 1);
270        assert_eq!(graph.get_by_kind(id1, RelationKind::Calls), vec![id2]);
271    }
272
273    #[test]
274    fn test_relation_kind() {
275        assert_ne!(RelationKind::Calls, RelationKind::CalledBy);
276    }
277}