Skip to main content

garbage_code_hunter/language/adapter/
rust.rs

1//! RustAdapter — Rust language adapter.
2
3use super::{
4    count_block_ancestors, count_nested_blocks, count_params, is_boolean_or_null,
5    is_common_safe_number, is_inside_declaration, is_repeating_chars, max_scope_depth,
6    FunctionNode, LanguageAdapter,
7};
8use crate::language::Language;
9use crate::treesitter::engine::ParsedFile;
10use crate::treesitter::query::QueryCapture;
11use regex::Regex;
12use std::sync::LazyLock;
13
14/// Returns byte ranges of `#[cfg(test)]` modules in Rust source.
15/// Each range covers from the opening brace of the module block to its closing brace.
16fn cfg_test_ranges(content: &str) -> Vec<(usize, usize)> {
17    let mut ranges = Vec::new();
18    let mut search_from = 0;
19    while let Some(attr_pos) = content[search_from..].find("#[cfg(test)]") {
20        let attr_start = search_from + attr_pos;
21        let after_attr = attr_start + "#[cfg(test)]".len();
22        if let Some(brace_offset) = content[after_attr..].find('{') {
23            let open_brace = after_attr + brace_offset;
24            let mut depth = 1i32;
25            let mut j = open_brace + 1;
26            for ch in content[open_brace + 1..].chars() {
27                match ch {
28                    '{' => depth += 1,
29                    '}' => depth -= 1,
30                    _ => {}
31                }
32                j += ch.len_utf8();
33                if depth == 0 {
34                    break;
35                }
36            }
37            if depth == 0 {
38                ranges.push((open_brace, j));
39            }
40            search_from = j;
41        } else {
42            search_from = after_attr;
43        }
44    }
45    ranges
46}
47
48const RUST_PATTERNS: &[&str] = &[
49    "(field_expression field: (field_identifier) @pc_method (#eq? @pc_method \"unwrap\"))",
50    "(macro_invocation macro: (identifier) @pc_m)",
51    "(function_item name: (identifier) @ex_name) @ex_fn",
52    "(let_declaration pattern: (identifier) @nv_var (#match? @nv_var \"^[a-z]$\"))",
53    "(let_declaration pattern: (identifier) @nv_name)",
54    "(identifier) @nv_id",
55    "(macro_invocation macro: (identifier) @dp_name (#match? @dp_name \"^(println|dbg|eprintln|eprint|todo|unimplemented)$\"))",
56    "(function_item parameters: (parameters) @ep_params)",
57    "(unsafe_block) @ub_unsafe",
58    "(integer_literal) @mn_num",
59];
60
61pub struct RustAdapter;
62
63impl LanguageAdapter for RustAdapter {
64    fn language(&self) -> Language {
65        Language::Rust
66    }
67
68    fn query_patterns(&self) -> &[&str] {
69        RUST_PATTERNS
70    }
71
72    fn count_panic_calls(&self, file: &ParsedFile) -> usize {
73        self.count_panic_from_batch(file, &self.batch_captures(file))
74    }
75
76    fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
77        self.extract_functions_from_batch(file, &self.batch_captures(file))
78    }
79
80    fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
81        max_scope_depth(file.root_node(), 0)
82    }
83
84    fn count_naming_violations(&self, file: &ParsedFile) -> usize {
85        self.count_naming_from_batch(file, &self.batch_captures(file))
86    }
87
88    fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
89        let threshold = 5;
90        let mut count = 0;
91        count_nested_blocks(file.root_node(), 0, threshold, &mut count);
92        count
93    }
94
95    fn count_debug_calls(&self, file: &ParsedFile) -> usize {
96        self.count_debug_from_batch(file, &self.batch_captures(file))
97    }
98
99    fn count_excessive_params(&self, file: &ParsedFile, threshold: usize) -> usize {
100        self.count_excessive_from_batch_with(file, &self.batch_captures(file), threshold)
101    }
102
103    fn count_unsafe_blocks(&self, file: &ParsedFile) -> usize {
104        self.count_unsafe_from_batch(file, &self.batch_captures(file))
105    }
106
107    fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
108        self.count_magic_from_batch(file, &self.batch_captures(file))
109    }
110
111    fn count_dead_code(&self, file: &ParsedFile) -> usize {
112        let mut count = 0;
113        let mut dead_start: Option<usize> = None;
114        for (line_num, line) in file.content.lines().enumerate() {
115            let trimmed = line.trim();
116            if matches!(
117                trimmed,
118                "return;" | "break;" | "continue;" | "unreachable!()" | "unreachable!();"
119            ) || (trimmed.starts_with("return ") && trimmed.ends_with(';'))
120                || (trimmed.starts_with("panic!(") && trimmed.ends_with(';'))
121                || (trimmed.starts_with("unreachable!(") && trimmed.ends_with(')'))
122            {
123                dead_start = Some(line_num + 2);
124                continue;
125            }
126            if let Some(start) = dead_start {
127                if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
128                    continue;
129                }
130                if trimmed == "}"
131                    || trimmed.starts_with("} else")
132                    || trimmed.starts_with("} else if")
133                {
134                    dead_start = None;
135                    continue;
136                }
137                if line_num + 1 >= start {
138                    count += 1;
139                    dead_start = None;
140                }
141            }
142        }
143        count
144    }
145
146    fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
147        super::count_duplicate_imports_with(file, &["use "])
148    }
149
150    fn count_panic_from_batch<'a>(
151        &self,
152        file: &ParsedFile,
153        batch: &[Vec<QueryCapture<'a>>],
154    ) -> usize {
155        let test_ranges = cfg_test_ranges(&file.content);
156        let mut count = 0;
157        for m in batch {
158            for c in m {
159                if (c.name == "pc_method" && c.text == "unwrap")
160                    || (c.name == "pc_m"
161                        && matches!(c.text, "panic" | "assert" | "assert_eq" | "assert_ne"))
162                {
163                    let byte_offset = c.node.start_byte();
164                    if test_ranges
165                        .iter()
166                        .any(|&(s, e)| byte_offset >= s && byte_offset < e)
167                    {
168                        continue;
169                    }
170                    count += 1;
171                }
172            }
173        }
174        count
175    }
176
177    fn extract_functions_from_batch<'a>(
178        &self,
179        _file: &ParsedFile,
180        batch: &[Vec<QueryCapture<'a>>],
181    ) -> Vec<FunctionNode> {
182        let mut functions = Vec::new();
183        for m in batch {
184            let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
185            if !has_ex {
186                continue;
187            }
188            let mut name = String::new();
189            let mut start_line = 0usize;
190            let mut end_line = 0usize;
191            for c in m {
192                match c.name.as_str() {
193                    "ex_name" => name = c.text.to_string(),
194                    "ex_fn" => {
195                        start_line = c.node.start_position().row + 1;
196                        end_line = c.node.end_position().row + 1;
197                    }
198                    _ => {}
199                }
200            }
201            if !name.is_empty() {
202                let nesting_depth = count_block_ancestors(m);
203                functions.push(FunctionNode {
204                    name,
205                    start_line,
206                    end_line,
207                    nesting_depth,
208                });
209            }
210        }
211        functions
212    }
213
214    fn count_naming_from_batch<'a>(
215        &self,
216        _file: &ParsedFile,
217        batch: &[Vec<QueryCapture<'a>>],
218    ) -> usize {
219        let mut count = 0usize;
220        let idiomatic_single: &[&str] = &["i", "j", "k", "n", "c", "e", "x", "t", "f"];
221
222        static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
223            Regex::new(
224                r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$",
225            ).ok()
226        });
227        let terrible_re = TERRIBLE_RE.as_ref();
228        let meaningless: &[&str] = &[
229            "foo", "bar", "baz", "qux", "quux", "quuz", "aaa", "bbb", "ccc", "ddd", "eee", "xxx",
230            "yyy", "zzz", "test1", "test2", "test3",
231        ];
232
233        let hungarian_prefixes: &[&str] = &[
234            "str", "int", "bool", "float", "double", "char", "arr", "vec", "list", "map", "set",
235        ];
236        let scope_prefixes: &[&str] = &["g_", "m_", "s_", "p_"];
237        let domain_prefixes: &[&str] = &[
238            "ctx", "req", "res", "err", "db", "kv", "fs", "io", "api", "http", "html", "ssh",
239            "tls", "uid", "uri", "url",
240        ];
241        let bad_abbrevs: &[&str] = &[
242            "mgr", "mngr", "ctrl", "hdlr", "usr", "pwd", "prefs", "btn", "lbl", "pic", "tbl",
243            "col", "cnt",
244        ];
245
246        for m in batch {
247            for c in m {
248                match c.name.as_str() {
249                    "nv_var" if !idiomatic_single.contains(&c.text) => {
250                        count += 1;
251                    }
252                    "nv_name" => {
253                        let name = c.text;
254                        let name_lower = name.to_lowercase();
255                        if let Some(re) = terrible_re {
256                            if re.is_match(&name_lower) {
257                                count += 1;
258                                continue;
259                            }
260                        }
261                        if meaningless.contains(&name) || is_repeating_chars(name) {
262                            count += 1;
263                        }
264                    }
265                    "nv_id" => {
266                        if count > 2000 {
267                            continue;
268                        }
269                        let name = c.text;
270                        let name_lower = name.to_lowercase();
271                        if domain_prefixes.iter().any(|p| name_lower.starts_with(p)) {
272                            continue;
273                        }
274                        if scope_prefixes.iter().any(|p| name_lower.starts_with(p))
275                            || hungarian_prefixes.iter().any(|p| {
276                                name_lower.starts_with(p)
277                                    && name.len() > p.len()
278                                    && name.as_bytes()[p.len()].is_ascii_uppercase()
279                            })
280                        {
281                            count += 1;
282                            continue;
283                        }
284                        if bad_abbrevs
285                            .iter()
286                            .any(|a| name_lower == *a || name_lower.starts_with(&format!("{}_", a)))
287                        {
288                            count += 1;
289                        }
290                    }
291                    _ => {}
292                }
293            }
294        }
295        count
296    }
297
298    fn count_debug_from_batch<'a>(
299        &self,
300        file: &ParsedFile,
301        batch: &[Vec<QueryCapture<'a>>],
302    ) -> usize {
303        let test_ranges = cfg_test_ranges(&file.content);
304        batch
305            .iter()
306            .filter(|m| {
307                m.iter().any(|c| {
308                    if c.name != "dp_name" {
309                        return false;
310                    }
311                    let byte_offset = c.node.start_byte();
312                    !test_ranges
313                        .iter()
314                        .any(|&(s, e)| byte_offset >= s && byte_offset < e)
315                })
316            })
317            .count()
318    }
319
320    fn count_excessive_from_batch<'a>(
321        &self,
322        _file: &ParsedFile,
323        batch: &[Vec<QueryCapture<'a>>],
324    ) -> usize {
325        self.count_excessive_from_batch_with(_file, batch, 5)
326    }
327
328    fn count_unsafe_from_batch<'a>(
329        &self,
330        _file: &ParsedFile,
331        batch: &[Vec<QueryCapture<'a>>],
332    ) -> usize {
333        batch
334            .iter()
335            .filter(|m| m.iter().any(|c| c.name == "ub_unsafe"))
336            .count()
337    }
338
339    fn count_magic_from_batch<'a>(
340        &self,
341        _file: &ParsedFile,
342        batch: &[Vec<QueryCapture<'a>>],
343    ) -> usize {
344        let mut count = 0;
345        for m in batch {
346            for c in m {
347                if c.name == "mn_num" && !is_inside_declaration(c.node) {
348                    let text = c.text;
349                    if text != "0"
350                        && text != "1"
351                        && text != "-1"
352                        && !is_common_safe_number(text)
353                        && !is_boolean_or_null(text)
354                    {
355                        count += 1;
356                    }
357                }
358            }
359        }
360        count
361    }
362}
363
364impl RustAdapter {
365    fn count_excessive_from_batch_with<'a>(
366        &self,
367        _file: &ParsedFile,
368        batch: &[Vec<QueryCapture<'a>>],
369        threshold: usize,
370    ) -> usize {
371        let mut count = 0;
372        for m in batch {
373            for c in m {
374                if c.name == "ep_params" && count_params(c.text) > threshold {
375                    count += 1;
376                }
377            }
378        }
379        count
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::super::parse_code;
386    use super::*;
387
388    fn parse_rust(code: &str) -> ParsedFile {
389        parse_code(code, "test.rs").expect("parse")
390    }
391
392    #[test]
393    fn test_rust_count_panic_unwrap_only() {
394        let code = "fn main() { let x = foo().unwrap(); let y = bar().expect(\"msg\"); }";
395        let file = parse_rust(code);
396        let adapter = RustAdapter;
397        assert_eq!(adapter.count_panic_calls(&file), 1);
398    }
399
400    #[test]
401    fn test_rust_count_panic_macro() {
402        let code = "fn main() { panic!(\"boom\"); }";
403        let file = parse_rust(code);
404        let adapter = RustAdapter;
405        assert_eq!(adapter.count_panic_calls(&file), 1);
406    }
407
408    #[test]
409    fn test_rust_count_panic_clean() {
410        let code = "fn main() { let x = 42; }";
411        let file = parse_rust(code);
412        let adapter = RustAdapter;
413        assert_eq!(adapter.count_panic_calls(&file), 0);
414    }
415
416    #[test]
417    fn test_rust_extract_functions() {
418        let code = r#"
419fn foo() {}
420fn bar(x: i32) -> i32 { x + 1 }
421"#;
422        let file = parse_rust(code);
423        let adapter = RustAdapter;
424        let fns = adapter.extract_functions(&file);
425        assert_eq!(fns.len(), 2, "should find 2 functions");
426        assert_eq!(fns[0].name, "foo");
427        assert_eq!(fns[1].name, "bar");
428        assert!(fns[0].start_line < fns[1].start_line, "foo before bar");
429    }
430
431    #[test]
432    fn test_rust_max_nesting_depth_flat() {
433        let code = "fn main() { let x = 1; }";
434        let file = parse_rust(code);
435        let adapter = RustAdapter;
436        assert_eq!(adapter.max_nesting_depth(&file), 1);
437    }
438
439    #[test]
440    fn test_rust_max_nesting_depth_nested() {
441        let code = r#"
442fn main() {
443    if true {
444        for i in 0..10 {
445            let x = i;
446        }
447    }
448}
449"#;
450        let file = parse_rust(code);
451        let adapter = RustAdapter;
452        let depth = adapter.max_nesting_depth(&file);
453        assert!(
454            depth >= 2,
455            "nested if+for should have depth >= 2, got {depth}"
456        );
457    }
458
459    #[test]
460    fn test_rust_max_nesting_depth_empty() {
461        let code = "";
462        let file = parse_rust(code);
463        let adapter = RustAdapter;
464        assert_eq!(adapter.max_nesting_depth(&file), 0);
465    }
466
467    #[test]
468    fn test_naming_single_letter() {
469        let code = "fn main() { let a = 1; let bb = 2; }";
470        let file = parse_rust(code);
471        let adapter = RustAdapter;
472        assert_eq!(adapter.count_naming_violations(&file), 1);
473    }
474
475    #[test]
476    fn test_naming_terrible() {
477        let code = "fn main() { let data = 1; let manager = 2; }";
478        let file = parse_rust(code);
479        let adapter = RustAdapter;
480        assert_eq!(adapter.count_naming_violations(&file), 2);
481    }
482
483    #[test]
484    fn test_naming_meaningless() {
485        let code = "fn main() { let foo = 1; let aaa = 2; }";
486        let file = parse_rust(code);
487        let adapter = RustAdapter;
488        assert_eq!(adapter.count_naming_violations(&file), 2);
489    }
490
491    #[test]
492    fn test_naming_hungarian() {
493        let code = "fn main() { let strName = \"hello\"; let g_count = 0; }";
494        let file = parse_rust(code);
495        let adapter = RustAdapter;
496        assert_eq!(adapter.count_naming_violations(&file), 2);
497    }
498
499    #[test]
500    fn test_naming_hungarian_exempts_domain_prefixes() {
501        let code = "fn main() { let ctxUser = 1; let dbQuery = 2; let kvStore = 3; }";
502        let file = parse_rust(code);
503        let adapter = RustAdapter;
504        assert_eq!(adapter.count_naming_violations(&file), 0);
505    }
506
507    #[test]
508    fn test_naming_abbreviation() {
509        let code = "fn main() { let mgr = \"boss\"; let btn_submit = true; }";
510        let file = parse_rust(code);
511        let adapter = RustAdapter;
512        assert_eq!(adapter.count_naming_violations(&file), 2);
513    }
514
515    #[test]
516    fn test_naming_clean() {
517        let code = "fn main() { let user_name = \"alice\"; let item_count = 42; }";
518        let file = parse_rust(code);
519        let adapter = RustAdapter;
520        assert_eq!(adapter.count_naming_violations(&file), 0);
521    }
522
523    #[test]
524    fn test_rust_count_unsafe_blocks() {
525        let code = r#"
526fn main() {
527    unsafe {
528        let p = 42 as *const i32;
529    }
530    unsafe {
531        let _ = 0usize;
532    }
533}
534"#;
535        let file = parse_rust(code);
536        let adapter = RustAdapter;
537        assert_eq!(adapter.count_unsafe_blocks(&file), 2);
538    }
539
540    #[test]
541    fn test_rust_count_unsafe_blocks_clean() {
542        let code = "fn main() { let x = 42; }";
543        let file = parse_rust(code);
544        let adapter = RustAdapter;
545        assert_eq!(adapter.count_unsafe_blocks(&file), 0);
546    }
547
548    #[test]
549    fn test_rust_count_magic_numbers() {
550        let code = r#"
551fn main() {
552    let x = 1;
553    foo(42);
554    bar(100);
555}
556"#;
557        let file = parse_rust(code);
558        let adapter = RustAdapter;
559        assert_eq!(adapter.count_magic_numbers(&file), 2);
560    }
561
562    #[test]
563    fn test_rust_count_magic_numbers_const_ok() {
564        let code = r#"
565const MAX: i32 = 100;
566fn main() {
567    let x = MAX;
568}
569"#;
570        let file = parse_rust(code);
571        let adapter = RustAdapter;
572        assert_eq!(adapter.count_magic_numbers(&file), 0);
573    }
574
575    #[test]
576    fn test_rust_count_magic_numbers_skips_trivial() {
577        let code = r#"
578fn main() {
579    let x = 0;
580    let y = x + 1;
581}
582"#;
583        let file = parse_rust(code);
584        let adapter = RustAdapter;
585        assert_eq!(adapter.count_magic_numbers(&file), 0);
586    }
587
588    #[test]
589    fn test_rust_compute_all() {
590        let code = r#"
591fn main() {
592    let x = foo().unwrap();
593    panic!("boom");
594    println!("debug");
595    unsafe { let p = 42 as *const i32; }
596    foo(100);
597}
598"#;
599        let file = parse_rust(code);
600        let adapter = RustAdapter;
601        let counts = adapter.compute_all(&file);
602        assert!(counts.panic_calls >= 2);
603        assert!(counts.debug_calls >= 1);
604        assert!(counts.unsafe_blocks >= 1);
605        assert!(counts.magic_numbers >= 1);
606    }
607}