1pub 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
22pub struct ZigPlugin {
24 graph_builder: ZigGraphBuilder,
25}
26
27impl ZigPlugin {
28 #[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 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 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 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 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), 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 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}