garbage_code_hunter/language/adapter/
go.rs1use super::{
4 count_duplicate_imports_with, count_nested_blocks, 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 GO_PATTERNS: &[&str] = &[
15 "(call_expression function: (identifier) @pc_fn (#eq? @pc_fn \"panic\"))",
17 "[(function_declaration name: (identifier) @ex_name) (method_declaration name: (field_identifier) @ex_name)] @ex_fn",
19 "[(short_var_declaration left: (expression_list (identifier) @nv_var)) (var_spec name: (identifier) @nv_var)]",
21 "(method_declaration receiver: (parameter_list (parameter_declaration name: (identifier) @nv_rec)))",
22 r#"(call_expression
24 function: (selector_expression
25 operand: (identifier) @dp_pkg
26 field: (field_identifier) @dp_method)
27 (#match? @dp_pkg "^(fmt|log)$")
28 (#match? @dp_method "^(Print|Println|Printf|Fprint|Fprintln|Fprintf|Sprint|Sprintln|Sprintf)$"))"#,
29 "[(function_declaration parameters: (parameter_list) @ep_params) (method_declaration parameters: (parameter_list) @ep_params)]",
31 "[(int_literal) @mn_num (float_literal) @mn_num]",
33 "(go_statement) @gs_go",
35 r#"(call_expression function: (selector_expression operand: (identifier) @cv_pkg field: (field_identifier) @cv_method) (#eq? @cv_pkg "fmt") (#match? @cv_method "^(Errorf|New)$"))"#,
37 r#"(selector_expression operand: (identifier) @ui_pkg (#eq? @ui_pkg "unsafe"))"#,
39 "(import_spec path: (interpreted_string_literal) @ui_import (#match? @ui_import \"unsafe\"))",
40];
41
42pub struct GoAdapter;
43
44impl LanguageAdapter for GoAdapter {
45 fn language(&self) -> Language {
46 Language::Go
47 }
48
49 fn query_patterns(&self) -> &[&str] {
50 GO_PATTERNS
51 }
52
53 fn count_panic_calls(&self, file: &ParsedFile) -> usize {
54 self.count_panic_from_batch(file, &self.batch_captures(file))
55 }
56
57 fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
58 self.extract_functions_from_batch(file, &self.batch_captures(file))
59 }
60
61 fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
62 fn go_scope_depth(node: tree_sitter::Node, depth: usize) -> usize {
63 let mut max = depth;
64 for i in 0..node.child_count() {
65 if let Some(child) = node.child(i as u32) {
66 let child_depth = match child.kind() {
67 "block" => depth + 1,
68 _ => depth,
69 };
70 max = max.max(go_scope_depth(child, child_depth));
71 }
72 }
73 max
74 }
75 go_scope_depth(file.root_node(), 0)
76 }
77
78 fn count_naming_violations(&self, file: &ParsedFile) -> usize {
79 self.count_naming_from_batch(file, &self.batch_captures(file))
80 }
81
82 fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
83 let mut count = 0;
84 count_nested_blocks(file.root_node(), 0, 5, &mut count);
85 count
86 }
87
88 fn count_debug_calls(&self, file: &ParsedFile) -> usize {
89 self.count_debug_from_batch(file, &self.batch_captures(file))
90 }
91
92 fn count_excessive_params(&self, file: &ParsedFile, threshold: usize) -> usize {
93 self.count_excessive_from_batch_with(file, &self.batch_captures(file), threshold)
94 }
95
96 fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
97 self.count_magic_from_batch(file, &self.batch_captures(file))
98 }
99
100 fn count_goroutine_spawns(&self, file: &ParsedFile) -> usize {
101 self.count_goroutine_from_batch(file, &self.batch_captures(file))
102 }
103
104 fn count_defer_in_loop(&self, file: &ParsedFile) -> usize {
105 fn has_defer_child(node: tree_sitter::Node) -> bool {
106 let mut cursor = node.walk();
107 let mut found = cursor.goto_first_child();
108 while found {
109 if cursor.node().kind() == "defer_statement" {
110 return true;
111 }
112 found = cursor.goto_next_sibling();
113 }
114 false
115 }
116
117 fn walk_for_loops(_file: &ParsedFile, node: tree_sitter::Node, count: &mut usize) {
118 if node.kind() == "for_statement" && has_defer_child(node) {
119 *count += 1;
120 }
121 let mut cursor = node.walk();
122 for child in node.children(&mut cursor) {
123 walk_for_loops(_file, child, count);
124 }
125 }
126
127 let mut count = 0;
128 walk_for_loops(file, file.root_node(), &mut count);
129 count
130 }
131
132 fn count_go_convention_violations(&self, file: &ParsedFile) -> usize {
133 self.count_go_convention_from_batch(file, &self.batch_captures(file))
134 }
135
136 fn count_panic_from_batch<'a>(
139 &self,
140 _file: &ParsedFile,
141 batch: &[Vec<QueryCapture<'a>>],
142 ) -> usize {
143 batch
144 .iter()
145 .filter(|m| m.iter().any(|c| c.name == "pc_fn"))
146 .count()
147 }
148
149 fn extract_functions_from_batch<'a>(
150 &self,
151 _file: &ParsedFile,
152 batch: &[Vec<QueryCapture<'a>>],
153 ) -> Vec<FunctionNode> {
154 let mut functions = Vec::new();
155 for m in batch {
156 let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
157 if !has_ex {
158 continue;
159 }
160 let mut name = String::new();
161 let mut start_line = 0usize;
162 let mut end_line = 0usize;
163 for c in m {
164 match c.name.as_str() {
165 "ex_name" => name = c.text.to_string(),
166 "ex_fn" => {
167 start_line = c.node.start_position().row + 1;
168 end_line = c.node.end_position().row + 1;
169 }
170 _ => {}
171 }
172 }
173 if !name.is_empty() {
174 functions.push(FunctionNode {
175 name,
176 start_line,
177 end_line,
178 nesting_depth: 0,
179 });
180 }
181 }
182 functions
183 }
184
185 fn count_naming_from_batch<'a>(
186 &self,
187 file: &ParsedFile,
188 batch: &[Vec<QueryCapture<'a>>],
189 ) -> usize {
190 let mut count = 0usize;
191 static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
192 Regex::new(r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$").ok()
193 });
194 let terrible_re = TERRIBLE_RE.as_ref();
195 let idiomatic_single: &[&str] = &["e", "g", "i", "j", "k", "n", "c"];
196
197 for m in batch {
198 for c in m {
199 match c.name.as_str() {
200 "nv_var" => {
201 let name = c.text;
202 if name.len() == 1 && name.chars().all(|ch| ch.is_ascii_lowercase()) {
203 if !idiomatic_single.contains(&name) {
204 count += 1;
205 }
206 continue;
207 }
208 if let Some(re) = terrible_re {
209 if re.is_match(&name.to_lowercase()) {
210 count += 1;
211 continue;
212 }
213 }
214 if MEANINGLESS_NAMES.contains(&name) || is_repeating_chars(name) {
215 count += 1;
216 }
217 }
218 "nv_rec" if c.text.len() > 2 => {
219 count += 1;
220 }
221 _ => {}
222 }
223 }
224 }
225
226 let go_idioms = [
228 "err", "ok", "ctx", "mu", "wg", "ch", "db", "id", "ip", "tx", "rx", "fd", "fs", "ns",
229 "fn", "hp", "os", "rc",
230 ];
231 for line in file.content.lines() {
232 let trimmed = line.trim();
233 if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
234 continue;
235 }
236 let name = if let Some(rest) = trimmed.strip_prefix("var ") {
237 rest.split_whitespace().next().unwrap_or("")
238 } else if let Some(idx) = trimmed.find(":=") {
239 trimmed[..idx].split_whitespace().last().unwrap_or("")
240 } else {
241 ""
242 };
243 if name.is_empty() || name.len() < 2 || go_idioms.contains(&name) || name == "_" {
244 continue;
245 }
246 if name.chars().next().is_some_and(|ch| ch.is_uppercase()) {
247 continue;
248 }
249 let has_underscore = name.contains('_') && name != "_";
250 let is_all_caps = name
251 .chars()
252 .all(|ch| ch.is_uppercase() || ch == '_' || ch.is_numeric())
253 && name.chars().any(|ch| ch.is_uppercase());
254 if has_underscore || is_all_caps {
255 count += 1;
256 }
257 }
258
259 count
260 }
261
262 fn count_debug_from_batch<'a>(
263 &self,
264 file: &ParsedFile,
265 batch: &[Vec<QueryCapture<'a>>],
266 ) -> usize {
267 let source = file.content.as_bytes();
268 let mut count = 0;
269 for m in batch {
270 let has_dp = m.iter().any(|c| c.name.starts_with("dp_"));
271 if !has_dp {
272 continue;
273 }
274 let mut exempt = false;
275 for c in m {
276 if c.name == "dp_pkg" && c.text == "fmt" {
277 let mut current = Some(c.node);
278 while let Some(n) = current {
279 if n.kind() == "function_declaration" {
280 if let Some(name_node) = n.child_by_field_name("name") {
281 if let Ok(text) = name_node.utf8_text(source) {
282 if text == "main" {
283 exempt = true;
284 break;
285 }
286 }
287 }
288 break;
289 }
290 current = n.parent();
291 }
292 }
293 if exempt {
294 break;
295 }
296 }
297 if !exempt {
298 count += 1;
299 }
300 }
301 count
302 }
303
304 fn count_excessive_from_batch<'a>(
305 &self,
306 _file: &ParsedFile,
307 batch: &[Vec<QueryCapture<'a>>],
308 ) -> usize {
309 self.count_excessive_from_batch_with(_file, batch, 5)
310 }
311
312 fn count_magic_from_batch<'a>(
313 &self,
314 _file: &ParsedFile,
315 batch: &[Vec<QueryCapture<'a>>],
316 ) -> usize {
317 let mut count = 0;
318 for m in batch {
319 for c in m {
320 if c.name == "mn_num" && !is_inside_declaration(c.node) {
321 let text = c.text;
322 if text != "0"
323 && text != "1"
324 && text != "-1"
325 && !is_common_safe_number(text)
326 && !is_boolean_or_null(text)
327 {
328 count += 1;
329 }
330 }
331 }
332 }
333 count
334 }
335
336 fn count_goroutine_from_batch<'a>(
337 &self,
338 _file: &ParsedFile,
339 batch: &[Vec<QueryCapture<'a>>],
340 ) -> usize {
341 batch
342 .iter()
343 .filter(|m| m.iter().any(|c| c.name == "gs_go"))
344 .count()
345 }
346
347 fn count_go_convention_from_batch<'a>(
348 &self,
349 file: &ParsedFile,
350 batch: &[Vec<QueryCapture<'a>>],
351 ) -> usize {
352 let mut count = 0;
353
354 for m in batch {
356 if !m.iter().any(|c| c.name == "cv_method") {
357 continue;
358 }
359 for c in m {
360 if c.name == "cv_method" {
361 if let Some(call_node) = c.node.parent().and_then(|p| p.parent()) {
362 for child in call_node.children(&mut call_node.walk()) {
363 if child.kind() == "argument_list" {
364 let text = file.node_text(child);
365 let trimmed = text.trim();
366 let start = trimmed.find('"');
367 let content = start
368 .map(|s| {
369 let from = &trimmed[s + 1..];
370 from.find('"').map(|e| &from[..e]).unwrap_or("")
371 })
372 .unwrap_or("");
373 if let Some(first) = content.chars().next() {
374 if first.is_uppercase() {
375 count += 1;
376 }
377 }
378 break;
379 }
380 }
381 }
382 }
383 }
384 }
385
386 for line in file.content.lines() {
388 let trimmed = line.trim();
389 if !trimmed.starts_with("func ") {
390 continue;
391 }
392 let params_start = trimmed.find('(');
393 let params_end = trimmed.rfind(')');
394 if let (Some(ps), Some(pe)) = (params_start, params_end) {
395 let params_str = &trimmed[ps + 1..pe];
396 if params_str.contains("context.Context") {
397 let first = params_str.split(',').next().unwrap_or("").trim();
398 if !first.contains("context.Context") {
399 count += 1;
400 }
401 }
402 }
403 }
404
405 fn has_return_statement(n: tree_sitter::Node) -> bool {
407 if n.kind() == "return_statement" {
408 return true;
409 }
410 let mut cursor = n.walk();
411 let mut inner = cursor.goto_first_child();
412 while inner {
413 if cursor.node().kind() == "return_statement" {
414 return true;
415 }
416 inner = cursor.goto_next_sibling();
417 }
418 false
419 }
420
421 fn check_else_return(_file: &ParsedFile, node: tree_sitter::Node, cnt: &mut usize) {
422 if node.kind() == "if_statement" {
423 let mut cx = node.walk();
424 let has_else = node.children(&mut cx).any(|c| c.kind() == "else");
425 if has_else {
426 let mut cx2 = node.walk();
427 for child in node.children(&mut cx2) {
428 if child.kind() == "block" || child.kind() == "compound_statement" {
429 let mut cx3 = child.walk();
430 let has_ret = child.children(&mut cx3).any(has_return_statement);
431 if has_ret {
432 *cnt += 1;
433 break;
434 }
435 }
436 }
437 }
438 }
439 let mut cx4 = node.walk();
440 for child in node.children(&mut cx4) {
441 check_else_return(_file, child, cnt);
442 }
443 }
444 check_else_return(file, file.root_node(), &mut count);
445
446 for m in batch {
447 for c in m {
448 if c.name == "ui_pkg" || c.name == "ui_import" {
449 count += 1;
450 }
451 }
452 }
453
454 count
455 }
456
457 fn count_dead_code(&self, file: &ParsedFile) -> usize {
458 let mut count = 0;
459 let mut dead_start: Option<usize> = None;
460 for (line_num, line) in file.content.lines().enumerate() {
461 let trimmed = line.trim();
462 if trimmed == "return"
463 || trimmed == "return;"
464 || trimmed == "break"
465 || trimmed == "break;"
466 || trimmed == "continue"
467 || trimmed == "continue;"
468 || (trimmed.starts_with("return ")
469 && (trimmed.ends_with(';') || !trimmed.ends_with('}')))
470 || trimmed.starts_with("panic(")
471 || trimmed.starts_with("goto ")
472 {
473 dead_start = Some(line_num + 2);
474 continue;
475 }
476 if let Some(start) = dead_start {
477 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
478 continue;
479 }
480 if trimmed == "}"
481 || trimmed.starts_with("} else")
482 || trimmed.starts_with("} else if")
483 {
484 dead_start = None;
485 continue;
486 }
487 if line_num + 1 >= start {
488 count += 1;
489 dead_start = None;
490 }
491 }
492 }
493 count
494 }
495
496 fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
497 count_duplicate_imports_with(file, &["import ", "import ("])
498 }
499}
500
501impl GoAdapter {
502 fn count_excessive_from_batch_with<'a>(
503 &self,
504 _file: &ParsedFile,
505 batch: &[Vec<QueryCapture<'a>>],
506 threshold: usize,
507 ) -> usize {
508 let mut count = 0;
509 for m in batch {
510 for c in m {
511 if c.name == "ep_params" && count_params(c.text) > threshold {
512 count += 1;
513 }
514 }
515 }
516 count
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::super::parse_code;
523 use super::*;
524
525 fn parse_go(code: &str) -> ParsedFile {
526 parse_code(code, "test.go").expect("parse")
527 }
528
529 #[test]
530 fn test_go_count_panic_calls() {
531 let code = r#"
532package main
533func main() {
534 panic("boom")
535 panic("bang")
536}
537"#;
538 let file = parse_go(code);
539 let adapter = GoAdapter;
540 assert_eq!(adapter.count_panic_calls(&file), 2);
541 }
542
543 #[test]
544 fn test_go_count_panic_calls_clean() {
545 let code = "package main\nfunc main() { println(\"ok\") }\n";
546 let file = parse_go(code);
547 let adapter = GoAdapter;
548 assert_eq!(adapter.count_panic_calls(&file), 0);
549 }
550
551 #[test]
552 fn test_go_extract_functions() {
553 let code = r#"
554package main
555func foo() {}
556func bar(x int) int { return x }
557"#;
558 let file = parse_go(code);
559 let adapter = GoAdapter;
560 let fns = adapter.extract_functions(&file);
561 assert_eq!(fns.len(), 2);
562 assert_eq!(fns[0].name, "foo");
563 assert_eq!(fns[1].name, "bar");
564 }
565
566 #[test]
567 fn test_go_naming_single_letter() {
568 let code = r#"
569package main
570func main() {
571 x := 1
572 y := 2
573}
574"#;
575 let file = parse_go(code);
576 let adapter = GoAdapter;
577 assert_eq!(adapter.count_naming_violations(&file), 2);
578 }
579
580 #[test]
581 fn test_go_debug_fmt_println() {
582 let code = r#"
583package main
584import "fmt"
585func helper() {
586 fmt.Println("hello")
587 fmt.Printf("x=%d", 1)
588}
589"#;
590 let file = parse_go(code);
591 let adapter = GoAdapter;
592 assert_eq!(adapter.count_debug_calls(&file), 2);
593 }
594
595 #[test]
596 fn test_go_excessive_params() {
597 let code = "package main\nfunc process(a, b, c, d, e, f int) {}\n";
598 let file = parse_go(code);
599 let adapter = GoAdapter;
600 assert_eq!(adapter.count_excessive_params(&file, 5), 1);
601 }
602
603 #[test]
604 fn test_go_magic_numbers() {
605 let code = r#"
606package main
607func main() {
608 x := 41 + 1
609 y := x * 100
610}
611"#;
612 let file = parse_go(code);
613 let adapter = GoAdapter;
614 assert_eq!(adapter.count_magic_numbers(&file), 2);
615 }
616
617 #[test]
618 fn test_go_magic_numbers_skips_trivial() {
619 let code = r#"
620package main
621func main() {
622 x := 0 + 1
623}
624"#;
625 let file = parse_go(code);
626 let adapter = GoAdapter;
627 assert_eq!(adapter.count_magic_numbers(&file), 0);
628 }
629
630 #[test]
631 fn test_go_unsafe_pointer() {
632 let code = r#"
633package main
634import "unsafe"
635func main() {
636 p := unsafe.Pointer(nil)
637}
638"#;
639 let file = parse_go(code);
640 let adapter = GoAdapter;
641 assert!(adapter.count_go_convention_violations(&file) >= 2);
642 }
643}