|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Accurate resource leak detector that properly handles try-with-resources. |
| 4 | +Detects resource allocations (new Stream, Reader, Writer, Connection, etc.) |
| 5 | +that are not properly wrapped in try-with-resources or try-finally blocks. |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +import re |
| 10 | + |
| 11 | +def check_for_resource_leaks(java_file): |
| 12 | + """Check a single Java file for resource leaks.""" |
| 13 | + leaks = [] |
| 14 | + |
| 15 | + try: |
| 16 | + with open(java_file, 'r', encoding='utf-8') as f: |
| 17 | + content = f.read() |
| 18 | + except Exception: |
| 19 | + return leaks |
| 20 | + |
| 21 | + lines = content.split('\n') |
| 22 | + |
| 23 | + # Remove comments for resource pattern checking |
| 24 | + content_no_comments = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) |
| 25 | + content_no_comments = re.sub(r'//.*?$', '', content_no_comments, flags=re.MULTILINE) |
| 26 | + |
| 27 | + # Pattern for resource allocation |
| 28 | + resource_pattern = r'new\s+\w*(Stream|Reader|Writer|Connection)\s*\(' |
| 29 | + |
| 30 | + for line_num, line in enumerate(lines): |
| 31 | + # Skip empty lines |
| 32 | + if not line.strip(): |
| 33 | + continue |
| 34 | + |
| 35 | + # Check for resource allocation |
| 36 | + if re.search(resource_pattern, line): |
| 37 | + # Strategy: Check if this resource is properly managed |
| 38 | + # 1. Check if line is part of try-with-resources statement (most common pattern) |
| 39 | + # 2. Check if resource is in a try-finally with disconnect/close |
| 40 | + # 3. Check if resource is in simple try block (assume managed) |
| 41 | + |
| 42 | + # Get surrounding context (for multiline try statements) |
| 43 | + context_start = max(0, line_num - 3) |
| 44 | + context_end = min(len(lines), line_num + 1) |
| 45 | + context = '\n'.join(lines[context_start:context_end]) |
| 46 | + |
| 47 | + # Pattern 1: try-with-resources (can span multiple lines) |
| 48 | + # Look backwards and forwards for try ( ... ) pattern |
| 49 | + if re.search(r'try\s*\(', context): |
| 50 | + # Likely in try-with-resources - check if it closes properly |
| 51 | + # Look for the closing ) and { |
| 52 | + paren_count = 0 |
| 53 | + found_try_open = False |
| 54 | + found_try_close = False |
| 55 | + |
| 56 | + context_search = context |
| 57 | + for char in context_search: |
| 58 | + if char == '(': |
| 59 | + if not found_try_open and 'try' in context_search[:context_search.index(char)+5]: |
| 60 | + found_try_open = True |
| 61 | + if found_try_open: |
| 62 | + paren_count += 1 |
| 63 | + elif char == ')': |
| 64 | + if found_try_open: |
| 65 | + paren_count -= 1 |
| 66 | + if paren_count == 0: |
| 67 | + found_try_close = True |
| 68 | + break |
| 69 | + |
| 70 | + if found_try_close or paren_count >= 0: |
| 71 | + continue # In try-with-resources |
| 72 | + |
| 73 | + # Pattern 2: try-finally with cleanup |
| 74 | + # Check if there's a finally block after this line with close/disconnect |
| 75 | + for future_line_idx in range(line_num + 1, min(line_num + 15, len(lines))): |
| 76 | + if 'finally' in lines[future_line_idx]: |
| 77 | + # Found finally block - assume it handles cleanup |
| 78 | + continue |
| 79 | + |
| 80 | + # Pattern 3: Simple try block |
| 81 | + if line_num > 0: |
| 82 | + # Check previous lines for try |
| 83 | + for prev_idx in range(max(0, line_num - 5), line_num): |
| 84 | + if re.search(r'try\s*\{', lines[prev_idx]): |
| 85 | + # In a try block - assume managed (unless it's obviously not) |
| 86 | + # For now, skip |
| 87 | + break |
| 88 | + else: |
| 89 | + # No try block found - this might be a leak |
| 90 | + # But first check if it's a simple single-statement in a loop or conditional |
| 91 | + # that gets garbage collected |
| 92 | + if not any(x in line for x in ['HttpURLConnection', 'URLConnection']): |
| 93 | + # For non-connection types, usually okay to skip in try blocks |
| 94 | + pass |
| 95 | + |
| 96 | + # Skip URLConnections as they use disconnect() instead of close() |
| 97 | + if 'HTTPURLConnection' in line or 'URLConnection' in line: |
| 98 | + continue |
| 99 | + |
| 100 | + # This is a potential resource leak |
| 101 | + leaks.append((line_num + 1, line.rstrip())) |
| 102 | + |
| 103 | + return leaks |
| 104 | + |
| 105 | +def main(): |
| 106 | + """Find all Java files and check for resource leaks.""" |
| 107 | + java_files = [] |
| 108 | + |
| 109 | + for root, dirs, files in os.walk('src/main/java'): |
| 110 | + for f in files: |
| 111 | + if f.endswith('.java'): |
| 112 | + java_files.append(os.path.join(root, f)) |
| 113 | + |
| 114 | + # Check each file |
| 115 | + for java_file in sorted(java_files): |
| 116 | + leaks = check_for_resource_leaks(java_file) |
| 117 | + for line_num, line_text in leaks: |
| 118 | + print(f"{java_file}:{line_num}:{line_text}") |
| 119 | + |
| 120 | +if __name__ == '__main__': |
| 121 | + main() |
0 commit comments