#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
代码质量检查脚本
检查PEP 8规范、类型注解、错误处理等代码质量指标
"""
import ast
import os
import sys
import re
from pathlib import Path
from typing import List, Dict, Any, Tuple
class CodeQualityChecker:
"""代码质量检查器"""
def __init__(self):
self.issues = []
self.stats = {
'files_checked': 0,
'lines_of_code': 0,
'functions': 0,
'classes': 0,
'type_annotations': 0,
'docstrings': 0
}
def check_file(self, filepath: str) -> Dict[str, Any]:
"""检查单个文件的代码质量"""
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
file_issues = []
lines = content.split('\n')
self.stats['lines_of_code'] += len(lines)
# 解析AST
try:
tree = ast.parse(content)
except SyntaxError as e:
return {'error': f'语法错误: {e}', 'issues': []}
# 检查各种质量指标
file_issues.extend(self._check_line_length(lines))
file_issues.extend(self._check_imports(tree))
file_issues.extend(self._check_functions(tree))
file_issues.extend(self._check_classes(tree))
file_issues.extend(self._check_docstrings(tree))
file_issues.extend(self._check_type_annotations(tree))
file_issues.extend(self._check_error_handling(tree))
# 更新统计
self._update_stats(tree)
return {
'file': filepath,
'issues': file_issues,
'stats': {
'lines': len(lines),
'functions': len([n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)]),
'classes': len([n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)])
}
}
except Exception as e:
return {'error': f'文件读取失败: {e}', 'issues': []}
def _check_line_length(self, lines: List[str]) -> List[Dict[str, Any]]:
"""检查行长度(PEP 8: 79字符,这里使用100字符作为宽松标准)"""
issues = []
for i, line in enumerate(lines, 1):
if len(line) > 100:
issues.append({
'line': i,
'type': 'line_length',
'message': f'行过长: {len(line)} 字符',
'severity': 'warning'
})
return issues
def _check_imports(self, tree: ast.AST) -> List[Dict[str, Any]]:
"""检查导入语句"""
issues = []
imports = []
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
imports.append(node)
# 检查导入顺序(标准库 -> 第三方 -> 本地)
if len(imports) > 1:
# 这里简化处理,实际应该更复杂
pass
return issues
def _check_functions(self, tree: ast.AST) -> List[Dict[str, Any]]:
"""检查函数定义"""
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# 检查函数名
if not re.match(r'^[a-z_][a-z0-9_]*$', node.name):
issues.append({
'line': node.lineno,
'type': 'function_name',
'message': f'函数名不符合snake_case命名规范: {node.name}',
'severity': 'warning'
})
# 检查函数复杂度(简化版)
if len(list(ast.walk(node))) > 50: # 简单的复杂度指标
issues.append({
'line': node.lineno,
'type': 'complexity',
'message': f'函数可能过于复杂: {node.name}',
'severity': 'info'
})
return issues
def _check_classes(self, tree: ast.AST) -> List[Dict[str, Any]]:
"""检查类定义"""
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
# 检查类名
if not re.match(r'^[A-Z][a-zA-Z0-9]*$', node.name):
issues.append({
'line': node.lineno,
'type': 'class_name',
'message': f'类名不符合PascalCase命名规范: {node.name}',
'severity': 'warning'
})
return issues
def _check_docstrings(self, tree: ast.AST) -> List[Dict[str, Any]]:
"""检查文档字符串"""
issues = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
# 检查是否有文档字符串
docstring = ast.get_docstring(node)
if not docstring:
severity = 'warning' if isinstance(node, ast.ClassDef) else 'info'
item_type = '类' if isinstance(node, ast.ClassDef) else '函数'
issues.append({
'line': node.lineno,
'type': 'missing_docstring',
'message': f'{item_type}缺少文档字符串: {node.name}',
'severity': severity
})
elif len(docstring.strip()) < 10:
issues.append({
'line': node.lineno,
'type': 'short_docstring',
'message': f'文档字符串过短: {node.name}',
'severity': 'info'
})
else:
self.stats['docstrings'] += 1
return issues
def _check_type_annotations(self, tree: ast.AST) -> List[Dict[str, Any]]:
"""检查类型注解"""
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
has_return_annotation = node.returns is not None
has_arg_annotations = all(arg.annotation is not None for arg in node.args.args)
if not has_return_annotation:
issues.append({
'line': node.lineno,
'type': 'missing_return_annotation',
'message': f'函数缺少返回类型注解: {node.name}',
'severity': 'info'
})
if not has_arg_annotations and node.args.args:
issues.append({
'line': node.lineno,
'type': 'missing_arg_annotations',
'message': f'函数参数缺少类型注解: {node.name}',
'severity': 'info'
})
if has_return_annotation or has_arg_annotations:
self.stats['type_annotations'] += 1
return issues
def _check_error_handling(self, tree: ast.AST) -> List[Dict[str, Any]]:
"""检查错误处理"""
issues = []
for node in ast.walk(tree):
if isinstance(node, ast.Try):
# 检查是否过于宽泛的异常捕获
for handler in node.handlers:
if handler.type is None:
issues.append({
'line': handler.lineno,
'type': 'bare_except',
'message': '使用了裸露的except语句',
'severity': 'warning'
})
elif isinstance(handler.type, ast.Name) and handler.type.id == 'Exception':
issues.append({
'line': handler.lineno,
'type': 'broad_except',
'message': '捕获了过于宽泛的Exception',
'severity': 'info'
})
return issues
def _update_stats(self, tree: ast.AST):
"""更新统计信息"""
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
self.stats['functions'] += 1
elif isinstance(node, ast.ClassDef):
self.stats['classes'] += 1
def check_directory(self, directory: str, pattern: str = "*.py") -> Dict[str, Any]:
"""检查目录下的所有Python文件"""
results = []
for filepath in Path(directory).rglob(pattern):
if '__pycache__' in str(filepath):
continue
self.stats['files_checked'] += 1
result = self.check_file(str(filepath))
results.append(result)
return {
'results': results,
'total_issues': sum(len(r.get('issues', [])) for r in results),
'stats': self.stats
}
def print_results(results: Dict[str, Any]):
"""打印检查结果"""
print("=" * 80)
print("代码质量检查报告")
print("=" * 80)
# 统计信息
stats = results['stats']
print(f"\n📊 统计信息:")
print(f" 检查文件数: {stats['files_checked']}")
print(f" 代码行数: {stats['lines_of_code']}")
print(f" 函数数量: {stats['functions']}")
print(f" 类数量: {stats['classes']}")
print(f" 类型注解: {stats['type_annotations']}")
print(f" 文档字符串: {stats['docstrings']}")
# 问题统计
total_issues = results['total_issues']
print(f"\n🔍 问题统计:")
print(f" 总问题数: {total_issues}")
if total_issues > 0:
# 按严重程度分类
severity_count = {'error': 0, 'warning': 0, 'info': 0}
type_count = {}
for result in results['results']:
for issue in result.get('issues', []):
severity = issue.get('severity', 'info')
issue_type = issue.get('type', 'unknown')
severity_count[severity] += 1
type_count[issue_type] = type_count.get(issue_type, 0) + 1
print(f" 错误: {severity_count['error']}")
print(f" 警告: {severity_count['warning']}")
print(f" 信息: {severity_count['info']}")
print(f"\n📋 问题类型分布:")
for issue_type, count in sorted(type_count.items()):
print(f" {issue_type}: {count}")
# 详细问题列表
print(f"\n📝 详细问题:")
for result in results['results']:
if 'error' in result:
print(f"❌ {result['file']}: {result['error']}")
continue
file_issues = result.get('issues', [])
if file_issues:
print(f"\n📄 {result['file']}:")
for issue in file_issues:
severity_icon = {'error': '❌', 'warning': '⚠️', 'info': 'ℹ️'}.get(issue.get('severity', 'info'), 'ℹ️')
print(f" {severity_icon} 行{issue['line']}: {issue['message']}")
print("\n" + "=" * 80)
# 质量评分
if total_issues == 0:
print("🎉 代码质量优秀!")
elif total_issues < 10:
print("👍 代码质量良好")
elif total_issues < 30:
print("👌 代码质量一般,建议优化")
else:
print("🚨 代码需要较多改进")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='代码质量检查工具')
parser.add_argument('path', nargs='?', default='.', help='要检查的文件或目录路径')
parser.add_argument('--pattern', default='*.py', help='文件匹配模式')
args = parser.parse_args()
checker = CodeQualityChecker()
if os.path.isfile(args.path):
if args.path.endswith('.py'):
result = checker.check_file(args.path)
results = {
'results': [result],
'total_issues': len(result.get('issues', [])),
'stats': checker.stats
}
else:
print("错误: 只能检查Python文件")
return
else:
results = checker.check_directory(args.path, args.pattern)
print_results(results)
if __name__ == "__main__":
main()