project_rag/relations/
types.rs

1//! Type definitions for code relationships (definitions, references, call graphs).
2//!
3//! This module provides the core data structures for representing code relationships:
4//! - `SymbolId`: Unique identifier for a symbol in the codebase
5//! - `Definition`: A symbol definition (function, class, method, etc.)
6//! - `Reference`: A reference to a symbol
7//! - `CallEdge`: An edge in the call graph
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::hash::{Hash, Hasher};
12
13/// Kind of symbol in the codebase
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "snake_case")]
16pub enum SymbolKind {
17    /// A function (standalone)
18    Function,
19    /// A method (belongs to a class/struct/impl)
20    Method,
21    /// A class definition
22    Class,
23    /// A struct definition
24    Struct,
25    /// An interface definition
26    Interface,
27    /// A trait definition (Rust)
28    Trait,
29    /// An enum definition
30    Enum,
31    /// A module/namespace
32    Module,
33    /// A variable/binding
34    Variable,
35    /// A constant
36    Constant,
37    /// A function/method parameter
38    Parameter,
39    /// A class/struct field
40    Field,
41    /// An import statement
42    Import,
43    /// An export statement
44    Export,
45    /// An enum variant
46    EnumVariant,
47    /// A type alias
48    TypeAlias,
49    /// Unknown or unclassified symbol
50    Unknown,
51}
52
53impl SymbolKind {
54    /// Convert from AST node kind string to SymbolKind.
55    ///
56    /// This consolidates AST node kinds from all supported languages into
57    /// a single mapping to avoid duplicates.
58    pub fn from_ast_kind(kind: &str) -> Self {
59        match kind {
60            // Functions (various languages)
61            "function_item" // Rust
62            | "function_definition" // Python, C, PHP
63            | "function_declaration" // JS/TS, Go, Swift
64            | "function_expression" // JS/TS
65            | "arrow_function" // JS/TS
66            | "decorated_definition" // Python (could be either, default to function)
67            => Self::Function,
68
69            // Methods
70            "method_definition" // JS/TS
71            | "method_declaration" // Java, Go, PHP
72            | "method" // Ruby
73            | "singleton_method" // Ruby
74            | "constructor_declaration" // Java
75            => Self::Method,
76
77            // Classes
78            "impl_item" // Rust (impl blocks treated as class-like)
79            | "class_definition" // Python
80            | "class_declaration" // JS/TS, Java, PHP, Swift
81            | "class_specifier" // C++
82            | "class" // Ruby
83            => Self::Class,
84
85            // Structs
86            "struct_item" // Rust
87            | "struct_specifier" // C/C++
88            | "struct_declaration" // Swift, C#
89            => Self::Struct,
90
91            // Interfaces/Protocols
92            "interface_declaration" // JS/TS, Java, PHP, C#
93            | "protocol_declaration" // Swift
94            => Self::Interface,
95
96            // Traits
97            "trait_item" // Rust
98            | "trait_declaration" // PHP
99            => Self::Trait,
100
101            // Enums
102            "enum_item" // Rust
103            | "enum_declaration" // JS/TS, Java, Swift, C#
104            | "enum_specifier" // C/C++
105            => Self::Enum,
106
107            // Modules/Namespaces
108            "mod_item" // Rust
109            | "module" // Ruby
110            | "namespace_definition" // C++, PHP
111            | "namespace_declaration" // C#
112            => Self::Module,
113
114            // Variables
115            "static_item" // Rust
116            | "variable_declaration" // JS/TS
117            | "lexical_declaration" // JS/TS
118            => Self::Variable,
119
120            // Constants
121            "const_item" // Rust
122            => Self::Constant,
123
124            // Type aliases
125            "type_item" // Rust
126            | "type_alias_declaration" // JS/TS
127            | "type_declaration" // Go
128            => Self::TypeAlias,
129
130            _ => Self::Unknown,
131        }
132    }
133
134    /// Get a human-readable display name for this kind
135    pub fn display_name(&self) -> &'static str {
136        match self {
137            Self::Function => "function",
138            Self::Method => "method",
139            Self::Class => "class",
140            Self::Struct => "struct",
141            Self::Interface => "interface",
142            Self::Trait => "trait",
143            Self::Enum => "enum",
144            Self::Module => "module",
145            Self::Variable => "variable",
146            Self::Constant => "constant",
147            Self::Parameter => "parameter",
148            Self::Field => "field",
149            Self::Import => "import",
150            Self::Export => "export",
151            Self::EnumVariant => "enum variant",
152            Self::TypeAlias => "type alias",
153            Self::Unknown => "unknown",
154        }
155    }
156}
157
158/// Visibility/access modifier for a symbol
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Default)]
160#[serde(rename_all = "snake_case")]
161pub enum Visibility {
162    /// Public - accessible from anywhere
163    Public,
164    /// Private - accessible only within the same scope
165    #[default]
166    Private,
167    /// Protected - accessible within class hierarchy
168    Protected,
169    /// Internal/package-private
170    Internal,
171}
172
173impl Visibility {
174    /// Parse visibility from source code keywords
175    pub fn from_keywords(text: &str) -> Self {
176        let lower = text.to_lowercase();
177        if lower.contains("pub ") || lower.contains("public ") || lower.contains("export ") {
178            Self::Public
179        } else if lower.contains("protected ") {
180            Self::Protected
181        } else if lower.contains("internal ") || lower.contains("package ") {
182            Self::Internal
183        } else {
184            Self::Private
185        }
186    }
187}
188
189/// Kind of reference to a symbol
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
191#[serde(rename_all = "snake_case")]
192pub enum ReferenceKind {
193    /// Function or method call
194    Call,
195    /// Variable read access
196    Read,
197    /// Variable write/assignment
198    Write,
199    /// Import statement
200    Import,
201    /// Type annotation or type reference
202    TypeReference,
203    /// Class inheritance (extends/implements)
204    Inheritance,
205    /// Instantiation (new Foo())
206    Instantiation,
207    /// Unknown reference type
208    Unknown,
209}
210
211/// A unique identifier for a symbol in the codebase.
212///
213/// Symbols are identified by their file path, name, kind, and position.
214/// This allows distinguishing between symbols with the same name in different files
215/// or different positions within the same file.
216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
217pub struct SymbolId {
218    /// Relative file path from the project root
219    pub file_path: String,
220    /// Symbol name (e.g., function name, class name)
221    pub name: String,
222    /// Kind of symbol
223    pub kind: SymbolKind,
224    /// Starting line number (1-based)
225    pub start_line: usize,
226    /// Starting column (0-based)
227    pub start_col: usize,
228}
229
230impl SymbolId {
231    /// Create a new SymbolId
232    pub fn new(
233        file_path: impl Into<String>,
234        name: impl Into<String>,
235        kind: SymbolKind,
236        start_line: usize,
237        start_col: usize,
238    ) -> Self {
239        Self {
240            file_path: file_path.into(),
241            name: name.into(),
242            kind,
243            start_line,
244            start_col,
245        }
246    }
247
248    /// Generate a unique string ID for storage
249    pub fn to_storage_id(&self) -> String {
250        format!(
251            "{}:{}:{}:{}",
252            self.file_path, self.name, self.start_line, self.start_col
253        )
254    }
255
256    /// Parse from a storage ID string
257    pub fn from_storage_id(id: &str) -> Option<Self> {
258        let parts: Vec<&str> = id.rsplitn(4, ':').collect();
259        if parts.len() != 4 {
260            return None;
261        }
262        // rsplitn gives parts in reverse order
263        let start_col = parts[0].parse().ok()?;
264        let start_line = parts[1].parse().ok()?;
265        let name = parts[2].to_string();
266        let file_path = parts[3].to_string();
267
268        Some(Self {
269            file_path,
270            name,
271            kind: SymbolKind::Unknown, // Kind not stored in ID
272            start_line,
273            start_col,
274        })
275    }
276}
277
278impl PartialEq for SymbolId {
279    fn eq(&self, other: &Self) -> bool {
280        self.file_path == other.file_path
281            && self.name == other.name
282            && self.start_line == other.start_line
283            && self.start_col == other.start_col
284    }
285}
286
287impl Eq for SymbolId {}
288
289impl Hash for SymbolId {
290    fn hash<H: Hasher>(&self, state: &mut H) {
291        self.file_path.hash(state);
292        self.name.hash(state);
293        self.start_line.hash(state);
294        self.start_col.hash(state);
295    }
296}
297
298/// A definition of a symbol in the codebase.
299///
300/// Contains full information about where a symbol is defined,
301/// its signature, documentation, and relationships.
302#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
303pub struct Definition {
304    /// Unique identifier for this symbol
305    pub symbol_id: SymbolId,
306    /// Absolute root path of the indexed codebase
307    pub root_path: Option<String>,
308    /// Project name (for multi-project support)
309    pub project: Option<String>,
310    /// Ending line number (1-based)
311    pub end_line: usize,
312    /// Ending column (0-based)
313    pub end_col: usize,
314    /// Full signature or declaration text
315    pub signature: String,
316    /// Documentation comment if available
317    pub doc_comment: Option<String>,
318    /// Visibility modifier
319    pub visibility: Visibility,
320    /// Parent symbol ID (e.g., containing class for a method)
321    pub parent_id: Option<String>,
322    /// Timestamp when this definition was indexed
323    pub indexed_at: i64,
324}
325
326impl Definition {
327    /// Generate a unique storage ID for this definition
328    pub fn to_storage_id(&self) -> String {
329        format!(
330            "def:{}:{}:{}",
331            self.symbol_id.file_path, self.symbol_id.name, self.symbol_id.start_line
332        )
333    }
334
335    /// Get the file path
336    pub fn file_path(&self) -> &str {
337        &self.symbol_id.file_path
338    }
339
340    /// Get the symbol name
341    pub fn name(&self) -> &str {
342        &self.symbol_id.name
343    }
344
345    /// Get the symbol kind
346    pub fn kind(&self) -> SymbolKind {
347        self.symbol_id.kind
348    }
349
350    /// Get the start line
351    pub fn start_line(&self) -> usize {
352        self.symbol_id.start_line
353    }
354}
355
356/// A reference to a symbol from another location in the codebase.
357#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
358pub struct Reference {
359    /// File path where the reference occurs
360    pub file_path: String,
361    /// Absolute root path of the indexed codebase
362    pub root_path: Option<String>,
363    /// Project name
364    pub project: Option<String>,
365    /// Starting line number (1-based)
366    pub start_line: usize,
367    /// Ending line number (1-based)
368    pub end_line: usize,
369    /// Starting column (0-based)
370    pub start_col: usize,
371    /// Ending column (0-based)
372    pub end_col: usize,
373    /// Storage ID of the target symbol being referenced
374    pub target_symbol_id: String,
375    /// Kind of reference
376    pub reference_kind: ReferenceKind,
377    /// Timestamp when this reference was indexed
378    pub indexed_at: i64,
379}
380
381impl Reference {
382    /// Generate a unique storage ID for this reference
383    pub fn to_storage_id(&self) -> String {
384        format!(
385            "ref:{}:{}:{}",
386            self.file_path, self.start_line, self.start_col
387        )
388    }
389}
390
391/// An edge in the call graph representing a function/method call.
392#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
393pub struct CallEdge {
394    /// The symbol making the call (caller)
395    pub caller_id: String,
396    /// The symbol being called (callee)
397    pub callee_id: String,
398    /// File where the call occurs
399    pub call_site_file: String,
400    /// Line where the call occurs
401    pub call_site_line: usize,
402    /// Column where the call occurs
403    pub call_site_col: usize,
404}
405
406/// Precision level of the relations provider
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
408#[serde(rename_all = "snake_case")]
409pub enum PrecisionLevel {
410    /// High precision: stack-graphs with full name resolution (~95% accuracy)
411    High,
412    /// Medium precision: AST-based with heuristic matching (~70% accuracy)
413    Medium,
414    /// Low precision: text-based pattern matching (~50% accuracy)
415    Low,
416}
417
418impl PrecisionLevel {
419    /// Get a human-readable description
420    pub fn description(&self) -> &'static str {
421        match self {
422            Self::High => "high (stack-graphs)",
423            Self::Medium => "medium (AST-based)",
424            Self::Low => "low (text-based)",
425        }
426    }
427}
428
429// ============================================================================
430// Result types for MCP tools
431// ============================================================================
432
433/// Result from find_definition containing the found definition
434#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
435pub struct DefinitionResult {
436    /// File path where the definition is located
437    pub file_path: String,
438    /// Symbol name
439    pub name: String,
440    /// Symbol kind
441    pub kind: SymbolKind,
442    /// Starting line (1-based)
443    pub start_line: usize,
444    /// Ending line (1-based)
445    pub end_line: usize,
446    /// Starting column (0-based)
447    pub start_col: usize,
448    /// Ending column (0-based)
449    pub end_col: usize,
450    /// Full signature or declaration
451    pub signature: String,
452    /// Documentation comment
453    pub doc_comment: Option<String>,
454}
455
456impl From<&Definition> for DefinitionResult {
457    fn from(def: &Definition) -> Self {
458        Self {
459            file_path: def.symbol_id.file_path.clone(),
460            name: def.symbol_id.name.clone(),
461            kind: def.symbol_id.kind,
462            start_line: def.symbol_id.start_line,
463            end_line: def.end_line,
464            start_col: def.symbol_id.start_col,
465            end_col: def.end_col,
466            signature: def.signature.clone(),
467            doc_comment: def.doc_comment.clone(),
468        }
469    }
470}
471
472/// Result from find_references containing a found reference
473#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
474pub struct ReferenceResult {
475    /// File path where the reference occurs
476    pub file_path: String,
477    /// Starting line (1-based)
478    pub start_line: usize,
479    /// Ending line (1-based)
480    pub end_line: usize,
481    /// Starting column (0-based)
482    pub start_col: usize,
483    /// Ending column (0-based)
484    pub end_col: usize,
485    /// Kind of reference
486    pub reference_kind: ReferenceKind,
487    /// Preview of the line containing the reference
488    pub preview: Option<String>,
489}
490
491impl From<&Reference> for ReferenceResult {
492    fn from(r: &Reference) -> Self {
493        Self {
494            file_path: r.file_path.clone(),
495            start_line: r.start_line,
496            end_line: r.end_line,
497            start_col: r.start_col,
498            end_col: r.end_col,
499            reference_kind: r.reference_kind,
500            preview: None,
501        }
502    }
503}
504
505/// A node in the call graph
506#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
507pub struct CallGraphNode {
508    /// Symbol name
509    pub name: String,
510    /// Symbol kind
511    pub kind: SymbolKind,
512    /// File path
513    pub file_path: String,
514    /// Line number
515    pub line: usize,
516    /// Nested callers/callees (for depth > 1)
517    pub children: Vec<CallGraphNode>,
518}
519
520/// Symbol info for call graph root
521#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
522pub struct SymbolInfo {
523    /// Symbol name
524    pub name: String,
525    /// Symbol kind
526    pub kind: SymbolKind,
527    /// File path
528    pub file_path: String,
529    /// Starting line
530    pub start_line: usize,
531    /// Ending line
532    pub end_line: usize,
533    /// Signature
534    pub signature: String,
535}
536
537// ============================================================================
538// Tests
539// ============================================================================
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_symbol_kind_from_ast_kind() {
547        assert_eq!(SymbolKind::from_ast_kind("function_item"), SymbolKind::Function);
548        assert_eq!(SymbolKind::from_ast_kind("class_definition"), SymbolKind::Class);
549        assert_eq!(SymbolKind::from_ast_kind("method_definition"), SymbolKind::Method);
550        assert_eq!(SymbolKind::from_ast_kind("unknown_node"), SymbolKind::Unknown);
551    }
552
553    #[test]
554    fn test_symbol_kind_display_name() {
555        assert_eq!(SymbolKind::Function.display_name(), "function");
556        assert_eq!(SymbolKind::Class.display_name(), "class");
557        assert_eq!(SymbolKind::Unknown.display_name(), "unknown");
558    }
559
560    #[test]
561    fn test_visibility_from_keywords() {
562        assert_eq!(Visibility::from_keywords("pub fn foo"), Visibility::Public);
563        assert_eq!(Visibility::from_keywords("public void bar"), Visibility::Public);
564        assert_eq!(Visibility::from_keywords("protected int x"), Visibility::Protected);
565        assert_eq!(Visibility::from_keywords("fn private_func"), Visibility::Private);
566    }
567
568    #[test]
569    fn test_symbol_id_equality() {
570        let id1 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
571        let id2 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
572        let id3 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 20, 0);
573
574        assert_eq!(id1, id2);
575        assert_ne!(id1, id3);
576    }
577
578    #[test]
579    fn test_symbol_id_hash() {
580        use std::collections::HashSet;
581
582        let id1 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
583        let id2 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
584
585        let mut set = HashSet::new();
586        set.insert(id1);
587        assert!(set.contains(&id2));
588    }
589
590    #[test]
591    fn test_symbol_id_storage_id() {
592        let id = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 5);
593        let storage_id = id.to_storage_id();
594        assert_eq!(storage_id, "src/main.rs:foo:10:5");
595    }
596
597    #[test]
598    fn test_definition_storage_id() {
599        let def = Definition {
600            symbol_id: SymbolId::new("src/lib.rs", "MyClass", SymbolKind::Class, 15, 0),
601            root_path: Some("/project".to_string()),
602            project: Some("test".to_string()),
603            end_line: 50,
604            end_col: 1,
605            signature: "class MyClass".to_string(),
606            doc_comment: None,
607            visibility: Visibility::Public,
608            parent_id: None,
609            indexed_at: 12345,
610        };
611
612        assert_eq!(def.to_storage_id(), "def:src/lib.rs:MyClass:15");
613        assert_eq!(def.file_path(), "src/lib.rs");
614        assert_eq!(def.name(), "MyClass");
615        assert_eq!(def.kind(), SymbolKind::Class);
616    }
617
618    #[test]
619    fn test_reference_storage_id() {
620        let reference = Reference {
621            file_path: "src/consumer.rs".to_string(),
622            root_path: None,
623            project: None,
624            start_line: 25,
625            end_line: 25,
626            start_col: 10,
627            end_col: 20,
628            target_symbol_id: "def:src/lib.rs:foo:10".to_string(),
629            reference_kind: ReferenceKind::Call,
630            indexed_at: 12345,
631        };
632
633        assert_eq!(reference.to_storage_id(), "ref:src/consumer.rs:25:10");
634    }
635
636    #[test]
637    fn test_precision_level_description() {
638        assert_eq!(PrecisionLevel::High.description(), "high (stack-graphs)");
639        assert_eq!(PrecisionLevel::Medium.description(), "medium (AST-based)");
640        assert_eq!(PrecisionLevel::Low.description(), "low (text-based)");
641    }
642
643    #[test]
644    fn test_definition_result_from_definition() {
645        let def = Definition {
646            symbol_id: SymbolId::new("src/lib.rs", "my_func", SymbolKind::Function, 10, 0),
647            root_path: None,
648            project: None,
649            end_line: 20,
650            end_col: 1,
651            signature: "fn my_func()".to_string(),
652            doc_comment: Some("Does stuff".to_string()),
653            visibility: Visibility::Public,
654            parent_id: None,
655            indexed_at: 0,
656        };
657
658        let result = DefinitionResult::from(&def);
659        assert_eq!(result.file_path, "src/lib.rs");
660        assert_eq!(result.name, "my_func");
661        assert_eq!(result.kind, SymbolKind::Function);
662        assert_eq!(result.start_line, 10);
663        assert_eq!(result.end_line, 20);
664        assert_eq!(result.doc_comment, Some("Does stuff".to_string()));
665    }
666
667    #[test]
668    fn test_serialization() {
669        let id = SymbolId::new("src/main.rs", "test", SymbolKind::Function, 1, 0);
670        let json = serde_json::to_string(&id).unwrap();
671        let deserialized: SymbolId = serde_json::from_str(&json).unwrap();
672        assert_eq!(id, deserialized);
673    }
674
675    #[test]
676    fn test_reference_kind_serialization() {
677        let kind = ReferenceKind::Call;
678        let json = serde_json::to_string(&kind).unwrap();
679        assert_eq!(json, "\"call\"");
680
681        let deserialized: ReferenceKind = serde_json::from_str(&json).unwrap();
682        assert_eq!(deserialized, ReferenceKind::Call);
683    }
684}