代码度量分析Skill code-metrics-analysis

代码度量分析是一种软件开发技术,用于量化代码复杂度、可维护性和其他质量属性,帮助开发者识别需要重构的代码区域,监控技术债务,并提高代码质量。

测试 0 次安装 0 次浏览 更新于 3/3/2026

代码度量分析

概览

测量和分析代码质量度量,以识别复杂性、可维护性问题和改进领域。

使用场景

  • 代码质量评估
  • 识别重构候选
  • 技术债务监控
  • 代码审查自动化
  • 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

资源