C#空方法扫描脚本
This commit is contained in:
parent
9a290bbeda
commit
0af9dd21d7
318
find_cs_empty_methods.py
Normal file
318
find_cs_empty_methods.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
# -*- 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()
|
||||||
Loading…
Reference in New Issue
Block a user