normalize_languages/
zig.rs1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
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 as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27 if node.kind() != "builtin_call_expression" {
29 return Vec::new();
30 }
31
32 let text = &content[node.byte_range()];
33 if !text.starts_with("@import") {
34 return Vec::new();
35 }
36
37 let mut cursor = node.walk();
39 for child in node.children(&mut cursor) {
40 if child.kind() == "string_literal" {
41 let module = content[child.byte_range()].trim_matches('"').to_string();
42 let is_relative = module.starts_with('.');
43 return vec![Import {
44 module,
45 names: Vec::new(),
46 alias: None,
47 is_wildcard: false,
48 is_relative,
49 line: node.start_position().row + 1,
50 }];
51 }
52 }
53
54 Vec::new()
55 }
56
57 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
58 format!("@import(\"{}\")", import.module)
60 }
61
62 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
63 if let Some(prev) = node.prev_sibling() {
65 let text = &content[prev.byte_range()];
66 if text == "pub" {
67 return Visibility::Public;
68 }
69 }
70 let text = &content[node.byte_range()];
72 if text.starts_with("pub ") {
73 Visibility::Public
74 } else {
75 Visibility::Private
76 }
77 }
78
79 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
80 let name = symbol.name.as_str();
81 match symbol.kind {
82 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
83 crate::SymbolKind::Module => name == "tests" || name == "test",
84 _ => false,
85 }
86 }
87
88 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
89 let name_node = node
92 .child_by_field_name("function")
93 .or_else(|| node.child_by_field_name("variable_type_function"))?;
94 Some(&content[name_node.byte_range()])
95 }
96
97 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
98 node.child_by_field_name("body")
99 }
100
101 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
102 static RESOLVER: ZigModuleResolver = ZigModuleResolver;
103 Some(&RESOLVER)
104 }
105}
106
107impl LanguageSymbols for Zig {}
108
109pub struct ZigModuleResolver;
119
120impl ModuleResolver for ZigModuleResolver {
121 fn workspace_config(&self, root: &Path) -> ResolverConfig {
122 ResolverConfig {
123 workspace_root: root.to_path_buf(),
124 path_mappings: Vec::new(),
125 search_roots: vec![root.to_path_buf()],
126 }
127 }
128
129 fn module_of_file(&self, root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
130 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
131 if ext != "zig" {
132 return Vec::new();
133 }
134 if let Ok(rel) = file.strip_prefix(root) {
135 let rel_str = rel.to_str().unwrap_or("").replace('\\', "/");
136 return vec![ModuleId {
137 canonical_path: rel_str,
138 }];
139 }
140 Vec::new()
141 }
142
143 fn resolve(&self, from_file: &Path, spec: &ImportSpec, _cfg: &ResolverConfig) -> Resolution {
144 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
145 if ext != "zig" {
146 return Resolution::NotApplicable;
147 }
148 let raw = &spec.raw;
149 if !raw.starts_with('.') && !raw.ends_with(".zig") {
151 return Resolution::NotFound;
152 }
153 if let Some(parent) = from_file.parent() {
155 let resolved = parent.join(raw);
156 if resolved.exists() {
157 let name = resolved
158 .file_stem()
159 .and_then(|s| s.to_str())
160 .unwrap_or("")
161 .to_string();
162 return Resolution::Resolved(resolved, name);
163 }
164 }
165 Resolution::NotFound
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::validate_unused_kinds_audit;
173
174 #[test]
175 fn unused_node_kinds_audit() {
176 #[rustfmt::skip]
177 let documented_unused: &[&str] = &[
178 "ArrayTypeStart", "BUILTINIDENTIFIER", "BitShiftOp", "BlockExpr",
180 "BlockExprStatement", "BlockLabel", "BuildinTypeExpr", "ContainerDeclType",
181 "ForArgumentsList", "ForExpr", "ForItem", "ForPrefix", "ForTypeExpr",
182 "FormatSequence", "IDENTIFIER", "IfExpr", "IfPrefix", "IfTypeExpr",
183 "LabeledStatement", "LabeledTypeExpr", "LoopExpr", "LoopStatement",
184 "LoopTypeExpr", "ParamType", "PrefixTypeOp", "PtrTypeStart",
185 "SliceTypeStart", "Statement", "SwitchCase", "WhileContinueExpr",
186 "WhileExpr", "WhilePrefix", "WhileTypeExpr",
187 "ForStatement",
189 "WhileStatement",
190 "Block",
191 "IfStatement",
192 ];
193 validate_unused_kinds_audit(&Zig, documented_unused)
194 .expect("Zig unused node kinds audit failed");
195 }
196}