Skip to main content

enya_analyzer/
parser.rs

1//! Tree-sitter integration for parsing Rust source files.
2//!
3//! Provides a wrapper around tree-sitter-rust for parsing Rust code
4//! and querying for specific patterns like macro invocations.
5
6use std::path::Path;
7
8use tree_sitter::{Language, Parser, Query, Tree};
9
10/// Error type for parsing operations.
11#[derive(Debug)]
12pub struct ParseError(pub String);
13
14impl std::fmt::Display for ParseError {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        write!(f, "{}", self.0)
17    }
18}
19
20impl std::error::Error for ParseError {}
21
22/// A wrapper around tree-sitter for parsing Rust source code.
23pub struct RustParser {
24    parser: Parser,
25    language: Language,
26}
27
28impl RustParser {
29    /// Creates a new Rust parser.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the tree-sitter language cannot be set.
34    pub fn new() -> Result<Self, ParseError> {
35        let language: Language = tree_sitter_rust::LANGUAGE.into();
36
37        let mut parser = Parser::new();
38        parser
39            .set_language(&language)
40            .map_err(|e| ParseError(format!("Failed to set language: {e}")))?;
41
42        Ok(Self { parser, language })
43    }
44
45    /// Parses the given source code into a syntax tree.
46    #[must_use]
47    pub fn parse(&mut self, source: &str) -> Option<Tree> {
48        self.parser.parse(source, None)
49    }
50
51    /// Parses a file from disk.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the file cannot be read or parsed.
56    pub fn parse_file(&mut self, path: &Path) -> Result<(String, Tree), ParseError> {
57        let source = std::fs::read_to_string(path)
58            .map_err(|e| ParseError(format!("Failed to read file {}: {e}", path.display())))?;
59
60        let tree = self
61            .parse(&source)
62            .ok_or_else(|| ParseError("Failed to parse source".to_string()))?;
63
64        Ok((source, tree))
65    }
66
67    /// Creates a query for matching patterns in the syntax tree.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the query is invalid.
72    pub fn create_query(&self, query_str: &str) -> Result<Query, ParseError> {
73        Query::new(&self.language, query_str).map_err(|e| ParseError(format!("Invalid query: {e}")))
74    }
75}
76
77impl Default for RustParser {
78    fn default() -> Self {
79        Self::new().expect("Failed to create Rust parser")
80    }
81}
82
83/// Tree-sitter query for finding metrics-rs macro invocations.
84///
85/// This query matches:
86/// - `counter!("name", ...)`
87/// - `gauge!("name", ...)`
88/// - `histogram!("name", ...)`
89pub const METRICS_QUERY: &str = r"
90(macro_invocation
91  macro: (identifier) @macro_name
92  (token_tree) @args)
93";
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_parse_simple_function() {
101        let mut parser = RustParser::new().expect("Failed to create parser");
102        let source = r#"
103fn main() {
104    println!("Hello, world!");
105}
106"#;
107        let tree = parser.parse(source).expect("Failed to parse");
108        assert_eq!(tree.root_node().kind(), "source_file");
109    }
110
111    #[test]
112    fn test_parse_with_metrics_macro() {
113        let mut parser = RustParser::new().expect("Failed to create parser");
114        let source = r#"
115fn handle_request() {
116    counter!("http.requests", "method" => "GET").increment(1);
117}
118"#;
119        let tree = parser.parse(source).expect("Failed to parse");
120        assert_eq!(tree.root_node().kind(), "source_file");
121    }
122
123    #[test]
124    fn test_metrics_query() {
125        use streaming_iterator::StreamingIterator;
126        use tree_sitter::QueryCursor;
127
128        let mut parser = RustParser::new().expect("Failed to create parser");
129        let source = r#"
130fn handle_request() {
131    counter!("http.requests", "method" => "GET").increment(1);
132    gauge!("connections.active").set(42.0);
133    histogram!("request.latency_ms").record(150.0);
134}
135"#;
136        let tree = parser.parse(source).expect("Failed to parse");
137        let query = parser
138            .create_query(METRICS_QUERY)
139            .expect("Failed to create query");
140        let mut cursor = QueryCursor::new();
141
142        let mut count = 0;
143        let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
144        while matches.next().is_some() {
145            count += 1;
146        }
147
148        // Should find 3 macro invocations (counter, gauge, histogram)
149        assert_eq!(count, 3);
150    }
151}