Skip to main content

perl_semantic_analyzer/analysis/
index.rs

1//! Cross-file workspace indexing for Perl symbols
2//!
3//! This module provides efficient indexing of symbols across all files in a workspace,
4//! enabling fast cross-file navigation, references, and refactoring.
5
6use crate::symbol::{SymbolKind, SymbolTable};
7use std::collections::{HashMap, HashSet};
8
9/// A symbol definition in the workspace
10#[derive(Clone, Debug)]
11pub struct SymbolDef {
12    /// The name of the symbol
13    pub name: String,
14    /// The kind of symbol (function, variable, package, etc.)
15    pub kind: SymbolKind,
16    /// The URI of the file containing this symbol
17    pub uri: String,
18    /// Start byte offset in the file
19    pub start: usize,
20    /// End byte offset in the file
21    pub end: usize,
22}
23
24/// Workspace-wide index for fast symbol lookups
25#[derive(Default)]
26pub struct WorkspaceIndex {
27    /// Index from symbol name to all its definitions
28    by_name: HashMap<String, Vec<SymbolDef>>,
29    /// Index from URI to all symbol names in that file (for fast removal)
30    by_uri: HashMap<String, HashSet<String>>,
31}
32
33impl WorkspaceIndex {
34    /// Create a new empty workspace index
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Update the index with symbols from a document
40    pub fn update_from_document(&mut self, uri: &str, _content: &str, symtab: &SymbolTable) {
41        // Remove old symbols from this file
42        self.remove_document(uri);
43
44        // Track all symbol names in this file
45        let mut names_in_file = HashSet::new();
46
47        // Add all symbols from the symbol table
48        for symbols in symtab.symbols.values() {
49            for symbol in symbols {
50                let name = symbol.name.clone();
51                names_in_file.insert(name.clone());
52
53                let def = SymbolDef {
54                    name: symbol.name.clone(),
55                    kind: symbol.kind,
56                    uri: uri.to_string(),
57                    start: symbol.location.start,
58                    end: symbol.location.end,
59                };
60
61                self.by_name.entry(name).or_default().push(def);
62            }
63        }
64
65        // Track which names are in this file
66        self.by_uri.insert(uri.to_string(), names_in_file);
67    }
68
69    /// Remove all symbols from a document
70    pub fn remove_document(&mut self, uri: &str) {
71        if let Some(names) = self.by_uri.remove(uri) {
72            for name in names {
73                if let Some(defs) = self.by_name.get_mut(&name) {
74                    defs.retain(|d| d.uri != uri);
75                    if defs.is_empty() {
76                        self.by_name.remove(&name);
77                    }
78                }
79            }
80        }
81    }
82
83    /// Find all definitions of a symbol by name
84    pub fn find_defs(&self, name: &str) -> &[SymbolDef] {
85        static EMPTY: Vec<SymbolDef> = Vec::new();
86        self.by_name.get(name).map(|v| v.as_slice()).unwrap_or(&EMPTY[..])
87    }
88
89    /// Find all references to a symbol (simplified version)
90    /// In a full implementation, this would analyze usage sites
91    pub fn find_refs(&self, name: &str) -> Vec<SymbolDef> {
92        // For now, return all definitions as references
93        // A full implementation would scan all files for usage sites
94        self.find_defs(name).to_vec()
95    }
96
97    /// Get all symbols in the workspace matching a query
98    pub fn search_symbols(&self, query: &str) -> Vec<SymbolDef> {
99        let query_lower = query.to_lowercase();
100        let mut results = Vec::new();
101
102        for (name, defs) in &self.by_name {
103            if name.to_lowercase().contains(&query_lower) {
104                results.extend(defs.clone());
105            }
106        }
107
108        results
109    }
110
111    /// Get the total number of indexed symbols
112    pub fn symbol_count(&self) -> usize {
113        self.by_name.values().map(|v| v.len()).sum()
114    }
115
116    /// Get the number of indexed files
117    pub fn file_count(&self) -> usize {
118        self.by_uri.len()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::SourceLocation;
126    use crate::symbol::Symbol;
127
128    #[test]
129    fn test_workspace_index() {
130        let mut index = WorkspaceIndex::new();
131
132        // Create a mock symbol table
133        let mut symtab = SymbolTable::new();
134
135        // Add a symbol to the symbol table
136        let symbol = Symbol {
137            name: "test_func".to_string(),
138            qualified_name: "main::test_func".to_string(),
139            kind: SymbolKind::Subroutine,
140            location: SourceLocation { start: 0, end: 10 },
141            scope_id: 0,
142            declaration: Some("sub".to_string()),
143            documentation: None,
144            attributes: Vec::new(),
145        };
146
147        symtab.symbols.entry("test_func".to_string()).or_default().push(symbol);
148
149        // Add document to index
150        index.update_from_document("file:///test.pl", "", &symtab);
151
152        // Find definitions
153        let defs = index.find_defs("test_func");
154        assert_eq!(defs.len(), 1);
155        assert_eq!(defs[0].name, "test_func");
156        assert_eq!(defs[0].uri, "file:///test.pl");
157
158        // Remove document
159        index.remove_document("file:///test.pl");
160        assert_eq!(index.find_defs("test_func").len(), 0);
161    }
162}