Skip to main content

garbage_code_hunter/language/adapter/
java.rs

1//! JavaAdapter — Java language adapter.
2
3use super::{
4    count_dead_code_with, count_duplicate_imports_with, count_nested_blocks, count_params,
5    is_boolean_or_null, is_common_safe_number, is_inside_declaration, is_repeating_chars,
6    FunctionNode, 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 JAVA_PATTERNS: &[&str] = &[
15    "(throw_statement) @pc_throw",
16    "(method_declaration name: (identifier) @ex_name) @ex_fn",
17    "(variable_declarator name: (identifier) @nv_var)",
18    "(method_invocation name: (identifier) @dp_method (#match? @dp_method \"^(println|printStackTrace)$\"))",
19    "(method_declaration parameters: (formal_parameters) @ep_params)",
20    "[(decimal_integer_literal) @mn_num (decimal_floating_point_literal) @mn_num]",
21];
22
23fn find_empty_catch(node: tree_sitter::Node, count: &mut usize) {
24    if node.kind() == "catch_clause" {
25        if let Some(body) = node.child_by_field_name("body") {
26            if body.named_child_count() == 0 {
27                *count += 1;
28            }
29        }
30    }
31    for i in 0..node.child_count() {
32        if let Some(child) = node.child(i as u32) {
33            find_empty_catch(child, count);
34        }
35    }
36}
37
38pub struct JavaAdapter;
39
40impl LanguageAdapter for JavaAdapter {
41    fn language(&self) -> Language {
42        Language::Java
43    }
44
45    fn query_patterns(&self) -> &[&str] {
46        JAVA_PATTERNS
47    }
48
49    fn count_panic_calls(&self, file: &ParsedFile) -> usize {
50        self.count_panic_from_batch(file, &self.batch_captures(file))
51    }
52
53    fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
54        self.extract_functions_from_batch(file, &self.batch_captures(file))
55    }
56
57    fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
58        fn java_scope_depth(node: tree_sitter::Node, depth: usize) -> usize {
59            let mut max = depth;
60            for i in 0..node.child_count() {
61                if let Some(child) = node.child(i as u32) {
62                    let child_depth = match child.kind() {
63                        "block" => depth + 1,
64                        _ => depth,
65                    };
66                    max = max.max(java_scope_depth(child, child_depth));
67                }
68            }
69            max
70        }
71        java_scope_depth(file.root_node(), 0)
72    }
73
74    fn count_naming_violations(&self, file: &ParsedFile) -> usize {
75        self.count_naming_from_batch(file, &self.batch_captures(file))
76    }
77
78    fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
79        let mut count = 0;
80        count_nested_blocks(file.root_node(), 0, 5, &mut count);
81        count
82    }
83
84    fn count_debug_calls(&self, file: &ParsedFile) -> usize {
85        self.count_debug_from_batch(file, &self.batch_captures(file))
86    }
87
88    fn count_excessive_params(&self, file: &ParsedFile, threshold: usize) -> usize {
89        self.count_excessive_from_batch_with(file, &self.batch_captures(file), threshold)
90    }
91
92    fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
93        self.count_magic_from_batch(file, &self.batch_captures(file))
94    }
95
96    fn count_dead_code(&self, file: &ParsedFile) -> usize {
97        count_dead_code_with(
98            file,
99            &["return;", "break;", "continue;"],
100            &["return ", "throw ", "System.exit("],
101            "//",
102        )
103    }
104
105    fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
106        count_duplicate_imports_with(file, &["import "])
107    }
108
109    fn count_java_issues(&self, file: &ParsedFile) -> usize {
110        let mut count = 0;
111
112        // empty-catch: catch blocks with no named children in body
113        find_empty_catch(file.root_node(), &mut count);
114
115        let lines: Vec<&str> = file.content.lines().collect();
116
117        // java-javadoc-missing: public/protected methods without Javadoc
118        let mut i = 0;
119        while i < lines.len() {
120            let trimmed = lines[i].trim();
121            if (trimmed.starts_with("public ") || trimmed.starts_with("protected "))
122                && trimmed.contains("(")
123                && (trimmed.contains(")")
124                    || lines.get(i + 1).is_some_and(|l| l.trim().contains(")")))
125            {
126                let mut j = i as i32 - 1;
127                while j >= 0 && lines[j as usize].trim().is_empty() {
128                    j -= 1;
129                }
130                let has_javadoc = j >= 0
131                    && (lines[j as usize].trim().starts_with("/**")
132                        || lines[j as usize].trim().ends_with("*/"));
133                let has_annotation = j >= 0
134                    && (lines[j as usize].trim().starts_with("@Override")
135                        || lines[j as usize].trim().starts_with("@Suppress")
136                        || lines[j as usize].trim().starts_with("@"));
137                if !has_javadoc && !has_annotation {
138                    count += 1;
139                }
140            }
141            i += 1;
142        }
143
144        // java-try-resource: finally with .close() within 3 lines
145        for (line_num, line) in lines.iter().enumerate() {
146            let trimmed = line.trim();
147            if trimmed.contains("finally") {
148                for k in 1..=3 {
149                    if lines
150                        .get(line_num + k)
151                        .is_some_and(|n| n.trim().contains(".close()"))
152                    {
153                        count += 1;
154                        break;
155                    }
156                }
157            }
158        }
159
160        // java-string-concat: += within 10 lines of for/while
161        let has_loop = file.content.contains("for ") || file.content.contains("while ");
162        if has_loop {
163            for (line_num, line) in file.content.lines().enumerate() {
164                let trimmed = line.trim();
165                if trimmed.contains(" += ") {
166                    let start = line_num.saturating_sub(10);
167                    for k in (start..line_num).rev() {
168                        let prev = lines[k].trim();
169                        if prev.starts_with("for ") || prev.starts_with("while ") {
170                            count += 1;
171                            break;
172                        }
173                    }
174                }
175            }
176        }
177
178        // java-wildcard-import: import ...*;
179        for line in &lines {
180            if line.trim().starts_with("import ") && line.trim().ends_with(".*;") {
181                count += 1;
182            }
183        }
184
185        count
186    }
187
188    fn count_panic_from_batch<'a>(
189        &self,
190        _file: &ParsedFile,
191        batch: &[Vec<QueryCapture<'a>>],
192    ) -> usize {
193        batch
194            .iter()
195            .filter(|m| m.iter().any(|c| c.name == "pc_throw"))
196            .count()
197    }
198
199    fn extract_functions_from_batch<'a>(
200        &self,
201        _file: &ParsedFile,
202        batch: &[Vec<QueryCapture<'a>>],
203    ) -> Vec<FunctionNode> {
204        let mut functions = Vec::new();
205        for m in batch {
206            let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
207            if !has_ex {
208                continue;
209            }
210            let mut name = String::new();
211            let mut start_line = 0usize;
212            let mut end_line = 0usize;
213            for c in m {
214                match c.name.as_str() {
215                    "ex_name" => name = c.text.to_string(),
216                    "ex_fn" => {
217                        start_line = c.node.start_position().row + 1;
218                        end_line = c.node.end_position().row + 1;
219                    }
220                    _ => {}
221                }
222            }
223            if !name.is_empty() {
224                functions.push(FunctionNode {
225                    name,
226                    start_line,
227                    end_line,
228                    nesting_depth: 0,
229                });
230            }
231        }
232        functions
233    }
234
235    fn count_naming_from_batch<'a>(
236        &self,
237        file: &ParsedFile,
238        batch: &[Vec<QueryCapture<'a>>],
239    ) -> usize {
240        let mut count = 0usize;
241        static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
242            Regex::new(r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$").ok()
243        });
244        let terrible_re = TERRIBLE_RE.as_ref();
245        let idiomatic_single: &[&str] = &["i", "j", "k", "e", "n"];
246
247        for m in batch {
248            for c in m {
249                if c.name == "nv_var" {
250                    let name = c.text;
251                    if name.len() == 1 && name.chars().all(|ch| ch.is_ascii_lowercase()) {
252                        if !idiomatic_single.contains(&name) {
253                            count += 1;
254                        }
255                        continue;
256                    }
257                    if let Some(re) = terrible_re {
258                        if re.is_match(&name.to_lowercase()) {
259                            count += 1;
260                            continue;
261                        }
262                    }
263                    if MEANINGLESS_NAMES.contains(&name) || is_repeating_chars(name) {
264                        count += 1;
265                        continue;
266                    }
267                }
268            }
269        }
270
271        // constant-name: static final fields should be UPPER_SNAKE_CASE
272        for line in file.content.lines() {
273            let trimmed = line.trim();
274            if !trimmed.contains("static final") && !trimmed.contains("final static") {
275                continue;
276            }
277            let parts: Vec<&str> = trimmed.split_whitespace().collect();
278            let name = parts
279                .iter()
280                .position(|p| *p == "=" || p.ends_with('=') || p.ends_with(';'))
281                .and_then(|idx| {
282                    if idx > 0 {
283                        parts
284                            .get(idx - 1)
285                            .map(|s| s.trim_end_matches('=').trim_end_matches(';'))
286                    } else {
287                        None
288                    }
289                })
290                .unwrap_or("");
291            if !name.is_empty()
292                && name != name.to_uppercase()
293                && name.chars().all(|c| c.is_alphanumeric() || c == '_')
294            {
295                count += 1;
296            }
297        }
298
299        count
300    }
301
302    fn count_debug_from_batch<'a>(
303        &self,
304        file: &ParsedFile,
305        batch: &[Vec<QueryCapture<'a>>],
306    ) -> usize {
307        let base = batch
308            .iter()
309            .filter(|m| m.iter().any(|c| c.name == "dp_method"))
310            .count();
311        let log_calls = file
312            .content
313            .lines()
314            .filter(|l| {
315                let t = l.trim();
316                if t.starts_with("//") || t.starts_with("/*") || t.starts_with("*") {
317                    return false;
318                }
319                t.contains(".info(")
320                    || t.contains(".debug(")
321                    || t.contains(".warn(")
322                    || t.contains(".error(")
323                    || t.contains(".fine(")
324                    || t.contains(".finest(")
325                    || t.contains(".severe(")
326            })
327            .count();
328        base + log_calls
329    }
330
331    fn count_excessive_from_batch<'a>(
332        &self,
333        _file: &ParsedFile,
334        batch: &[Vec<QueryCapture<'a>>],
335    ) -> usize {
336        self.count_excessive_from_batch_with(_file, batch, 5)
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 JavaAdapter {
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_java(code: &str) -> ParsedFile {
389        parse_code(code, "Test.java").expect("parse")
390    }
391
392    #[test]
393    fn test_java_count_panic_throw() {
394        let code = r#"
395class Test {
396    void main() {
397        throw new RuntimeException("boom");
398        throw new Exception("bang");
399    }
400}
401"#;
402        let file = parse_java(code);
403        let adapter = JavaAdapter;
404        assert_eq!(adapter.count_panic_calls(&file), 2);
405    }
406
407    #[test]
408    fn test_java_count_panic_clean() {
409        let code = r#"
410class Test {
411    void main() {
412        return;
413    }
414}
415"#;
416        let file = parse_java(code);
417        let adapter = JavaAdapter;
418        assert_eq!(adapter.count_panic_calls(&file), 0);
419    }
420
421    #[test]
422    fn test_java_extract_functions() {
423        let code = r#"
424class Test {
425    void foo() {}
426    void bar(int x) {}
427}
428"#;
429        let file = parse_java(code);
430        let adapter = JavaAdapter;
431        let fns = adapter.extract_functions(&file);
432        assert_eq!(fns.len(), 2);
433        assert_eq!(fns[0].name, "foo");
434        assert_eq!(fns[1].name, "bar");
435    }
436
437    #[test]
438    fn test_java_naming_single_letter() {
439        let code = r#"
440class Test {
441    void main() {
442        int x = 1;
443        int y = 2;
444    }
445}
446"#;
447        let file = parse_java(code);
448        let adapter = JavaAdapter;
449        assert_eq!(adapter.count_naming_violations(&file), 2);
450    }
451
452    #[test]
453    fn test_java_debug_sout() {
454        let code = r#"
455class Test {
456    void main() {
457        System.out.println("hello");
458        System.err.println("bad");
459    }
460}
461"#;
462        let file = parse_java(code);
463        let adapter = JavaAdapter;
464        assert_eq!(adapter.count_debug_calls(&file), 2);
465    }
466
467    #[test]
468    fn test_java_debug_print_stack_trace() {
469        let code = r#"
470class Test {
471    void main() {
472        e.printStackTrace();
473    }
474}
475"#;
476        let file = parse_java(code);
477        let adapter = JavaAdapter;
478        assert_eq!(adapter.count_debug_calls(&file), 1);
479    }
480
481    #[test]
482    fn test_java_excessive_params() {
483        let code = r#"
484class Test {
485    void process(int a, int b, int c, int d, int e, int f) {}
486}
487"#;
488        let file = parse_java(code);
489        let adapter = JavaAdapter;
490        assert_eq!(adapter.count_excessive_params(&file, 5), 1);
491    }
492
493    #[test]
494    fn test_java_magic_numbers() {
495        let code = r#"
496class Test {
497    void main() {
498        foo(42);
499        bar(100);
500    }
501}
502"#;
503        let file = parse_java(code);
504        let adapter = JavaAdapter;
505        assert_eq!(adapter.count_magic_numbers(&file), 2);
506    }
507
508    #[test]
509    fn test_java_magic_numbers_skips_trivial() {
510        let code = r#"
511class Test {
512    void main() {
513        foo(0);
514        bar(1);
515    }
516}
517"#;
518        let file = parse_java(code);
519        let adapter = JavaAdapter;
520        assert_eq!(adapter.count_magic_numbers(&file), 0);
521    }
522
523    #[test]
524    fn test_java_dead_code_after_return() {
525        let code = r#"
526void foo() {
527    return;
528    System.out.println("dead");
529}
530"#;
531        let file = parse_java(code);
532        let adapter = JavaAdapter;
533        assert_eq!(adapter.count_dead_code(&file), 1);
534    }
535
536    #[test]
537    fn test_java_debug_logging() {
538        let code = r#"
539class Test {
540    void main() {
541        logger.info("started");
542        log.debug("step 1");
543        log.error("failed");
544    }
545}
546"#;
547        let file = parse_java(code);
548        let adapter = JavaAdapter;
549        assert_eq!(adapter.count_debug_calls(&file), 3);
550    }
551}