garbage_code_hunter/language/adapter/
java.rs1use 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 find_empty_catch(file.root_node(), &mut count);
114
115 let lines: Vec<&str> = file.content.lines().collect();
116
117 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 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 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 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 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}