Skip to main content

normalize_languages/
zig.rs

1//! Zig language support.
2
3use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
6use tree_sitter::Node;
7
8/// Zig language support.
9pub 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        // Look for @import("module")
28        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        // Extract the string argument
38        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        // Zig: @import("module")
59        format!("@import(\"{}\")", import.module)
60    }
61
62    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
63        // Check for pub keyword before the declaration
64        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        // Also check if node starts with pub
71        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        // FnProto uses field "function" for the name identifier.
90        // VarDecl uses field "variable_type_function" for the name identifier.
91        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
109// =============================================================================
110// Zig Module Resolver
111// =============================================================================
112
113/// Module resolver for Zig.
114///
115/// Zig uses `@import("path.zig")` for file imports. Relative paths are resolved
116/// relative to the importing file. `@import("std")` and other named imports
117/// return `NotFound`.
118pub 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        // Named imports (std, builtin, etc.) — not resolvable to files
150        if !raw.starts_with('.') && !raw.ends_with(".zig") {
151            return Resolution::NotFound;
152        }
153        // Relative path import
154        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            // Zig grammar uses PascalCase node kinds
179            "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            // control flow — not extracted as symbols
188            "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}