garbage_code_hunter/language/adapter/
zig.rs1use 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}