project_rag/relations/
mod.rs

1//! Code relationships module for definition/reference tracking and call graphs.
2//!
3//! This module provides capabilities for understanding code relationships:
4//! - Find where symbols are defined
5//! - Find all references to a symbol
6//! - Build call graphs for functions/methods
7//!
8//! ## Architecture
9//!
10//! The module uses a hybrid approach:
11//! - **Stack-graphs** (optional feature): High-precision name resolution for Python,
12//!   TypeScript, Java, and Ruby (~95% accuracy)
13//! - **RepoMap**: AST-based extraction with heuristic matching for all languages
14//!   (~70% accuracy)
15//!
16//! ## Usage
17//!
18//! ```ignore
19//! use project_rag::relations::{HybridRelationsProvider, RelationsProvider};
20//!
21//! let provider = HybridRelationsProvider::new()?;
22//! let definitions = provider.extract_definitions(&file_info)?;
23//! let references = provider.extract_references(&file_info, &symbol_index)?;
24//! ```
25
26pub mod repomap;
27pub mod storage;
28pub mod types;
29
30#[cfg(feature = "stack-graphs")]
31pub mod stack_graphs;
32
33use anyhow::Result;
34
35pub use types::{
36    CallEdge, CallGraphNode, Definition, DefinitionResult, PrecisionLevel, Reference,
37    ReferenceKind, ReferenceResult, SymbolId, SymbolInfo, SymbolKind, Visibility,
38};
39
40use crate::indexer::FileInfo;
41use std::collections::HashMap;
42
43/// Trait for extracting code relationships from source files.
44///
45/// Implementors of this trait can extract symbol definitions and references
46/// from source code files.
47pub trait RelationsProvider: Send + Sync {
48    /// Extract definitions from a file.
49    ///
50    /// Returns a list of all symbol definitions (functions, classes, etc.)
51    /// found in the given file.
52    fn extract_definitions(&self, file_info: &FileInfo) -> Result<Vec<Definition>>;
53
54    /// Extract references from a file.
55    ///
56    /// `symbol_index` maps symbol names to their definitions, used for
57    /// resolving which symbol a reference points to.
58    fn extract_references(
59        &self,
60        file_info: &FileInfo,
61        symbol_index: &HashMap<String, Vec<Definition>>,
62    ) -> Result<Vec<Reference>>;
63
64    /// Check if this provider supports the given language.
65    fn supports_language(&self, language: &str) -> bool;
66
67    /// Get the precision level of this provider for the given language.
68    fn precision_level(&self, language: &str) -> PrecisionLevel;
69}
70
71/// Hybrid provider that selects the best available provider per language.
72///
73/// Uses stack-graphs for supported languages (Python, TypeScript, Java, Ruby)
74/// when the feature is enabled, and falls back to RepoMap for all other languages.
75pub struct HybridRelationsProvider {
76    /// Stack-graphs provider (if feature enabled)
77    #[cfg(feature = "stack-graphs")]
78    stack_graphs: Option<stack_graphs::StackGraphsProvider>,
79
80    /// RepoMap provider (always available)
81    repomap: repomap::RepoMapProvider,
82}
83
84impl HybridRelationsProvider {
85    /// Create a new hybrid relations provider.
86    ///
87    /// If `enable_stack_graphs` is true and the feature is enabled,
88    /// stack-graphs will be used for supported languages.
89    pub fn new(_enable_stack_graphs: bool) -> Result<Self> {
90        #[cfg(feature = "stack-graphs")]
91        let stack_graphs = if _enable_stack_graphs {
92            match stack_graphs::StackGraphsProvider::new() {
93                Ok(sg) => Some(sg),
94                Err(e) => {
95                    tracing::warn!("Failed to initialize stack-graphs: {}", e);
96                    None
97                }
98            }
99        } else {
100            None
101        };
102
103        Ok(Self {
104            #[cfg(feature = "stack-graphs")]
105            stack_graphs,
106            repomap: repomap::RepoMapProvider::new(),
107        })
108    }
109
110    /// Get the best provider for a given language.
111    fn provider_for_language(&self, _language: &str) -> &dyn RelationsProvider {
112        #[cfg(feature = "stack-graphs")]
113        if let Some(ref sg) = self.stack_graphs {
114            if sg.supports_language(_language) {
115                return sg;
116            }
117        }
118
119        &self.repomap
120    }
121
122    /// Check if stack-graphs is available for a language.
123    #[cfg(feature = "stack-graphs")]
124    pub fn has_stack_graphs_for(&self, language: &str) -> bool {
125        self.stack_graphs
126            .as_ref()
127            .is_some_and(|sg| sg.supports_language(language))
128    }
129
130    /// Check if stack-graphs is available for a language.
131    #[cfg(not(feature = "stack-graphs"))]
132    pub fn has_stack_graphs_for(&self, _language: &str) -> bool {
133        false
134    }
135}
136
137impl RelationsProvider for HybridRelationsProvider {
138    fn extract_definitions(&self, file_info: &FileInfo) -> Result<Vec<Definition>> {
139        let language = file_info.language.as_deref().unwrap_or("Unknown");
140        self.provider_for_language(language)
141            .extract_definitions(file_info)
142    }
143
144    fn extract_references(
145        &self,
146        file_info: &FileInfo,
147        symbol_index: &HashMap<String, Vec<Definition>>,
148    ) -> Result<Vec<Reference>> {
149        let language = file_info.language.as_deref().unwrap_or("Unknown");
150        self.provider_for_language(language)
151            .extract_references(file_info, symbol_index)
152    }
153
154    fn supports_language(&self, language: &str) -> bool {
155        // We support all languages through RepoMap fallback
156        self.repomap.supports_language(language)
157    }
158
159    fn precision_level(&self, language: &str) -> PrecisionLevel {
160        #[cfg(feature = "stack-graphs")]
161        if self.has_stack_graphs_for(language) {
162            return PrecisionLevel::High;
163        }
164
165        self.repomap.precision_level(language)
166    }
167}
168
169/// Configuration for relations extraction
170#[derive(Debug, Clone)]
171pub struct RelationsConfig {
172    /// Whether relations extraction is enabled
173    pub enabled: bool,
174    /// Whether to use stack-graphs when available
175    pub use_stack_graphs: bool,
176    /// Maximum call graph traversal depth
177    pub max_call_depth: usize,
178}
179
180impl Default for RelationsConfig {
181    fn default() -> Self {
182        Self {
183            enabled: true,
184            use_stack_graphs: cfg!(feature = "stack-graphs"),
185            max_call_depth: 3,
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_hybrid_provider_creation() {
196        let provider = HybridRelationsProvider::new(false).unwrap();
197        assert!(provider.supports_language("Rust"));
198        assert!(provider.supports_language("Python"));
199        assert!(provider.supports_language("Unknown"));
200    }
201
202    #[test]
203    fn test_precision_level_without_stack_graphs() {
204        let provider = HybridRelationsProvider::new(false).unwrap();
205        // Without stack-graphs, everything uses RepoMap (medium precision)
206        assert_eq!(provider.precision_level("Rust"), PrecisionLevel::Medium);
207        assert_eq!(provider.precision_level("Python"), PrecisionLevel::Medium);
208    }
209
210    #[test]
211    fn test_relations_config_default() {
212        let config = RelationsConfig::default();
213        assert!(config.enabled);
214        assert_eq!(config.max_call_depth, 3);
215    }
216
217    #[test]
218    fn test_has_stack_graphs() {
219        let provider = HybridRelationsProvider::new(false).unwrap();
220        // Without the feature, should always return false
221        #[cfg(not(feature = "stack-graphs"))]
222        assert!(!provider.has_stack_graphs_for("Python"));
223    }
224}