dx_forge/server/
semantic_analyzer.rs

1//! Semantic Analysis Module
2//!
3//! Uses tree-sitter to analyze code structure, detect symbols, and provide semantic information
4
5use anyhow::Result;
6use std::collections::HashMap;
7use tree_sitter::{Parser, Node};
8use std::path::Path;
9
10
11
12/// Symbol information
13#[derive(Debug, Clone)]
14pub struct Symbol {
15    pub name: String,
16    pub kind: SymbolKind,
17    pub range: Range,
18    pub children: Vec<Symbol>,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub enum SymbolKind {
23    Function,
24    Struct,
25    Enum,
26    Impl,
27    Mod,
28    Const,
29    Static,
30    Trait,
31    Type,
32    Variable,
33}
34
35#[derive(Debug, Clone)]
36pub struct Range {
37    pub start_line: usize,
38    pub start_col: usize,
39    pub end_line: usize,
40    pub end_col: usize,
41}
42
43/// Semantic analyzer for Rust code
44pub struct SemanticAnalyzer {
45    parser: Parser,
46    symbol_table: HashMap<String, Vec<Symbol>>,
47}
48
49impl SemanticAnalyzer {
50    /// Create a new semantic analyzer
51    pub fn new() -> Result<Self> {
52        let mut parser = Parser::new();
53        let language = tree_sitter_rust::LANGUAGE.into();
54        parser.set_language(&language)?;
55
56        Ok(Self {
57            parser,
58            symbol_table: HashMap::new(),
59        })
60    }
61
62    /// Parse and analyze a file
63    pub fn analyze_file(&mut self, file_path: &Path, source: &str) -> Result<Vec<Symbol>> {
64        let tree = self.parser.parse(source, None)
65            .ok_or_else(|| anyhow::anyhow!("Failed to parse file"))?;
66
67        let root = tree.root_node();
68        let symbols = self.extract_symbols(root, source)?;
69
70        // Store in symbol table
71        self.symbol_table.insert(
72            file_path.to_string_lossy().to_string(),
73            symbols.clone()
74        );
75
76        Ok(symbols)
77    }
78
79    /// Extract symbols from AST node
80    fn extract_symbols(&self, node: Node, source: &str) -> Result<Vec<Symbol>> {
81        let mut symbols = Vec::new();
82        let mut cursor = node.walk();
83
84        for child in node.children(&mut cursor) {
85            if let Some(symbol) = self.node_to_symbol(child, source)? {
86                symbols.push(symbol);
87            }
88        }
89
90        Ok(symbols)
91    }
92
93    /// Convert tree-sitter node to symbol
94    fn node_to_symbol(&self, node: Node, source: &str) -> Result<Option<Symbol>> {
95        let kind_str = node.kind();
96        
97        let kind = match kind_str {
98            "function_item" => SymbolKind::Function,
99            "struct_item" => SymbolKind::Struct,
100            "enum_item" => SymbolKind::Enum,
101            "impl_item" => SymbolKind::Impl,
102            "mod_item" => SymbolKind::Mod,
103            "const_item" => SymbolKind::Const,
104            "static_item" => SymbolKind::Static,
105            "trait_item" => SymbolKind::Trait,
106            "type_item" => SymbolKind::Type,
107            _ => return Ok(None),
108        };
109
110        // Extract name
111        let name = self.extract_name(node, source)?;
112        
113        // Create range
114        let range = Range {
115            start_line: node.start_position().row + 1,
116            start_col: node.start_position().column,
117            end_line: node.end_position().row + 1,
118            end_col: node.end_position().column,
119        };
120
121        // Extract children symbols
122        let children = self.extract_symbols(node, source)?;
123
124        Ok(Some(Symbol {
125            name,
126            kind,
127            range,
128            children,
129        }))
130    }
131
132    /// Extract name from node
133    fn extract_name(&self, node: Node, source: &str) -> Result<String> {
134        // Find identifier child node
135        let mut cursor = node.walk();
136        for child in node.children(&mut cursor) {
137            if child.kind() == "identifier" {
138                let start = child.start_byte();
139                let end = child.end_byte();
140                return Ok(source[start..end].to_string());
141            }
142        }
143        Ok("(anonymous)".to_string())
144    }
145
146    /// Find symbol at position
147    pub fn find_symbol_at_position(
148        &self,
149        file_path: &Path,
150        line: usize,
151        column: usize
152    ) -> Option<&Symbol> {
153        let symbols = self.symbol_table.get(&file_path.to_string_lossy().to_string())?;
154        self.find_symbol_in_tree(symbols, line, column)
155    }
156
157    /// Recursively search symbol tree
158    fn find_symbol_in_tree<'a>(
159        &self,
160        symbols: &'a [Symbol],
161        line: usize,
162        column: usize
163    ) -> Option<&'a Symbol> {
164        for symbol in symbols {
165            if self.contains_position(&symbol.range, line, column) {
166                // Check children first (more specific)
167                if let Some(child) = self.find_symbol_in_tree(&symbol.children, line, column) {
168                    return Some(child);
169                }
170                return Some(symbol);
171            }
172        }
173        None
174    }
175
176    /// Check if range contains position
177    fn contains_position(&self, range: &Range, line: usize, column: usize) -> bool {
178        if line < range.start_line || line > range.end_line {
179            return false;
180        }
181        if line == range.start_line && column < range.start_col {
182            return false;
183        }
184        if line == range.end_line && column > range.end_col {
185            return false;
186        }
187        true
188    }
189
190    /// Get all symbols in file
191    pub fn get_symbols(&self, file_path: &Path) -> Option<&Vec<Symbol>> {
192        self.symbol_table.get(&file_path.to_string_lossy().to_string())
193    }
194
195    /// Detect DX component patterns using tree-sitter
196    pub fn detect_dx_patterns(&mut self, source: &str) -> Result<Vec<DxPattern>> {
197        let tree = self.parser.parse(source, None)
198            .ok_or_else(|| anyhow::anyhow!("Failed to parse source"))?;
199
200        let mut patterns = Vec::new();
201        let root = tree.root_node();
202
203        // Query for JSX/TSX elements that match dx* pattern
204        self.find_dx_elements(root, source, &mut patterns)?;
205
206        Ok(patterns)
207    }
208
209    /// Recursively find DX element patterns
210    fn find_dx_elements(
211        &self,
212        node: Node,
213        source: &str,
214        patterns: &mut Vec<DxPattern>
215    ) -> Result<()> {
216        // Check if this node is a JSX element starting with "dx"
217        if node.kind() == "jsx_element" || node.kind() == "jsx_self_closing_element" {
218            let mut cursor = node.walk();
219            for child in node.children(&mut cursor) {
220                if child.kind() == "jsx_opening_element" || child.kind() == "identifier" {
221                    let start = child.start_byte();
222                    let end = child.end_byte();
223                    let text = &source[start..end];
224                    
225                    if text.starts_with("dx") || text.contains("<dx") {
226                        let component_name = text
227                            .trim_start_matches('<')
228                            .split(|c: char| c.is_whitespace() || c == '>')
229                            .next()
230                            .unwrap_or("")
231                            .to_string();
232
233                        if component_name.starts_with("dx") {
234                            patterns.push(DxPattern {
235                                component_name,
236                                line: node.start_position().row + 1,
237                                col: node.start_position().column,
238                            });
239                        }
240                    }
241                }
242            }
243        }
244
245        // Recurse through children
246        let mut cursor = node.walk();
247        for child in node.children(&mut cursor) {
248            self.find_dx_elements(child, source, patterns)?;
249        }
250
251        Ok(())
252    }
253}
254
255/// DX component pattern match
256#[derive(Debug, Clone)]
257pub struct DxPattern {
258    pub component_name: String,
259    pub line: usize,
260    pub col: usize,
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_analyzer_creation() {
269        let analyzer = SemanticAnalyzer::new();
270        assert!(analyzer.is_ok());
271    }
272
273    #[test]
274    fn test_rust_parsing() {
275        let mut analyzer = SemanticAnalyzer::new().unwrap();
276        let source = r#"
277            fn main() {
278                println!("Hello");
279            }
280            
281            struct MyStruct {
282                field: i32,
283            }
284        "#;
285
286        let path = Path::new("test.rs");
287        let symbols = analyzer.analyze_file(path, source).unwrap();
288        
289        assert!(!symbols.is_empty());
290        assert!(symbols.iter().any(|s| s.kind == SymbolKind::Function));
291        assert!(symbols.iter().any(|s| s.kind == SymbolKind::Struct));
292    }
293
294    #[test]
295    fn test_nested_symbols() {
296        let mut analyzer = SemanticAnalyzer::new().unwrap();
297        let source = r#"
298            mod my_mod {
299                struct Inner {
300                    x: i32
301                }
302                
303                impl Inner {
304                    fn new() -> Self { Self { x: 0 } }
305                }
306            }
307        "#;
308
309        let path = Path::new("nested.rs");
310        let symbols = analyzer.analyze_file(path, source).unwrap();
311        
312        let mod_symbol = symbols.iter().find(|s| s.kind == SymbolKind::Mod).unwrap();
313        assert_eq!(mod_symbol.name, "my_mod");
314        assert!(!mod_symbol.children.is_empty());
315        
316        let struct_symbol = mod_symbol.children.iter().find(|s| s.kind == SymbolKind::Struct).unwrap();
317        assert_eq!(struct_symbol.name, "Inner");
318    }
319
320    #[test]
321    fn test_find_symbol_at_position() {
322        let mut analyzer = SemanticAnalyzer::new().unwrap();
323        let source = r#"
324            fn target_function() {
325                // code
326            }
327        "#;
328        
329        let path = Path::new("lookup.rs");
330        analyzer.analyze_file(path, source).unwrap();
331        
332        // Line 2 (1-indexed), column 15 should be inside the function
333        let symbol = analyzer.find_symbol_at_position(path, 2, 15);
334        assert!(symbol.is_some());
335        assert_eq!(symbol.unwrap().name, "target_function");
336        
337        // Line 10 should be None
338        let symbol = analyzer.find_symbol_at_position(path, 10, 0);
339        assert!(symbol.is_none());
340    }
341}