Skip to main content

garbage_code_hunter/language/adapter/
zig.rs

1//! ZigAdapter — Zig language adapter.
2
3use super::{
4    count_dead_code_with, count_duplicate_imports_with, count_params, is_boolean_or_null,
5    is_common_safe_number, is_inside_declaration, is_repeating_chars, FunctionNode,
6    LanguageAdapter, MEANINGLESS_NAMES,
7};
8use crate::language::Language;
9use crate::treesitter::engine::ParsedFile;
10use crate::treesitter::query::QueryCapture;
11use regex::Regex;
12use std::sync::LazyLock;
13
14const ZIG_PATTERNS: &[&str] = &[
15    "(builtin_function (builtin_identifier) @pc_f (#eq? @pc_f \"@panic\"))",
16    "(function_declaration name: (identifier) @ex_name) @ex_fn",
17    "(variable_declaration (identifier) @nv_var)",
18    "(call_expression function: (field_expression member: (identifier) @dp_method (#match? @dp_method \"^(print|warn)$\")))",
19    "(builtin_function (builtin_identifier) @dp_bl (#eq? @dp_bl \"@compileLog\"))",
20    "(function_declaration (parameters) @ep_params)",
21    "[(integer) @mn_num (float) @mn_num]",
22];
23
24pub struct ZigAdapter;
25
26impl LanguageAdapter for ZigAdapter {
27    fn language(&self) -> Language {
28        Language::Zig
29    }
30
31    fn query_patterns(&self) -> &[&str] {
32        ZIG_PATTERNS
33    }
34
35    fn count_panic_calls(&self, file: &ParsedFile) -> usize {
36        self.count_panic_from_batch(file, &self.batch_captures(file))
37    }
38
39    fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
40        self.extract_functions_from_batch(file, &self.batch_captures(file))
41    }
42
43    fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
44        fn zig_scope_depth(node: tree_sitter::Node, depth: usize) -> usize {
45            let mut max = depth;
46            for i in 0..node.child_count() {
47                if let Some(child) = node.child(i as u32) {
48                    let child_depth = match child.kind() {
49                        "block" => depth + 1,
50                        _ => depth,
51                    };
52                    max = max.max(zig_scope_depth(child, child_depth));
53                }
54            }
55            max
56        }
57        zig_scope_depth(file.root_node(), 0)
58    }
59
60    fn count_naming_violations(&self, file: &ParsedFile) -> usize {
61        self.count_naming_from_batch(file, &self.batch_captures(file))
62    }
63
64    fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
65        fn walk_zig_nodes(
66            node: tree_sitter::Node,
67            depth: usize,
68            threshold: usize,
69            count: &mut usize,
70        ) {
71            if node.kind() == "block" && depth >= threshold {
72                *count += 1;
73            }
74            let child_depth = match node.kind() {
75                "block" => depth + 1,
76                _ => depth,
77            };
78            for i in 0..node.child_count() {
79                if let Some(child) = node.child(i as u32) {
80                    walk_zig_nodes(child, child_depth, threshold, count);
81                }
82            }
83        }
84        let mut count = 0;
85        walk_zig_nodes(file.root_node(), 0, 5, &mut count);
86        count
87    }
88
89    fn count_debug_calls(&self, file: &ParsedFile) -> usize {
90        self.count_debug_from_batch(file, &self.batch_captures(file))
91    }
92
93    fn count_excessive_params(&self, file: &ParsedFile, threshold: usize) -> usize {
94        self.count_excessive_from_batch_with(file, &self.batch_captures(file), threshold)
95    }
96
97    fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
98        self.count_magic_from_batch(file, &self.batch_captures(file))
99    }
100
101    fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
102        count_duplicate_imports_with(file, &["@import("])
103    }
104
105    fn count_dead_code(&self, file: &ParsedFile) -> usize {
106        count_dead_code_with(
107            file,
108            &["return;", "break;", "continue;"],
109            &["return ", "@panic(", "unreachable "],
110            "//",
111        )
112    }
113
114    fn count_panic_from_batch<'a>(
115        &self,
116        file: &ParsedFile,
117        batch: &[Vec<QueryCapture<'a>>],
118    ) -> usize {
119        let base = batch
120            .iter()
121            .filter(|m| m.iter().any(|c| c.name == "pc_f"))
122            .count();
123        let unreach = file
124            .content
125            .lines()
126            .filter(|l| {
127                let t = l.trim();
128                !t.starts_with("//") && t.contains("unreachable")
129            })
130            .count();
131        base + unreach
132    }
133
134    fn extract_functions_from_batch<'a>(
135        &self,
136        _file: &ParsedFile,
137        batch: &[Vec<QueryCapture<'a>>],
138    ) -> Vec<FunctionNode> {
139        let mut functions = Vec::new();
140        for m in batch {
141            let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
142            if !has_ex {
143                continue;
144            }
145            let mut name = String::new();
146            let mut start_line = 0usize;
147            let mut end_line = 0usize;
148            for c in m {
149                match c.name.as_str() {
150                    "ex_name" => name = c.text.to_string(),
151                    "ex_fn" => {
152                        start_line = c.node.start_position().row + 1;
153                        end_line = c.node.end_position().row + 1;
154                    }
155                    _ => {}
156                }
157            }
158            if !name.is_empty() {
159                functions.push(FunctionNode {
160                    name,
161                    start_line,
162                    end_line,
163                    nesting_depth: 0,
164                });
165            }
166        }
167        functions
168    }
169
170    fn count_naming_from_batch<'a>(
171        &self,
172        _file: &ParsedFile,
173        batch: &[Vec<QueryCapture<'a>>],
174    ) -> usize {
175        let mut count = 0usize;
176        static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
177            Regex::new(r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$").ok()
178        });
179        let terrible_re = TERRIBLE_RE.as_ref();
180        let idiomatic_single: &[&str] = &["i", "j", "k", "n", "e", "x"];
181
182        for m in batch {
183            for c in m {
184                if c.name == "nv_var" {
185                    let name = c.text;
186                    if name.len() == 1 && name.chars().all(|ch| ch.is_ascii_lowercase()) {
187                        if !idiomatic_single.contains(&name) {
188                            count += 1;
189                        }
190                        continue;
191                    }
192                    if let Some(re) = terrible_re {
193                        if re.is_match(&name.to_lowercase()) {
194                            count += 1;
195                            continue;
196                        }
197                    }
198                    if MEANINGLESS_NAMES.contains(&name) || is_repeating_chars(name) {
199                        count += 1;
200                        continue;
201                    }
202                }
203            }
204        }
205        count
206    }
207
208    fn count_debug_from_batch<'a>(
209        &self,
210        _file: &ParsedFile,
211        batch: &[Vec<QueryCapture<'a>>],
212    ) -> usize {
213        batch
214            .iter()
215            .filter(|m| m.iter().any(|c| c.name == "dp_method" || c.name == "dp_bl"))
216            .count()
217    }
218
219    fn count_excessive_from_batch<'a>(
220        &self,
221        _file: &ParsedFile,
222        batch: &[Vec<QueryCapture<'a>>],
223    ) -> usize {
224        self.count_excessive_from_batch_with(_file, batch, 5)
225    }
226
227    fn count_magic_from_batch<'a>(
228        &self,
229        _file: &ParsedFile,
230        batch: &[Vec<QueryCapture<'a>>],
231    ) -> usize {
232        let mut count = 0;
233        for m in batch {
234            for c in m {
235                if c.name == "mn_num" && !is_inside_declaration(c.node) {
236                    let text = c.text;
237                    if text != "0"
238                        && text != "1"
239                        && text != "-1"
240                        && !is_common_safe_number(text)
241                        && !is_boolean_or_null(text)
242                    {
243                        count += 1;
244                    }
245                }
246            }
247        }
248        count
249    }
250}
251
252impl ZigAdapter {
253    fn count_excessive_from_batch_with<'a>(
254        &self,
255        _file: &ParsedFile,
256        batch: &[Vec<QueryCapture<'a>>],
257        threshold: usize,
258    ) -> usize {
259        let mut count = 0;
260        for m in batch {
261            for c in m {
262                if c.name == "ep_params" && count_params(c.text) > threshold {
263                    count += 1;
264                }
265            }
266        }
267        count
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::super::parse_code;
274    use super::*;
275
276    fn parse_zig(code: &str) -> ParsedFile {
277        parse_code(code, "test.zig").expect("parse")
278    }
279
280    #[test]
281    fn test_zig_count_panic_at_panic() {
282        let code = r#"
283fn main() void {
284    @panic("boom");
285    @panic("bang");
286}
287"#;
288        let file = parse_zig(code);
289        let adapter = ZigAdapter;
290        assert_eq!(adapter.count_panic_calls(&file), 2);
291    }
292
293    #[test]
294    fn test_zig_count_panic_clean() {
295        let code = "fn add(x: i32) i32 { return x + 1; }\n";
296        let file = parse_zig(code);
297        let adapter = ZigAdapter;
298        assert_eq!(adapter.count_panic_calls(&file), 0);
299    }
300
301    #[test]
302    fn test_zig_extract_functions() {
303        let code = r#"
304fn foo() void {}
305fn bar(x: i32) i32 { return x; }
306"#;
307        let file = parse_zig(code);
308        let adapter = ZigAdapter;
309        let fns = adapter.extract_functions(&file);
310        assert_eq!(fns.len(), 2);
311        assert_eq!(fns[0].name, "foo");
312        assert_eq!(fns[1].name, "bar");
313    }
314
315    #[test]
316    fn test_zig_naming_single_letter() {
317        let code = r#"
318fn main() void {
319    const a: i32 = 1;
320    var b: i32 = 2;
321}
322"#;
323        let file = parse_zig(code);
324        let adapter = ZigAdapter;
325        assert_eq!(adapter.count_naming_violations(&file), 2);
326    }
327
328    #[test]
329    fn test_zig_debug_print() {
330        let code = r#"
331const std = @import("std");
332fn main() void {
333    std.debug.print("hello", .{});
334}
335"#;
336        let file = parse_zig(code);
337        let adapter = ZigAdapter;
338        assert_eq!(adapter.count_debug_calls(&file), 1);
339    }
340
341    #[test]
342    fn test_zig_excessive_params() {
343        let code = "fn process(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) void {}\n";
344        let file = parse_zig(code);
345        let adapter = ZigAdapter;
346        assert_eq!(adapter.count_excessive_params(&file, 5), 1);
347    }
348
349    #[test]
350    fn test_zig_magic_numbers() {
351        let code = r#"
352fn main() void {
353    foo(41);
354    bar(100);
355}
356"#;
357        let file = parse_zig(code);
358        let adapter = ZigAdapter;
359        assert_eq!(adapter.count_magic_numbers(&file), 2);
360    }
361
362    #[test]
363    fn test_zig_magic_numbers_skips_trivial() {
364        let code = "fn main() void { foo(0); bar(1); }\n";
365        let file = parse_zig(code);
366        let adapter = ZigAdapter;
367        assert_eq!(adapter.count_magic_numbers(&file), 0);
368    }
369
370    #[test]
371    fn test_zig_panic_unreachable() {
372        let code = "fn main() void { unreachable; }\n";
373        let file = parse_zig(code);
374        let adapter = ZigAdapter;
375        assert_eq!(adapter.count_panic_calls(&file), 1);
376    }
377
378    #[test]
379    fn test_zig_debug_compile_log() {
380        let code = "fn main() void { @compileLog(\"debug\"); }\n";
381        let file = parse_zig(code);
382        let adapter = ZigAdapter;
383        assert_eq!(adapter.count_debug_calls(&file), 1);
384    }
385
386    #[test]
387    fn test_zig_dead_code_after_return() {
388        let code = r#"
389fn foo() i32 {
390    return 42;
391    var x: i32 = 1;
392}
393"#;
394        let file = parse_zig(code);
395        let adapter = ZigAdapter;
396        assert_eq!(adapter.count_dead_code(&file), 1);
397    }
398}