Skip to main content

brainwires_rag/code_analysis/
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    /// Medium precision: AST-based with heuristic matching.
411    Medium,
412    /// Low precision: text-based pattern matching.
413    Low,
414}
415
416impl PrecisionLevel {
417    /// Get a human-readable description
418    pub fn description(&self) -> &'static str {
419        match self {
420            Self::Medium => "medium (AST-based)",
421            Self::Low => "low (text-based)",
422        }
423    }
424}
425
426// ============================================================================
427// Result types for MCP tools
428// ============================================================================
429
430/// Result from find_definition containing the found definition
431#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
432pub struct DefinitionResult {
433    /// File path where the definition is located
434    pub file_path: String,
435    /// Symbol name
436    pub name: String,
437    /// Symbol kind
438    pub kind: SymbolKind,
439    /// Starting line (1-based)
440    pub start_line: usize,
441    /// Ending line (1-based)
442    pub end_line: usize,
443    /// Starting column (0-based)
444    pub start_col: usize,
445    /// Ending column (0-based)
446    pub end_col: usize,
447    /// Full signature or declaration
448    pub signature: String,
449    /// Documentation comment
450    pub doc_comment: Option<String>,
451}
452
453impl From<&Definition> for DefinitionResult {
454    fn from(def: &Definition) -> Self {
455        Self {
456            file_path: def.symbol_id.file_path.clone(),
457            name: def.symbol_id.name.clone(),
458            kind: def.symbol_id.kind,
459            start_line: def.symbol_id.start_line,
460            end_line: def.end_line,
461            start_col: def.symbol_id.start_col,
462            end_col: def.end_col,
463            signature: def.signature.clone(),
464            doc_comment: def.doc_comment.clone(),
465        }
466    }
467}
468
469/// Result from find_references containing a found reference
470#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
471pub struct ReferenceResult {
472    /// File path where the reference occurs
473    pub file_path: String,
474    /// Starting line (1-based)
475    pub start_line: usize,
476    /// Ending line (1-based)
477    pub end_line: usize,
478    /// Starting column (0-based)
479    pub start_col: usize,
480    /// Ending column (0-based)
481    pub end_col: usize,
482    /// Kind of reference
483    pub reference_kind: ReferenceKind,
484    /// Preview of the line containing the reference
485    pub preview: Option<String>,
486}
487
488impl From<&Reference> for ReferenceResult {
489    fn from(r: &Reference) -> Self {
490        Self {
491            file_path: r.file_path.clone(),
492            start_line: r.start_line,
493            end_line: r.end_line,
494            start_col: r.start_col,
495            end_col: r.end_col,
496            reference_kind: r.reference_kind,
497            preview: None,
498        }
499    }
500}
501
502/// A node in the call graph
503#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
504pub struct CallGraphNode {
505    /// Symbol name
506    pub name: String,
507    /// Symbol kind
508    pub kind: SymbolKind,
509    /// File path
510    pub file_path: String,
511    /// Line number
512    pub line: usize,
513    /// Nested callers/callees (for depth > 1)
514    pub children: Vec<CallGraphNode>,
515}
516
517/// Symbol info for call graph root
518#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
519pub struct SymbolInfo {
520    /// Symbol name
521    pub name: String,
522    /// Symbol kind
523    pub kind: SymbolKind,
524    /// File path
525    pub file_path: String,
526    /// Starting line
527    pub start_line: usize,
528    /// Ending line
529    pub end_line: usize,
530    /// Signature
531    pub signature: String,
532}
533
534// ============================================================================
535// Tests
536// ============================================================================
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_symbol_kind_from_ast_kind() {
544        assert_eq!(
545            SymbolKind::from_ast_kind("function_item"),
546            SymbolKind::Function
547        );
548        assert_eq!(
549            SymbolKind::from_ast_kind("class_definition"),
550            SymbolKind::Class
551        );
552        assert_eq!(
553            SymbolKind::from_ast_kind("method_definition"),
554            SymbolKind::Method
555        );
556        assert_eq!(
557            SymbolKind::from_ast_kind("unknown_node"),
558            SymbolKind::Unknown
559        );
560    }
561
562    #[test]
563    fn test_symbol_kind_display_name() {
564        assert_eq!(SymbolKind::Function.display_name(), "function");
565        assert_eq!(SymbolKind::Class.display_name(), "class");
566        assert_eq!(SymbolKind::Unknown.display_name(), "unknown");
567    }
568
569    #[test]
570    fn test_visibility_from_keywords() {
571        assert_eq!(Visibility::from_keywords("pub fn foo"), Visibility::Public);
572        assert_eq!(
573            Visibility::from_keywords("public void bar"),
574            Visibility::Public
575        );
576        assert_eq!(
577            Visibility::from_keywords("protected int x"),
578            Visibility::Protected
579        );
580        assert_eq!(
581            Visibility::from_keywords("fn private_func"),
582            Visibility::Private
583        );
584    }
585
586    #[test]
587    fn test_symbol_id_equality() {
588        let id1 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
589        let id2 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
590        let id3 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 20, 0);
591
592        assert_eq!(id1, id2);
593        assert_ne!(id1, id3);
594    }
595
596    #[test]
597    fn test_symbol_id_hash() {
598        use std::collections::HashSet;
599
600        let id1 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
601        let id2 = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 0);
602
603        let mut set = HashSet::new();
604        set.insert(id1);
605        assert!(set.contains(&id2));
606    }
607
608    #[test]
609    fn test_symbol_id_storage_id() {
610        let id = SymbolId::new("src/main.rs", "foo", SymbolKind::Function, 10, 5);
611        let storage_id = id.to_storage_id();
612        assert_eq!(storage_id, "src/main.rs:foo:10:5");
613    }
614
615    #[test]
616    fn test_definition_storage_id() {
617        let def = Definition {
618            symbol_id: SymbolId::new("src/lib.rs", "MyClass", SymbolKind::Class, 15, 0),
619            root_path: Some("/project".to_string()),
620            project: Some("test".to_string()),
621            end_line: 50,
622            end_col: 1,
623            signature: "class MyClass".to_string(),
624            doc_comment: None,
625            visibility: Visibility::Public,
626            parent_id: None,
627            indexed_at: 12345,
628        };
629
630        assert_eq!(def.to_storage_id(), "def:src/lib.rs:MyClass:15");
631        assert_eq!(def.file_path(), "src/lib.rs");
632        assert_eq!(def.name(), "MyClass");
633        assert_eq!(def.kind(), SymbolKind::Class);
634    }
635
636    #[test]
637    fn test_reference_storage_id() {
638        let reference = Reference {
639            file_path: "src/consumer.rs".to_string(),
640            root_path: None,
641            project: None,
642            start_line: 25,
643            end_line: 25,
644            start_col: 10,
645            end_col: 20,
646            target_symbol_id: "def:src/lib.rs:foo:10".to_string(),
647            reference_kind: ReferenceKind::Call,
648            indexed_at: 12345,
649        };
650
651        assert_eq!(reference.to_storage_id(), "ref:src/consumer.rs:25:10");
652    }
653
654    #[test]
655    fn test_precision_level_description() {
656        assert_eq!(PrecisionLevel::Medium.description(), "medium (AST-based)");
657        assert_eq!(PrecisionLevel::Low.description(), "low (text-based)");
658    }
659
660    #[test]
661    fn test_definition_result_from_definition() {
662        let def = Definition {
663            symbol_id: SymbolId::new("src/lib.rs", "my_func", SymbolKind::Function, 10, 0),
664            root_path: None,
665            project: None,
666            end_line: 20,
667            end_col: 1,
668            signature: "fn my_func()".to_string(),
669            doc_comment: Some("Does stuff".to_string()),
670            visibility: Visibility::Public,
671            parent_id: None,
672            indexed_at: 0,
673        };
674
675        let result = DefinitionResult::from(&def);
676        assert_eq!(result.file_path, "src/lib.rs");
677        assert_eq!(result.name, "my_func");
678        assert_eq!(result.kind, SymbolKind::Function);
679        assert_eq!(result.start_line, 10);
680        assert_eq!(result.end_line, 20);
681        assert_eq!(result.doc_comment, Some("Does stuff".to_string()));
682    }
683
684    #[test]
685    fn test_serialization() {
686        let id = SymbolId::new("src/main.rs", "test", SymbolKind::Function, 1, 0);
687        let json = serde_json::to_string(&id).unwrap();
688        let deserialized: SymbolId = serde_json::from_str(&json).unwrap();
689        assert_eq!(id, deserialized);
690    }
691
692    #[test]
693    fn test_reference_kind_serialization() {
694        let kind = ReferenceKind::Call;
695        let json = serde_json::to_string(&kind).unwrap();
696        assert_eq!(json, "\"call\"");
697
698        let deserialized: ReferenceKind = serde_json::from_str(&json).unwrap();
699        assert_eq!(deserialized, ReferenceKind::Call);
700    }
701}