Skip to main content
Glama
code_quality_check.py13 kB
#!/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()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kscz0000/Zhiwen-Assistant-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server