Skip to main content

sqry_lang_zig/
lib.rs

1//! Zig language plugin
2//!
3//! Provides relation tracking for `@import`/`usingnamespace` calls.
4//!
5//! ## Supported Zig Versions
6//!
7//! Zig 0.13+ (grammar pinned to tree-sitter-zig 1.1.2)
8
9pub mod relations;
10
11pub use relations::ZigGraphBuilder;
12
13use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
14use sqry_core::plugin::{LanguageMetadata, LanguagePlugin, error::ParseError};
15use std::path::Path;
16use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator, Tree};
17
18const LANGUAGE_ID: &str = "zig";
19const LANGUAGE_NAME: &str = "Zig";
20const TREE_SITTER_VERSION: &str = "1.1.2";
21
22/// Zig plugin implementation
23pub struct ZigPlugin {
24    graph_builder: ZigGraphBuilder,
25}
26
27impl ZigPlugin {
28    /// Creates a new Zig plugin instance.
29    #[must_use]
30    pub fn new() -> Self {
31        Self {
32            graph_builder: ZigGraphBuilder::default(),
33        }
34    }
35}
36
37impl Default for ZigPlugin {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl LanguagePlugin for ZigPlugin {
44    fn metadata(&self) -> LanguageMetadata {
45        LanguageMetadata {
46            id: LANGUAGE_ID,
47            name: LANGUAGE_NAME,
48            version: env!("CARGO_PKG_VERSION"),
49            author: "Verivus Pty Ltd",
50            description: "Zig language support for sqry",
51            tree_sitter_version: TREE_SITTER_VERSION,
52        }
53    }
54
55    fn extensions(&self) -> &'static [&'static str] {
56        &["zig", "zon"]
57    }
58
59    fn language(&self) -> Language {
60        tree_sitter_zig::LANGUAGE.into()
61    }
62
63    fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
64        let mut parser = Parser::new();
65        parser
66            .set_language(&self.language())
67            .map_err(|e| ParseError::LanguageSetFailed(e.to_string()))?;
68
69        parser
70            .parse(content, None)
71            .ok_or(ParseError::TreeSitterFailed)
72    }
73
74    fn extract_scopes(
75        &self,
76        tree: &Tree,
77        content: &[u8],
78        file_path: &Path,
79    ) -> Result<Vec<Scope>, sqry_core::plugin::error::ScopeError> {
80        Self::extract_zig_scopes(tree, content, file_path)
81    }
82
83    fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
84        Some(&self.graph_builder)
85    }
86}
87
88impl ZigPlugin {
89    /// Extract scopes from Zig source using tree-sitter queries
90    fn extract_zig_scopes(
91        tree: &Tree,
92        content: &[u8],
93        file_path: &Path,
94    ) -> Result<Vec<Scope>, sqry_core::plugin::error::ScopeError> {
95        use sqry_core::plugin::error::ScopeError;
96
97        let root_node = tree.root_node();
98        let language = tree_sitter_zig::LANGUAGE.into();
99
100        // Zig scope query: functions, structs, enums, unions, tests
101        // Note: Zig containers often get names from parent variable_declaration
102        let scope_query = r"
103; Function declarations
104(function_declaration
105  (identifier) @function.name
106) @function.type
107
108; Struct declarations (containers get name from parent variable_declaration)
109(variable_declaration
110  (identifier) @struct.name
111  (struct_declaration)
112) @struct.type
113
114; Enum declarations
115(variable_declaration
116  (identifier) @enum.name
117  (enum_declaration)
118) @enum.type
119
120; Union declarations
121(variable_declaration
122  (identifier) @union.name
123  (union_declaration)
124) @union.type
125
126; Test declarations
127(test_declaration
128  (string
129    (string_content) @test.name
130  )?
131) @test.type
132";
133
134        let query = Query::new(&language, scope_query)
135            .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
136
137        let mut scopes = Vec::new();
138        let mut cursor = QueryCursor::new();
139        let mut query_matches = cursor.matches(&query, root_node, content);
140
141        while let Some(m) = query_matches.next() {
142            let mut scope_type = None;
143            let mut scope_name = None;
144            let mut scope_start = None;
145            let mut scope_end = None;
146
147            for capture in m.captures {
148                let capture_name = query.capture_names()[capture.index as usize];
149                let node = capture.node;
150
151                let capture_ext = std::path::Path::new(capture_name)
152                    .extension()
153                    .and_then(|ext| ext.to_str());
154
155                if capture_ext.is_some_and(|ext| ext.eq_ignore_ascii_case("type")) {
156                    scope_type = Some(capture_name.trim_end_matches(".type").to_string());
157                    scope_start = Some(node.start_position());
158                    scope_end = Some(node.end_position());
159                } else if capture_ext.is_some_and(|ext| ext.eq_ignore_ascii_case("name")) {
160                    scope_name = node
161                        .utf8_text(content)
162                        .ok()
163                        .map(std::string::ToString::to_string);
164                }
165            }
166
167            // For anonymous tests, generate a name from location
168            if scope_type.as_deref() == Some("test")
169                && scope_name.is_none()
170                && let Some(start) = scope_start
171            {
172                scope_name = Some(format!("test@{}", start.row + 1));
173            }
174
175            if let (Some(stype), Some(sname), Some(start), Some(end)) =
176                (scope_type, scope_name, scope_start, scope_end)
177            {
178                // Normalize scope type
179                let normalized_type = match stype.as_str() {
180                    "function" | "test" => "function",
181                    "struct" | "union" => "struct",
182                    "enum" => "enum",
183                    other => other,
184                };
185
186                let scope = Scope {
187                    id: ScopeId::new(0), // Will be reassigned by link_nested_scopes
188                    scope_type: normalized_type.to_string(),
189                    name: sname,
190                    file_path: file_path.to_path_buf(),
191                    start_line: start.row + 1,
192                    start_column: start.column,
193                    end_line: end.row + 1,
194                    end_column: end.column,
195                    parent_id: None,
196                };
197                scopes.push(scope);
198            }
199        }
200
201        // Sort by (start_line, start_column) for link_nested_scopes
202        scopes.sort_by_key(|s| (s.start_line, s.start_column));
203
204        link_nested_scopes(&mut scopes);
205        Ok(scopes)
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_plugin_metadata() {
215        let plugin = ZigPlugin::default();
216        let metadata = plugin.metadata();
217        assert_eq!(metadata.id, "zig");
218        assert_eq!(metadata.name, "Zig");
219    }
220
221    #[test]
222    fn test_extensions() {
223        let plugin = ZigPlugin::default();
224        assert_eq!(plugin.extensions(), &["zig", "zon"]);
225    }
226
227    #[test]
228    fn test_can_parse() {
229        let plugin = ZigPlugin::default();
230        let content = b"const std = @import(\"std\");";
231        let tree = plugin.parse_ast(content);
232        assert!(tree.is_ok());
233    }
234
235    #[test]
236    fn test_graph_builder_returns_some() {
237        let plugin = ZigPlugin::default();
238        assert!(plugin.graph_builder().is_some());
239    }
240}