1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Zig;
10
11impl Language for Zig {
12 fn name(&self) -> &'static str {
13 "Zig"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["zig"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "zig"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["ContainerDecl"]
28 }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["FnProto", "TestDecl"]
32 }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["ContainerDecl"]
36 }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["SuffixExpr"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["FnProto", "ContainerDecl"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::ExplicitExport }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 if !self.is_public(node, content) {
52 return Vec::new();
53 }
54
55 let name = match self.node_name(node, content) {
56 Some(n) => n.to_string(),
57 None => return Vec::new(),
58 };
59
60 let kind = match node.kind() {
61 "FnProto" | "TestDecl" => SymbolKind::Function,
62 "ContainerDecl" => SymbolKind::Struct, _ => return Vec::new(),
64 };
65
66 vec![Export {
67 name,
68 kind,
69 line: node.start_position().row + 1,
70 }]
71 }
72
73 fn scope_creating_kinds(&self) -> &'static [&'static str] {
74 &["Block", "ForStatement", "WhileStatement"]
75 }
76
77 fn control_flow_kinds(&self) -> &'static [&'static str] {
78 &[
79 "IfStatement",
80 "ForStatement",
81 "WhileStatement",
82 "SwitchExpr",
83 ]
84 }
85
86 fn complexity_nodes(&self) -> &'static [&'static str] {
87 &[
88 "IfStatement",
89 "ForStatement",
90 "WhileStatement",
91 "SwitchExpr",
92 "ErrorUnionExpr",
93 "BinaryExpr",
94 ]
95 }
96
97 fn nesting_nodes(&self) -> &'static [&'static str] {
98 &[
99 "IfStatement",
100 "ForStatement",
101 "WhileStatement",
102 "SwitchExpr",
103 "FnProto",
104 "ContainerDecl",
105 ]
106 }
107
108 fn signature_suffix(&self) -> &'static str {
109 ""
110 }
111
112 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
113 let name = self.node_name(node, content)?;
114
115 let params = node
116 .child_by_field_name("parameters")
117 .map(|p| content[p.byte_range()].to_string())
118 .unwrap_or_else(|| "()".to_string());
119
120 let return_type = node
121 .child_by_field_name("return_type")
122 .map(|t| content[t.byte_range()].to_string());
123
124 let is_pub = self.is_public(node, content);
125 let prefix = if is_pub { "pub fn" } else { "fn" };
126
127 let signature = if let Some(ret) = return_type {
128 format!("{} {}{} {}", prefix, name, params, ret)
129 } else {
130 format!("{} {}{}", prefix, name, params)
131 };
132
133 Some(Symbol {
134 name: name.to_string(),
135 kind: SymbolKind::Function,
136 signature,
137 docstring: self.extract_docstring(node, content),
138 attributes: Vec::new(),
139 start_line: node.start_position().row + 1,
140 end_line: node.end_position().row + 1,
141 visibility: self.get_visibility(node, content),
142 children: Vec::new(),
143 is_interface_impl: false,
144 implements: Vec::new(),
145 })
146 }
147
148 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
149 let name = self.node_name(node, content)?;
150
151 let mut cursor = node.walk();
153 let mut kind = SymbolKind::Struct;
154 let mut keyword = "struct";
155 for child in node.children(&mut cursor) {
156 if child.kind() == "ContainerDeclType" {
157 if let Some(keyword_node) = child.child(0) {
159 let kw = &content[keyword_node.byte_range()];
160 if kw == "enum" {
161 kind = SymbolKind::Enum;
162 keyword = "enum";
163 } else if kw == "union" {
164 keyword = "union";
165 }
166 }
167 break;
168 }
169 }
170
171 let is_pub = self.is_public(node, content);
172 let prefix = if is_pub {
173 format!("pub {}", keyword)
174 } else {
175 keyword.to_string()
176 };
177
178 Some(Symbol {
179 name: name.to_string(),
180 kind,
181 signature: format!("{} {}", prefix, name),
182 docstring: self.extract_docstring(node, content),
183 attributes: Vec::new(),
184 start_line: node.start_position().row + 1,
185 end_line: node.end_position().row + 1,
186 visibility: self.get_visibility(node, content),
187 children: Vec::new(),
188 is_interface_impl: false,
189 implements: Vec::new(),
190 })
191 }
192
193 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
194 self.extract_container(node, content)
195 }
196
197 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
198 let mut prev = node.prev_sibling();
200 let mut doc_lines = Vec::new();
201
202 while let Some(sibling) = prev {
203 let text = &content[sibling.byte_range()];
204 if sibling.kind() == "doc_comment" || text.starts_with("///") {
205 let line = text.strip_prefix("///").unwrap_or(text).trim();
206 doc_lines.push(line.to_string());
207 prev = sibling.prev_sibling();
208 } else {
209 break;
210 }
211 }
212
213 if doc_lines.is_empty() {
214 return None;
215 }
216
217 doc_lines.reverse();
218 Some(doc_lines.join(" "))
219 }
220
221 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
222 Vec::new()
223 }
224
225 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
226 if node.kind() != "builtin_call_expression" {
228 return Vec::new();
229 }
230
231 let text = &content[node.byte_range()];
232 if !text.starts_with("@import") {
233 return Vec::new();
234 }
235
236 let mut cursor = node.walk();
238 for child in node.children(&mut cursor) {
239 if child.kind() == "string_literal" {
240 let module = content[child.byte_range()].trim_matches('"').to_string();
241 let is_relative = module.starts_with('.');
242 return vec![Import {
243 module,
244 names: Vec::new(),
245 alias: None,
246 is_wildcard: false,
247 is_relative,
248 line: node.start_position().row + 1,
249 }];
250 }
251 }
252
253 Vec::new()
254 }
255
256 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
257 format!("@import(\"{}\")", import.module)
259 }
260
261 fn is_public(&self, node: &Node, content: &str) -> bool {
262 if let Some(prev) = node.prev_sibling() {
264 let text = &content[prev.byte_range()];
265 if text == "pub" {
266 return true;
267 }
268 }
269 let text = &content[node.byte_range()];
271 text.starts_with("pub ")
272 }
273
274 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
275 if self.is_public(node, content) {
276 Visibility::Public
277 } else {
278 Visibility::Private
279 }
280 }
281
282 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
283 let name = symbol.name.as_str();
284 match symbol.kind {
285 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
286 crate::SymbolKind::Module => name == "tests" || name == "test",
287 _ => false,
288 }
289 }
290
291 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
292 None
293 }
294
295 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
296 node.child_by_field_name("body")
297 }
298
299 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
300 false
301 }
302
303 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
304 node.child_by_field_name("name")
305 .map(|n| &content[n.byte_range()])
306 }
307
308 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
309 let ext = path.extension()?.to_str()?;
310 if ext != "zig" {
311 return None;
312 }
313 let stem = path.file_stem()?.to_str()?;
314 Some(stem.to_string())
315 }
316
317 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
318 vec![format!("{}.zig", module)]
319 }
320
321 fn lang_key(&self) -> &'static str {
322 "zig"
323 }
324
325 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
326 import_name == "std" || import_name == "builtin"
327 }
328
329 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
330 None
332 }
333
334 fn resolve_local_import(
335 &self,
336 import: &str,
337 current_file: &Path,
338 project_root: &Path,
339 ) -> Option<PathBuf> {
340 if !import.ends_with(".zig") {
341 return None;
342 }
343
344 if import.starts_with('.') {
346 if let Some(dir) = current_file.parent() {
347 let full = dir.join(import);
348 if full.is_file() {
349 return Some(full);
350 }
351 }
352 }
353
354 let full = project_root.join(import);
356 if full.is_file() {
357 return Some(full);
358 }
359
360 None
361 }
362
363 fn resolve_external_import(
364 &self,
365 _import_name: &str,
366 _project_root: &Path,
367 ) -> Option<ResolvedPackage> {
368 None
370 }
371
372 fn get_version(&self, project_root: &Path) -> Option<String> {
373 let zon = project_root.join("build.zig.zon");
375 if zon.is_file() {
376 if let Ok(content) = std::fs::read_to_string(&zon) {
377 for line in content.lines() {
379 if line.contains(".version") && line.contains('"') {
380 if let Some(start) = line.find('"') {
381 let rest = &line[start + 1..];
382 if let Some(end) = rest.find('"') {
383 return Some(rest[..end].to_string());
384 }
385 }
386 }
387 }
388 }
389 }
390 None
391 }
392
393 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
394 None
395 }
396 fn indexable_extensions(&self) -> &'static [&'static str] {
397 &["zig"]
398 }
399 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
400 Vec::new()
401 }
402
403 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
404 use crate::traits::{has_extension, skip_dotfiles};
405 if skip_dotfiles(name) {
406 return true;
407 }
408 if is_dir && name == "zig-cache" {
409 return true;
410 }
411 !is_dir && !has_extension(name, self.indexable_extensions())
412 }
413
414 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
415 Vec::new()
416 }
417
418 fn package_module_name(&self, entry_name: &str) -> String {
419 entry_name
420 .strip_suffix(".zig")
421 .unwrap_or(entry_name)
422 .to_string()
423 }
424
425 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
426 if path.is_file() {
427 return Some(path.to_path_buf());
428 }
429 for name in &["src/main.zig", "src/root.zig", "main.zig"] {
431 let entry = path.join(name);
432 if entry.is_file() {
433 return Some(entry);
434 }
435 }
436 None
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use crate::validate_unused_kinds_audit;
444
445 #[test]
446 fn unused_node_kinds_audit() {
447 #[rustfmt::skip]
448 let documented_unused: &[&str] = &[
449 "ArrayTypeStart", "BUILTINIDENTIFIER", "BitShiftOp", "BlockExpr",
451 "BlockExprStatement", "BlockLabel", "BuildinTypeExpr", "ContainerDeclType",
452 "ForArgumentsList", "ForExpr", "ForItem", "ForPrefix", "ForTypeExpr",
453 "FormatSequence", "IDENTIFIER", "IfExpr", "IfPrefix", "IfTypeExpr",
454 "LabeledStatement", "LabeledTypeExpr", "LoopExpr", "LoopStatement",
455 "LoopTypeExpr", "ParamType", "PrefixTypeOp", "PtrTypeStart",
456 "SliceTypeStart", "Statement", "SwitchCase", "WhileContinueExpr",
457 "WhileExpr", "WhilePrefix", "WhileTypeExpr",
458 ];
459 validate_unused_kinds_audit(&Zig, documented_unused)
460 .expect("Zig unused node kinds audit failed");
461 }
462}