Skip to main content

ryo_analysis/discovery/
engine.rs

1//! DiscoveryEngine - Main symbol discovery interface.
2
3use super::{
4    DiscoveredSymbol, DiscoveryQuery, DiscoveryResult, RelationGraph, RelationKind, SortOrder,
5    TypeFilter,
6};
7use crate::query::{CodeEdgeV2, CodeGraphV2, DataFlowGraphV2, TypeFlowGraphV2, UsageContext};
8use crate::symbol::{SymbolId, SymbolRegistry};
9use crate::Pattern;
10use crate::SymbolKind;
11
12/// Main discovery engine for pattern-based symbol search.
13///
14/// # Example
15/// ```rust,ignore
16/// let engine = DiscoveryEngine::new(&code_graph, &registry, None, None);
17///
18/// let query = DiscoveryQuery::symbol("*Config")
19///     .kinds(vec![SymbolKind::Struct])
20///     .with_relations();
21///
22/// let result = engine.execute(&query)?;
23/// for symbol in result.iter() {
24///     println!("{}: {:?}", symbol.path, symbol.kind);
25/// }
26/// ```
27pub struct DiscoveryEngine<'a> {
28    graph: &'a CodeGraphV2,
29    registry: &'a SymbolRegistry,
30    _dataflow: Option<&'a DataFlowGraphV2>,
31    typeflow: Option<&'a TypeFlowGraphV2>,
32}
33
34impl<'a> DiscoveryEngine<'a> {
35    /// Create a new discovery engine.
36    pub fn new(
37        graph: &'a CodeGraphV2,
38        registry: &'a SymbolRegistry,
39        dataflow: Option<&'a DataFlowGraphV2>,
40    ) -> Self {
41        Self {
42            graph,
43            registry,
44            _dataflow: dataflow,
45            typeflow: None,
46        }
47    }
48
49    /// Add TypeFlowGraphV2 to the engine.
50    pub fn set_typeflow(mut self, typeflow: &'a TypeFlowGraphV2) -> Self {
51        self.typeflow = Some(typeflow);
52        self
53    }
54
55    /// Execute a discovery query.
56    pub fn execute(&self, query: &DiscoveryQuery) -> DiscoveryResult {
57        let mut result = DiscoveryResult::new();
58        let mut matches = Vec::new();
59
60        // Find matching symbols
61        for (id, path) in self.registry.iter() {
62            // Check pattern match
63            if !query.pattern.matches(path.name()) {
64                continue;
65            }
66
67            // Check crate filter
68            if let Some(ref crate_name) = query.in_crate {
69                if path.crate_name() != crate_name {
70                    continue;
71                }
72            }
73
74            // Check module filter
75            if let Some(ref module) = query.in_module {
76                if !path.to_string().contains(module) {
77                    continue;
78                }
79            }
80
81            // Check kind filter
82            if let Some(ref kinds) = query.kinds {
83                if let Some(kind) = self.registry.kind(id) {
84                    if !kinds.iter().any(|k| k.matches(&kind)) {
85                        continue;
86                    }
87                } else {
88                    continue;
89                }
90            }
91
92            // Symbol matches!
93            let kind = self.registry.kind(id).unwrap_or(SymbolKind::Other);
94            let mut symbol = DiscoveredSymbol::new(id, path.clone(), kind);
95
96            // Add persistent UUID if available
97            if let Some(uuid) = self.registry.uuid(id) {
98                symbol = symbol.with_uuid(uuid);
99            }
100
101            // Add metadata if available
102            if let Some(span) = self.registry.span(id) {
103                symbol = symbol.with_span(span.clone());
104            }
105            if let Some(vis) = self.registry.visibility(id) {
106                symbol = symbol.with_visibility(vis.clone());
107            }
108
109            // Calculate match score and reference count
110            let score = self.calculate_score(path.name(), &query.pattern);
111            let ref_count = self.graph.reference_count(id);
112            let impl_count = self.graph.impl_count(id);
113            symbol = symbol
114                .with_score(score)
115                .with_ref_count(ref_count)
116                .with_impl_count(impl_count);
117
118            matches.push(symbol);
119        }
120
121        // Apply type filter if specified and TypeFlowGraph is available
122        if let (Some(ref type_filter), Some(typeflow)) = (&query.type_filter, self.typeflow) {
123            if !type_filter.is_empty() {
124                matches = self.apply_type_filter(matches, type_filter, typeflow);
125            }
126        }
127
128        // Sort based on query sort order
129        match query.sort {
130            SortOrder::Refs => {
131                // Sort by reference count (descending), then by score
132                matches.sort_by(|a, b| {
133                    b.ref_count.cmp(&a.ref_count).then_with(|| {
134                        b.score
135                            .partial_cmp(&a.score)
136                            .unwrap_or(std::cmp::Ordering::Equal)
137                    })
138                });
139            }
140            SortOrder::Alpha => {
141                // Alphabetical order
142                matches.sort_by(|a, b| a.path.name().cmp(b.path.name()));
143            }
144            SortOrder::Impls => {
145                // Sort by impl count (descending)
146                matches.sort_by(|a, b| {
147                    b.impl_count
148                        .cmp(&a.impl_count)
149                        .then_with(|| b.ref_count.cmp(&a.ref_count))
150                });
151            }
152        }
153
154        // Apply limit
155        result.total_matches = matches.len();
156        if let Some(limit) = query.limit {
157            if matches.len() > limit {
158                result.truncated = true;
159                matches.truncate(limit);
160            }
161        }
162
163        result.symbols = matches;
164
165        // Build relation graph if requested
166        if query.include_relations && !result.is_empty() {
167            result.relations = Some(self.build_relations(&result, query.relation_depth));
168        }
169
170        result
171    }
172
173    /// Calculate match score for ranking.
174    fn calculate_score(&self, name: &str, pattern: &crate::Pattern) -> f32 {
175        let pattern_str = pattern.as_str();
176
177        // Exact match gets highest score
178        if name == pattern_str {
179            return 1.0;
180        }
181
182        // Prefix match (pattern starts with name or vice versa)
183        if pattern.is_exact() {
184            return 0.0; // Should have matched exactly above
185        }
186
187        // For glob patterns, favor shorter names and prefix matches
188        let base_score = 0.5;
189
190        // Bonus for shorter names
191        let length_bonus = 1.0 / (name.len() as f32 + 1.0) * 0.2;
192
193        // Bonus for prefix match
194        let prefix_bonus = if pattern_str.starts_with('*') {
195            // Suffix pattern like "*Config" - favor exact suffix
196            let suffix = pattern_str.trim_start_matches('*');
197            if name.ends_with(suffix) {
198                0.2
199            } else {
200                0.0
201            }
202        } else if pattern_str.ends_with('*') {
203            // Prefix pattern like "get_*"
204            let prefix = pattern_str.trim_end_matches('*');
205            if name.starts_with(prefix) {
206                0.2
207            } else {
208                0.0
209            }
210        } else {
211            0.0
212        };
213
214        base_score + length_bonus + prefix_bonus
215    }
216
217    /// Build relation graph for discovered symbols.
218    fn build_relations(&self, result: &DiscoveryResult, max_depth: usize) -> RelationGraph {
219        let mut graph = RelationGraph::new();
220
221        for symbol in result.iter() {
222            self.add_relations(&mut graph, symbol.id, max_depth, 0);
223        }
224
225        graph
226    }
227
228    /// Recursively add relations for a symbol.
229    fn add_relations(
230        &self,
231        graph: &mut RelationGraph,
232        id: crate::symbol::SymbolId,
233        max_depth: usize,
234        current_depth: usize,
235    ) {
236        if current_depth >= max_depth {
237            return;
238        }
239
240        // Check if symbol exists in graph
241        if !self.graph.contains(id) {
242            return;
243        }
244
245        // Add outgoing relations
246        for edge_data in self.graph.outgoing_edges(id) {
247            let target_id = edge_data.to;
248            let kind = match edge_data.kind {
249                CodeEdgeV2::Calls => RelationKind::Calls,
250                CodeEdgeV2::Implements => RelationKind::Implements,
251                CodeEdgeV2::Contains => RelationKind::Contains,
252            };
253            graph.add(id, target_id, kind);
254        }
255
256        // Add incoming relations
257        for edge_data in self.graph.incoming_edges(id) {
258            let source_id = edge_data.from;
259            let kind = match edge_data.kind {
260                CodeEdgeV2::Calls => RelationKind::CalledBy,
261                CodeEdgeV2::Implements => RelationKind::ImplementedBy,
262                CodeEdgeV2::Contains => RelationKind::ContainedBy,
263            };
264            graph.add(id, source_id, kind);
265        }
266
267        // Add type reference relations from TypeFlow
268        if let Some(typeflow) = self.typeflow {
269            // Forward: what types does this symbol reference?
270            for target_id in typeflow.types_used_by(id) {
271                graph.add(id, target_id, RelationKind::TypeReferences);
272            }
273            // Reverse: who references this symbol as a type?
274            for user_id in typeflow.type_users(id) {
275                graph.add(id, user_id, RelationKind::TypeReferencedBy);
276            }
277        }
278    }
279
280    /// Find symbols by exact name.
281    pub fn find_exact(&self, name: &str) -> DiscoveryResult {
282        self.execute(&DiscoveryQuery::exact(name))
283    }
284
285    /// Find symbols matching a glob pattern.
286    pub fn find_glob(&self, pattern: &str) -> DiscoveryResult {
287        self.execute(&DiscoveryQuery::symbol(pattern))
288    }
289
290    /// Find all implementations of a trait.
291    pub fn find_implementations(
292        &self,
293        trait_id: crate::symbol::SymbolId,
294    ) -> Vec<crate::symbol::SymbolId> {
295        self.graph.implementors_of(trait_id).collect()
296    }
297
298    /// Find all callers of a function.
299    pub fn find_callers(&self, func_id: crate::symbol::SymbolId) -> Vec<crate::symbol::SymbolId> {
300        self.graph.callers_of(func_id).collect()
301    }
302
303    /// Find all callees of a function.
304    pub fn find_callees(&self, func_id: crate::symbol::SymbolId) -> Vec<crate::symbol::SymbolId> {
305        self.graph.callees_of(func_id).collect()
306    }
307
308    // =========================================================================
309    // TypeFlow filtering (V2 API)
310    // =========================================================================
311
312    /// Apply type filter to matched symbols using TypeFlowGraphV2.
313    fn apply_type_filter(
314        &self,
315        symbols: Vec<DiscoveredSymbol>,
316        filter: &TypeFilter,
317        typeflow: &TypeFlowGraphV2,
318    ) -> Vec<DiscoveredSymbol> {
319        symbols
320            .into_iter()
321            .filter(|symbol| self.matches_type_filter(symbol.id, filter, typeflow))
322            .collect()
323    }
324
325    /// Check if a symbol matches the type filter.
326    fn matches_type_filter(
327        &self,
328        id: SymbolId,
329        filter: &TypeFilter,
330        typeflow: &TypeFlowGraphV2,
331    ) -> bool {
332        // Check return type filter
333        if let Some(ref pattern) = filter.return_type {
334            if !self.has_matching_usage_v2(id, UsageContext::ReturnType, pattern, typeflow) {
335                return false;
336            }
337        }
338
339        // Check parameter type filter
340        if let Some(ref pattern) = filter.param_type {
341            if !self.has_matching_usage_v2(id, UsageContext::ParamType, pattern, typeflow) {
342                return false;
343            }
344        }
345
346        // Check field type filter (structs/enums)
347        if let Some(ref pattern) = filter.field_type {
348            let has_field =
349                self.has_matching_usage_v2(id, UsageContext::FieldType, pattern, typeflow)
350                    || self.has_matching_usage_v2(
351                        id,
352                        UsageContext::VariantField,
353                        pattern,
354                        typeflow,
355                    );
356            if !has_field {
357                return false;
358            }
359        }
360
361        // Check uses_type filter (any context)
362        if let Some(ref pattern) = filter.uses_type {
363            if !self.has_any_matching_usage_v2(id, pattern, typeflow) {
364                return false;
365            }
366        }
367
368        // Check has_bound filter (trait bounds)
369        if let Some(ref pattern) = filter.has_bound {
370            if !self.has_matching_bound_v2(id, pattern, typeflow) {
371                return false;
372            }
373        }
374
375        true
376    }
377
378    /// Check if there's a usage in a specific context matching the pattern (V2).
379    fn has_matching_usage_v2(
380        &self,
381        id: SymbolId,
382        expected_context: UsageContext,
383        pattern: &Pattern,
384        typeflow: &TypeFlowGraphV2,
385    ) -> bool {
386        for usage_id in typeflow.usages(id) {
387            if let Some(usage_data) = typeflow.get_usage(usage_id) {
388                if usage_data.context != expected_context {
389                    continue;
390                }
391                // V2 is String-free: resolve SymbolId to name via registry
392                if let Some(resolved_id) = usage_data.resolved {
393                    if let Some(path) = self.registry.resolve(resolved_id) {
394                        if pattern.matches(path.name()) {
395                            return true;
396                        }
397                    }
398                }
399            }
400        }
401        false
402    }
403
404    /// Check if there's any usage matching the pattern (any context) (V2).
405    fn has_any_matching_usage_v2(
406        &self,
407        id: SymbolId,
408        pattern: &Pattern,
409        typeflow: &TypeFlowGraphV2,
410    ) -> bool {
411        for usage_id in typeflow.usages(id) {
412            if let Some(usage_data) = typeflow.get_usage(usage_id) {
413                // V2 is String-free: resolve SymbolId to name via registry
414                if let Some(resolved_id) = usage_data.resolved {
415                    if let Some(path) = self.registry.resolve(resolved_id) {
416                        if pattern.matches(path.name()) {
417                            return true;
418                        }
419                    }
420                }
421            }
422        }
423        false
424    }
425
426    /// Check if the symbol has trait bounds matching the pattern (V2).
427    fn has_matching_bound_v2(
428        &self,
429        id: SymbolId,
430        pattern: &Pattern,
431        typeflow: &TypeFlowGraphV2,
432    ) -> bool {
433        // Get the definition node for this symbol
434        let Some(def_node) = typeflow.definition(id) else {
435            return false;
436        };
437
438        // Check trait bounds
439        for bound_id in typeflow.trait_bounds(def_node) {
440            if let Some(bound_data) = typeflow.get_trait_bound(bound_id) {
441                // V2 is String-free: resolve SymbolId to name via registry
442                if let Some(trait_id) = bound_data.trait_id {
443                    if let Some(path) = self.registry.resolve(trait_id) {
444                        if pattern.matches(path.name()) {
445                            return true;
446                        }
447                    }
448                }
449            }
450        }
451
452        false
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::symbol::SymbolPath;
460    use crate::SymbolKind;
461
462    fn setup() -> (SymbolRegistry, CodeGraphV2) {
463        let mut registry = SymbolRegistry::new();
464
465        registry
466            .register(
467                SymbolPath::parse("mylib::AppConfig").unwrap(),
468                SymbolKind::Struct,
469            )
470            .unwrap();
471        registry
472            .register(
473                SymbolPath::parse("mylib::UserConfig").unwrap(),
474                SymbolKind::Struct,
475            )
476            .unwrap();
477        registry
478            .register(
479                SymbolPath::parse("mylib::handlers::handle").unwrap(),
480                SymbolKind::Function,
481            )
482            .unwrap();
483        registry
484            .register(
485                SymbolPath::parse("other::Config").unwrap(),
486                SymbolKind::Struct,
487            )
488            .unwrap();
489
490        let graph = CodeGraphV2::new();
491
492        (registry, graph)
493    }
494
495    #[test]
496    fn test_find_by_pattern() {
497        let (registry, graph) = setup();
498        let engine = DiscoveryEngine::new(&graph, &registry, None);
499
500        let result = engine.find_glob("*Config");
501        assert_eq!(result.len(), 3); // AppConfig, UserConfig, Config
502    }
503
504    #[test]
505    fn test_find_with_crate_filter() {
506        let (registry, graph) = setup();
507        let engine = DiscoveryEngine::new(&graph, &registry, None);
508
509        let query = DiscoveryQuery::symbol("*Config").in_crate("mylib");
510        let result = engine.execute(&query);
511        assert_eq!(result.len(), 2); // AppConfig, UserConfig
512    }
513
514    #[test]
515    fn test_find_with_kind_filter() {
516        let (registry, graph) = setup();
517        let engine = DiscoveryEngine::new(&graph, &registry, None);
518
519        let query = DiscoveryQuery::symbol("*").kind(SymbolKind::Function);
520        let result = engine.execute(&query);
521        assert_eq!(result.len(), 1); // handle
522    }
523
524    #[test]
525    fn test_find_with_module_filter() {
526        let (registry, graph) = setup();
527        let engine = DiscoveryEngine::new(&graph, &registry, None);
528
529        let query = DiscoveryQuery::symbol("*").in_module("handlers");
530        let result = engine.execute(&query);
531        assert_eq!(result.len(), 1); // handle
532    }
533
534    #[test]
535    fn test_find_exact() {
536        let (registry, graph) = setup();
537        let engine = DiscoveryEngine::new(&graph, &registry, None);
538
539        let result = engine.find_exact("AppConfig");
540        assert_eq!(result.len(), 1);
541        assert_eq!(result.first().unwrap().path.name(), "AppConfig");
542    }
543
544    #[test]
545    fn test_find_with_limit() {
546        let (registry, graph) = setup();
547        let engine = DiscoveryEngine::new(&graph, &registry, None);
548
549        let query = DiscoveryQuery::symbol("*").limit(2);
550        let result = engine.execute(&query);
551        assert_eq!(result.len(), 2);
552        assert!(result.truncated);
553        assert_eq!(result.total_matches, 4);
554    }
555}