代码度量分析
概览
测量和分析代码质量度量,以识别复杂性、可维护性问题和改进领域。
使用场景
- 代码质量评估
- 识别重构候选
- 技术债务监控
- 代码审查自动化
- CI/CD质量门禁
- 团队绩效跟踪
- 遗留代码分析
关键度量
| 度量 | 描述 | 良好范围 |
|---|---|---|
| 圈复杂度 | 线性独立路径的数量 | 1-10 |
| 认知复杂度 | 代码可理解性的度量 | <15 |
| 代码行数 | 总行数(LOC) | 函数:<50 |
| 可维护性指数 | 整体可维护性得分 | >65 |
| 代码变更频率 | 变更频率 | 低 |
| 测试覆盖率 | 被测试覆盖的百分比 | >80% |
实施示例
1. TypeScript复杂度分析器
import * as ts from 'typescript';
import * as fs from 'fs';
interface ComplexityMetrics {
cyclomaticComplexity: number;
cognitiveComplexity: number;
linesOfCode: number;
functionCount: number;
classCount: number;
maxNestingDepth: number;
}
class CodeMetricsAnalyzer {
analyzeFile(filePath: string): ComplexityMetrics {
const sourceCode = fs.readFileSync(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(
filePath,
sourceCode,
ts.ScriptTarget.Latest,
true
);
const metrics: ComplexityMetrics = {
cyclomaticComplexity: 0,
cognitiveComplexity: 0,
linesOfCode: sourceCode.split('
').length,
functionCount: 0,
classCount: 0,
maxNestingDepth: 0
};
this.visit(sourceFile, metrics);
return metrics;
}
private visit(node: ts.Node, metrics: ComplexityMetrics, depth: number = 0): void {
metrics.maxNestingDepth = Math.max(metrics.maxNestingDepth, depth);
// Count functions
if (
ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node) ||
ts.isArrowFunction(node)
) {
metrics.functionCount++;
metrics.cyclomaticComplexity++;
}
// Count classes
if (ts.isClassDeclaration(node)) {
metrics.classCount++;
}
// Cyclomatic complexity contributors
if (
ts.isIfStatement(node) ||
ts.isConditionalExpression(node) ||
ts.isWhileStatement(node) ||
ts.isForStatement(node) ||
ts.isCaseClause(node)
) {
metrics.cyclomaticComplexity++;
}
// Cognitive complexity (simplified)
if (ts.isIfStatement(node)) {
metrics.cognitiveComplexity += 1 + depth;
}
if (ts.isWhileStatement(node) || ts.isForStatement(node)) {
metrics.cognitiveComplexity += 1 + depth;
}
// Recurse
const newDepth = this.increasesNesting(node) ? depth + 1 : depth;
ts.forEachChild(node, child => {
this.visit(child, metrics, newDepth);
});
}
private increasesNesting(node: ts.Node): boolean {
return (
ts.isIfStatement(node) ||
ts.isWhileStatement(node) ||
ts.isForStatement(node) ||
ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node)
);
}
calculateMaintainabilityIndex(metrics: ComplexityMetrics): number {
// Simplified maintainability index
const halsteadVolume = metrics.linesOfCode * 4.5; // Simplified
const cyclomaticComplexity = metrics.cyclomaticComplexity;
const linesOfCode = metrics.linesOfCode;
const mi = Math.max(
0,
(171 - 5.2 * Math.log(halsteadVolume) -
0.23 * cyclomaticComplexity -
16.2 * Math.log(linesOfCode)) * 100 / 171
);
return Math.round(mi);
}
analyzeProject(directory: string): Record<string, ComplexityMetrics> {
const results: Record<string, ComplexityMetrics> = {};
const files = this.getTypeScriptFiles(directory);
for (const file of files) {
results[file] = this.analyzeFile(file);
}
return results;
}
private getTypeScriptFiles(dir: string): string[] {
const files: string[] = [];
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = `${dir}/${item}`;
const stat = fs.statSync(fullPath);
if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
files.push(...this.getTypeScriptFiles(fullPath));
} else if (item.endsWith('.ts') && !item.endsWith('.d.ts')) {
files.push(fullPath);
}
}
return files;
}
generateReport(results: Record<string, ComplexityMetrics>): string {
let report = '# 代码度量报告
';
// Summary
const totalFiles = Object.keys(results).length;
const avgComplexity = Object.values(results).reduce(
(sum, m) => sum + m.cyclomaticComplexity, 0
) / totalFiles;
report += `## 摘要
`;
report += `- 总文件数:${totalFiles}
`;
report += `- 平均复杂度:${avgComplexity.toFixed(2)}
`;
// High complexity files
report += `## 高复杂度文件
`;
const highComplexity = Object.entries(results)
.filter(([_, m]) => m.cyclomaticComplexity > 10)
.sort((a, b) => b[1].cyclomaticComplexity - a[1].cyclomaticComplexity);
if (highComplexity.length === 0) {
report += '未发现。
';
} else {
for (const [file, metrics] of highComplexity) {
report += `- ${file}
`;
report += ` - 圈复杂度:${metrics.cyclomaticComplexity}
`;
report += ` - 认知复杂度:${metrics.cognitiveComplexity}
`;
report += ` - LOC:${metrics.linesOfCode}
`;
}
}
return report;
}
}
// 使用方法
const analyzer = new CodeMetricsAnalyzer();
const results = analyzer.analyzeProject('./src');
const report = analyzer.generateReport(results);
console.log(report);
2. Python代码度量(使用radon)
from radon.complexity import cc_visit
from radon.metrics import mi_visit, h_visit
from radon.raw import analyze
import os
from typing import Dict, List
import json
class CodeMetricsAnalyzer:
def analyze_file(self, file_path: str) -> Dict:
"""分析单个Python文件。"""
with open(file_path, 'r') as f:
code = f.read()
# 圈复杂度
complexity = cc_visit(code)
# 可维护性指数
mi = mi_visit(code, True)
# Halstead度量
halstead = h_visit(code)
# 原始度量
raw = analyze(code)
return {
'file': file_path,
'complexity': [{
'name': block.name,
'complexity': block.complexity,
'lineno': block.lineno
} for block in complexity],
'maintainability_index': mi,
'halstead': {
'volume': halstead.total.volume if halstead.total else 0,
'difficulty': halstead.total.difficulty if halstead.total else 0,
'effort': halstead.total.effort if halstead.total else 0
},
'raw': {
'loc': raw.loc,
'lloc': raw.lloc,
'sloc': raw.sloc,
'comments': raw.comments,
'multi': raw.multi,
'blank': raw.blank
}
}
def analyze_project(self, directory: str) -> List[Dict]:
"""分析项目中的所有Python文件。"""
results = []
for root, dirs, files in os.walk(directory):
# 跳过常见目录
dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__', 'venv', 'node_modules']]
for file in files:
if file.endswith('.py'):
file_path = os.path.join(root, file)
try:
result = self.analyze_file(file_path)
results.append(result)
except Exception as e:
print(f"分析{file_path}时出错:{e}")
return results
def generate_report(self, results: List[Dict]) -> str:
"""生成Markdown报告。"""
report = "# 代码度量报告
"
# 摘要
total_files = len(results)
avg_mi = sum(r['maintainability_index'] for r in results) / total_files if total_files > 0 else 0
total_loc = sum(r['raw']['loc'] for r in results)
report += "## 摘要
"
report += f"- 总文件数:{total_files}
"
report += f"- 总LOC:{total_loc}
"
report += f"- 平均可维护性指数:{avg_mi:.2f}
"
# 高复杂度函数
report += "## 高复杂度函数
"
high_complexity = []
for result in results:
for func in result['complexity']:
if func['complexity'] > 10:
high_complexity.append({
'file': result['file'],
**func
})
high_complexity.sort(key=lambda x: x['complexity'], reverse=True)
if not high_complexity:
report += "未发现。
"
else:
for func in high_complexity[:10]: # 前10
report += f"- {func['file']}:{func['lineno']} - {func['name']}
"
report += f" 复杂度:{func['complexity']}
"
# 低可维护性文件
report += "## 低可维护性文件
"
low_mi = [r for r in results if r['maintainability_index'] < 65]
low_mi.sort(key=lambda x: x['maintainability_index'])
if not low_mi:
report += "未发现。
"
else:
for file in low_mi[:10]:
report += f"- {file['file']}
"
report += f" MI:{file['maintainability_index']:.2f}
"
report += f" LOC:{file['raw']['loc']}
"
return report
def export_json(self, results: List[Dict], output_file: str):
"""将结果导出为JSON。"""
with open(output_file, 'w') as f:
json.dump(results, f, indent=2)
# 使用方法
analyzer = CodeMetricsAnalyzer()
results = analyzer.analyze_project('./src')
report = analyzer.generate_report(results)
print(report)
# 导出到JSON
analyzer.export_json(results, 'metrics.json')
3. ESLint插件用于复杂度
// eslint-plugin-complexity.js
module.exports = {
rules: {
'max-complexity': {
create(context) {
const maxComplexity = context.options[0] || 10;
let complexity = 0;
function increaseComplexity(node) {
complexity++;
}
function checkComplexity(node) {
if (complexity > maxComplexity) {
context.report({
node,
message: `函数的复杂度为${complexity}。允许的最大值是${maxComplexity}。`
});
}
}
return {
FunctionDeclaration(node) {
complexity = 1;
},
'FunctionDeclaration:exit': checkComplexity,
IfStatement: increaseComplexity,
SwitchCase: increaseComplexity,
ForStatement: increaseComplexity,
WhileStatement: increaseComplexity,
DoWhileStatement: increaseComplexity,
ConditionalExpression: increaseComplexity,
LogicalExpression(node) {
if (node.operator === '&&' || node.operator === '||') {
increaseComplexity();
}
}
};
}
}
}
};
4. CI/CD质量门禁
# .github/workflows/code-quality.yml
name: 代码质量
on: [pull_request]
jobs:
metrics:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 设置Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: 安装依赖
run: npm install
- name: 运行复杂度分析
run: npx ts-node analyze-metrics.ts
- name: 检查质量门禁
run: |
COMPLEXITY=$(cat metrics.json | jq '.avgComplexity')
if (( $(echo "$COMPLEXITY > 10" | bc -l) )); then
echo "平均复杂度过高:$COMPLEXITY"
exit 1
fi
- name: 上传度量
uses: actions/upload-artifact@v2
with:
name: code-metrics
path: metrics.json
最佳实践
✅ 要做
- 随时间监控度量
- 设置合理的阈值
- 关注趋势,而不是绝对数值
- 自动化度量收集
- 使用度量指导重构
- 结合多个度量
- 将度量包含在代码审查中
❌ 不要做
- 将度量作为唯一的质量指标
- 设置不切实际的阈值
- 忽略上下文和领域
- 因度量惩罚开发人员
- 只关注一个度量
- 跳过文档
工具
- TypeScript/JavaScript: ESLint, ts-morph, complexity-report
- Python: radon, mccabe, pylint
- Java: PMD, Checkstyle, SonarQube
- C#: NDepend, SonarQube
- 多语言: SonarQube, CodeClimate