# -*- coding: utf-8 -*- """ Scan all .cs files to find empty methods (methods with no actual implementation). Usage: python find_empty_methods.py [target_directory] Empty method definitions: - Method body is completely empty: void Foo() { } - Body contains only whitespace/newlines - Body contains only comments (no actual code) - Body contains only return default; / return null; / return 0; etc. - Body contains only throw new NotImplementedException(); - Body contains only throw new NotSupportedException(); Output format: [file_path:line_number] return_type method_name(params) -> reason """ import os import re import sys from pathlib import Path from typing import List, Tuple, Optional # Common encodings to try (in priority order) COMMON_ENCODINGS = [ 'utf-8', 'utf-8-sig', 'utf-16-le', 'utf-16-be', 'gbk', 'gb2312', 'gb18030', 'utf-16', 'shift_jis', 'big5', 'euc-kr', 'ascii', 'latin-1', ] def detect_encoding(file_path: Path) -> Optional[str]: """ Detect file encoding. Priority: BOM -> chardet library -> brute-force try common encodings. """ try: raw_data = file_path.read_bytes() except Exception: return None if not raw_data: return None # 1. Check BOM header if raw_data[:3] == b'\xef\xbb\xbf': return 'utf-8-sig' if raw_data[:2] == b'\xff\xfe': return 'utf-16-le' if raw_data[:2] == b'\xfe\xff': return 'utf-16-be' # 2. Try using chardet (if installed) try: import chardet result = chardet.detect(raw_data) encoding = result.get('encoding') confidence = result.get('confidence', 0) if encoding and confidence > 0.7: encoding = encoding.lower().replace('-', '') mapping = { 'utf8': 'utf-8', 'utf8sig': 'utf-8-sig', 'utf16': 'utf-16', 'utf16le': 'utf-16-le', 'utf16be': 'utf-16-be', 'gb2312': 'gbk', 'ascii': 'utf-8', } return mapping.get(encoding, encoding) except ImportError: pass # 3. Fallback: try each common encoding for encoding in COMMON_ENCODINGS: try: raw_data.decode(encoding) return encoding except (UnicodeDecodeError, LookupError): continue return None def read_file_with_encoding(file_path: Path) -> Tuple[Optional[str], Optional[str]]: """ Detect encoding and read file content. Returns (content, encoding_used) or (None, None) on failure. """ encoding = detect_encoding(file_path) if encoding is None: encoding = sys.getdefaultencoding() try: content = file_path.read_text(encoding=encoding) return content, encoding except Exception: for enc in COMMON_ENCODINGS: try: content = file_path.read_text(encoding=enc) return content, enc except Exception: continue return None, None # Compiled regex for matching C# method definitions # Captures: modifiers+return_type, method_name, parameter_list, method_body METHOD_PATTERN = re.compile( r'(?:(?:\w+\s+)*)' # modifiers (public, static, async, override, virtual...) r'(\w+(?:[\[\]\s<>,\.]+)?)' # return type (including generics, arrays) r'\s+' # whitespace separator r'(\w+)\s*' # method name r'\(([^)]*)\)\s*' # parameter list r'(?::\s*\w+(?:<[^>]+>)?)?' # optional return type annotation (C# 7+) r'\s*' # whitespace r'\{([^}]*)' # method body opening { to first } r'\}', # method body closing } re.MULTILINE ) # Match single-line cases: void Foo() { } SINGLE_LINE_METHOD_PATTERN = re.compile( r'((?:\w+\s+)*)' # modifier group r'(\w+(?:[\[\]\s<>,\."]+)?)' # return type r'\s+' r'(\w+)\s*' r'\(([^)]*)\)\s*' r'(?::\s*\w+(?:<[^>]+>)?)?\s*' r'\{([^}]*)\}', # method body (may be on same line) re.MULTILINE ) # Keywords that should not be treated as method names NON_METHOD_KEYWORDS = frozenset({ 'class', 'struct', 'interface', 'enum', 'if', 'else', 'for', 'foreach', 'while', 'do', 'switch', 'using', 'namespace', 'try', 'catch', 'finally', 'lock', 'where', 'select', 'from', 'async', 'await', 'get', 'set', 'init', 'add', 'remove', 'value', 'this', 'new', 'equals', 'orderby', 'groupby', }) def strip_comments(code: str) -> str: """Remove C# single-line comments // and multi-line comments /* */""" # Remove multi-line comments first code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL) # Then remove single-line comments code = re.sub(r'//.*?$', '', code, flags=re.MULTILINE) return code def is_method_empty(method_body: str) -> Tuple[bool, Optional[str]]: """ Check if method body is effectively empty. Returns (is_empty, reason_description). """ body_stripped = method_body.strip() if not body_stripped: return True, "body is completely empty" # Contains only comments body_no_comments = strip_comments(body_stripped).strip() if not body_no_comments: return True, "body contains only comments" # Common "empty implementation" patterns empty_patterns = [ (r'^\s*return\s+default\s*;\s*$', "return default"), (r'^\s*return\s+null\s*;\s*$', "return null"), (r'^\s*return\s+0\s*;\s*$', "return 0"), (r'^\s*return\s+false\s*;\s*$', "return false"), (r'^\s*return\s+true\s*;\s*$', "return true"), (r'^\s*return\s+""\s*;\s*$', 'return ""'), (r'^\s*return\s+string\.Empty\s*;\s*$', "return string.Empty"), (r'^\s*throw\s+new\s+NotImplementedException\s*\(\s*\)\s*;\s*$', "throw NotImplementedException"), (r'^\s*throw\s+new\s+NotSupportedException\s*\(\s*\)\s*;\s*$', "throw NotSupportedException"), (r'^\s*;\s*$', "empty statement ;"), ] for pattern, reason in empty_patterns: if re.match(pattern, body_no_comments, re.DOTALL): return True, reason return False, None def find_methods_in_file(file_path: Path) -> List[Tuple[int, str, str, str, str]]: """ Parse a single .cs file and return all methods found. Returns list: [(line_number, return_type, method_name, params, body)] """ content, encoding = read_file_with_encoding(file_path) if content is None: print(f" [WARNING] Could not read file: {file_path} (encoding detection failed)") return [] methods = [] # Try multi-line matching first for match in METHOD_PATTERN.finditer(content): ret_type = match.group(1).strip() method_name = match.group(2).strip() params = match.group(3).strip() body = match.group(4).strip() if match.group(4) else '' if not ret_type or not method_name: continue if method_name in NON_METHOD_KEYWORDS: continue # Exclude lambda arrow syntax false positives if '=>' in content[match.start():match.start()+len(match.group(0))]: continue line_num = content[:match.start()].count('\n') + 1 methods.append((line_num, ret_type, method_name, params, body)) # Supplement with single-line matching (for cases not caught above) for match in SINGLE_LINE_METHOD_PATTERN.finditer(content): ret_type = match.group(2).strip() method_name = match.group(3).strip() params = match.group(4).strip() body = match.group(5).strip() if match.group(5) else '' if not ret_type or not method_name: continue if method_name in NON_METHOD_KEYWORDS: continue if '=>' in content[match.start():match.start()+len(match.group(0))]: continue # Deduplicate against multi-line matches line_num = content[:match.start()].count('\n') + 1 existing = any(m[2] == method_name and m[0] == line_num for m in methods) if existing: continue methods.append((line_num, ret_type, method_name, params, body)) return methods def scan_directory(target_dir: Path) -> None: """Recursively scan all .cs files in the directory for empty methods.""" if not target_dir.exists(): print(f"Error: directory not found - {target_dir}") sys.exit(1) cs_files = list(target_dir.rglob('*.cs')) # Exclude common auto-generated directories exclude_dirs = {'bin', 'obj', 'node_modules', 'packages', '.git', '.vs'} cs_files = [f for f in cs_files if not any(part in exclude_dirs for part in f.parts)] if not cs_files: print(f"No .cs files found under {target_dir}.") return empty_methods = [] total_methods = 0 print(f"Scanning {len(cs_files)} .cs files...\n") for cs_file in cs_files: methods = find_methods_in_file(cs_file) total_methods += len(methods) for line_num, ret_type, method_name, params, body in methods: is_empty, reason = is_method_empty(body) if is_empty: empty_methods.append((cs_file, line_num, ret_type, method_name, params, reason)) # Output results if empty_methods: print(f"Scanned {total_methods} methods, found {len(empty_methods)} empty method(s):\n") print(f"{'File':<60} {'Line':<6} {'Method Signature':<50} {'Reason'}") print("-" * 140) # Sort by file path then line number empty_methods.sort(key=lambda x: (str(x[0]), x[1])) for file_path, line_num, ret_type, method_name, params, reason in empty_methods: rel_path = file_path.relative_to(target_dir) if file_path.is_relative_to(target_dir) else file_path signature = f"{ret_type} {method_name}({params})" if len(signature) > 47: signature = signature[:44] + "..." print(f"{str(rel_path):<60} {line_num:<6} {signature:<50} {reason}") else: print(f"Scanned {total_methods} methods, no empty methods found.") def main(): if len(sys.argv) > 1: target_dir = Path(sys.argv[1]).resolve() else: target_dir = Path.cwd().resolve() print(f"Target directory: {target_dir}\n") scan_directory(target_dir) if __name__ == '__main__': main()