import os
import ast
import re
from pathlib import Path
from typing import List, Dict, Tuple
class ProjectAnalyzer:
def __init__(self, project_root: str):
self.project_root = Path(project_root)
self.issues = []
self.file_stats = {}
def analyze_file(self, filepath: Path) -> Dict:
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
lines = f.readlines()
except:
return {'error': 'Could not read file'}
file_issues = []
# 1. 行长度检查
for i, line in enumerate(lines, 1):
if len(line.rstrip()) > 100:
file_issues.append(f'第{i}行过长: {len(line.rstrip())}字符')
# 2. 异常处理检查
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.ExceptHandler):
if node.type is None:
file_issues.append(f'第{node.lineno}行使用裸except语句')
elif isinstance(node.type, ast.Name) and node.type.id == 'Exception':
file_issues.append(f'第{node.lineno}行捕获过于宽泛的异常')
except:
file_issues.append('语法错误,无法解析AST')
# 3. 函数复杂度检查
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.For, ast.While, ast.With, ast.ExceptHandler)):
complexity += 1
if complexity > 10:
file_issues.append(f'函数 {node.name} 圈复杂度过高: {complexity}')
except:
pass
return {
'path': str(filepath),
'lines': len(lines),
'issues': file_issues,
'issue_count': len(file_issues)
}
def analyze_project(self) -> Dict:
total_files = 0
total_lines = 0
total_issues = 0
# 遍历Python文件
for root, dirs, files in os.walk(self.project_root):
# 跳过虚拟环境和缓存目录
dirs[:] = [d for d in dirs if d not in ['__pycache__', '.git', 'venv', 'mcp-env']]
for file in files:
if file.endswith('.py'):
filepath = Path(root) / file
total_files += 1
# 分析文件
result = self.analyze_file(filepath)
if 'error' not in result:
total_lines += result['lines']
total_issues += result['issue_count']
# 保存详细信息
relative_path = str(filepath.relative_to(self.project_root))
self.file_stats[relative_path] = result
# 添加到总问题列表
for issue in result['issues']:
self.issues.append(f"{relative_path}: {issue}")
return {
'total_files': total_files,
'total_lines': total_lines,
'total_issues': total_issues,
'files_with_issues': len([f for f in self.file_stats.values() if f['issue_count'] > 0])
}
def generate_report(self) -> str:
# 分析项目
stats = self.analyze_project()
report = []
report.append("=" * 60)
report.append("项目代码质量分析报告")
report.append("=" * 60)
report.append(f"总文件数: {stats['total_files']}")
report.append(f"总代码行数: {stats['total_lines']}")
report.append(f"总问题数: {stats['total_issues']}")
report.append(f"有问题的文件数: {stats['files_with_issues']}")
report.append(f"平均每文件问题数: {stats['total_issues']/stats['total_files']:.1f}")
report.append("")
# 按问题数排序显示文件
sorted_files = sorted(
self.file_stats.items(),
key=lambda x: x[1]['issue_count'],
reverse=True
)
report.append("文件问题排行榜 (前10个):")
report.append("-" * 40)
for i, (filename, stats) in enumerate(sorted_files[:10], 1):
report.append(f"{i:2d}. {filename} ({stats['issue_count']}个问题, {stats['lines']}行)")
report.append("")
report.append("详细问题列表:")
report.append("-" * 40)
# 显示前50个问题
for i, issue in enumerate(self.issues[:50], 1):
report.append(f"{i:3d}. {issue}")
if len(self.issues) > 50:
report.append(f"... 还有{len(self.issues)-50}个问题未显示")
return "\n".join(report)
if __name__ == "__main__":
analyzer = ProjectAnalyzer("src")
report = analyzer.generate_report()
print(report)
# 保存报告到文件
with open("project_quality_report.txt", "w", encoding="utf-8") as f:
f.write(report)