静态代码分析 static-code-analysis

静态代码分析是一种在不运行代码的情况下自动检测代码中的错误、安全问题和风格问题的技术,用于提高代码质量和安全性,关键词包括代码质量、安全性、自动化测试、CI/CD集成。

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

静态代码分析

概述

使用自动化工具在不执行代码的情况下分析代码,及早发现错误、安全问题和风格违规。

何时使用

  • 执行编码标准
  • 安全漏洞检测
  • 预防错误
  • 代码审查自动化
  • CI/CD 管道
  • 预提交钩子
  • 重构协助

实施示例

1. ESLint 配置

// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:security/recommended'
  ],
  plugins: ['@typescript-eslint', 'security', 'import'],
  rules: {
    'no-console': ['warn', { allow: ['error', 'warn'] }],
    'no-unused-vars': 'error',
    'prefer-const': 'error',
    'eqeqeq': ['error', 'always'],
    'no-eval': 'error',
    'security/detect-object-injection': 'warn',
    'security/detect-non-literal-regexp': 'warn',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'error',
    'import/order': ['error', {
      'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
      'newlines-between': 'always'
    }]
  }
};

2. Python 代码检查 (pylint + mypy)

# .pylintrc
[MASTER]
ignore=venv,.git,__pycache__
jobs=4

[MESSAGES CONTROL]
disable=
    missing-docstring,
    too-few-public-methods

[FORMAT]
max-line-length=100
max-module-lines=1000

[DESIGN]
max-args=5
max-locals=15
max-returns=6
max-branches=12
max-statements=50
# mypy.ini
[mypy]
python_version = 3.10
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_calls = True
warn_redundant_casts = True
warn_unused_ignores = True
strict_equality = True

3. 预提交钩子

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-json
      - id: check-merge-conflict
      - id: detect-private-key

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v8.50.0
    hooks:
      - id: eslint
        files: \.[jt]sx?$
        types: [file]

  - repo: https://github.com/psf/black
    rev: 23.9.1
    hooks:
      - id: black

  - repo: https://github.com/PyCQA/pylint
    rev: v3.0.0
    hooks:
      - id: pylint

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.5.1
    hooks:
      - id: mypy
        additional_dependencies: [types-requests]

  - repo: https://github.com/trufflesecurity/trufflehog
    rev: v3.58.0
    hooks:
      - id: trufflehog
        entry: trufflehog filesystem --directory .

4. SonarQube 集成

# sonar-project.properties
sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0

sonar.sources=src
sonar.tests=tests
sonar.exclusions=**/node_modules/**,**/*.test.ts

sonar.typescript.lcov.reportPaths=coverage/lcov.info

sonar.qualitygate.wait=true

# 质量门
sonar.coverage.exclusions=**/*.test.ts
# .github/workflows/sonar.yml
name: SonarQube Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  sonar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

      - name: Quality Gate Check
        uses: sonarsource/sonarqube-quality-gate-action@master
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

5. 自定义 AST 分析

import * as ts from 'typescript';
import * as fs from 'fs';

interface Issue {
  file: string;
  line: number;
  column: number;
  message: string;
  severity: 'error' | 'warning' | 'info';
  rule: string;
}

class CustomLinter {
  private issues: Issue[] = [];

  lintFile(filePath: string): Issue[] {
    this.issues = [];

    const sourceCode = fs.readFileSync(filePath, 'utf-8');
    const sourceFile = ts.createSourceFile(
      filePath,
      sourceCode,
      ts.ScriptTarget.Latest,
      true
    );

    this.visit(sourceFile, filePath);

    return this.issues;
  }

  private visit(node: ts.Node, filePath: string): void {
    // 检查 console.log
    if (
      ts.isCallExpression(node) &&
      ts.isPropertyAccessExpression(node.expression) &&
      node.expression.expression.getText() === 'console' &&
      node.expression.name.getText() === 'log'
    ) {
      const { line, character } = ts.getLineAndCharacterOfPosition(
        node.getSourceFile(),
        node.getStart()
      );

      this.issues.push({
        file: filePath,
        line: line + 1,
        column: character + 1,
        message: 'Unexpected console.log statement',
        severity: 'warning',
        rule: 'no-console'
      });
    }

    // 检查 any 类型
    if (
      ts.isTypeReferenceNode(node) &&
      node.typeName.getText() === 'any'
    ) {
      const { line, character } = ts.getLineAndCharacterOfPosition(
        node.getSourceFile(),
        node.getStart()
      );

      this.issues.push({
        file: filePath,
        line: line + 1,
        column: character + 1,
        message: 'Avoid using any type',
        severity: 'warning',
        rule: 'no-any'
      });
    }

    // 检查长函数
    if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
      const body = node.body;
      if (body && body.getFullText().split('
').length > 50) {
        const { line, character } = ts.getLineAndCharacterOfPosition(
          node.getSourceFile(),
          node.getStart()
        );

        this.issues.push({
          file: filePath,
          line: line + 1,
          column: character + 1,
          message: 'Function is too long (>50 lines)',
          severity: 'warning',
          rule: 'max-lines-per-function'
        });
      }
    }

    ts.forEachChild(node, child => this.visit(child, filePath));
  }

  formatIssues(issues: Issue[]): string {
    if (issues.length === 0) {
      return 'No issues found.';
    }

    return issues.map(issue =>
      `${issue.file}:${issue.line}:${issue.column} - ${issue.severity}: ${issue.message} (${issue.rule})`
    ).join('
');
  }
}

// 使用方法
const linter = new CustomLinter();
const issues = linter.lintFile('./src/example.ts');
console.log(linter.formatIssues(issues));

6. 安全扫描

import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

interface SecurityIssue {
  severity: 'critical' | 'high' | 'medium' | 'low';
  title: string;
  description: string;
  file?: string;
  line?: number;
  remediation?: string;
}

class SecurityScanner {
  async scanDependencies(): Promise<SecurityIssue[]> {
    try {
      const { stdout } = await execAsync('npm audit --json');
      const auditResult = JSON.parse(stdout);

      const issues: SecurityIssue[] = [];

      for (const [name, advisory] of Object.entries(auditResult.vulnerabilities || {})) {
        const vuln = advisory as any;

        issues.push({
          severity: vuln.severity,
          title: vuln.via[0]?.title || name,
          description: vuln.via[0]?.url || '',
          remediation: `Update ${name} to ${vuln.fixAvailable || 'latest'}`
        });
      }

      return issues;
    } catch (error) {
      console.error('Dependency scan failed:', error);
      return [];
    }
  }

  async scanSecrets(directory: string): Promise<SecurityIssue[]> {
    const issues: SecurityIssue[] = [];

    // 简单的基于正则表达式的密钥检测
    const patterns = [
      { name: 'API Key', pattern: /api[_-]?key['"]?\s*[:=]\s*['"]([a-zA-Z0-9]{32,})['"]/ },
      { name: 'AWS Key', pattern: /(AKIA[0-9A-Z]{16})/ },
      { name: 'Private Key', pattern: /-----BEGIN (RSA |EC )?PRIVATE KEY-----/ },
      { name: 'Password', pattern: /password['"]?\s*[:=]\s*['"]((?!<%= ).{8,})['"]/ }
    ];

    // 扫描文件
    const files = this.getFiles(directory);

    for (const file of files) {
      const content = fs.readFileSync(file, 'utf-8');
      const lines = content.split('
');

      for (let i = 0; i < lines.length; i++) {
        for (const { name, pattern } of patterns) {
          if (pattern.test(lines[i])) {
            issues.push({
              severity: 'critical',
              title: `Potential ${name} detected`,
              description: `Found in ${file}:${i + 1}`,
              file,
              line: i + 1,
              remediation: 'Remove secret and use environment variables'
            });
          }
        }
      }
    }

    return issues;
  }

  private getFiles(dir: string): string[] {
    // 递归获取文件的实现
    return [];
  }

  generateReport(issues: SecurityIssue[]): string {
    let report = '# Security Scan Report

';


    const grouped = issues.reduce((acc, issue) => {
      acc[issue.severity] = acc[issue.severity] || [];
      acc[issue.severity].push(issue);
      return acc;
    }, {} as Record<string, SecurityIssue[]>);

    for (const [severity, items] of Object.entries(grouped)) {
      report += `## ${severity.toUpperCase()} (${items.length})

`;


      for (const issue of items) {
        report += `### ${issue.title}
`;
        report += `${issue.description}
`;
        if (issue.remediation) {
          report += `**Remediation:** ${issue.remediation}
`;
        }
        report += '
';
      }
    }

    return report;
  }
}

// 使用方法
const scanner = new SecurityScanner();
const depIssues = await scanner.scanDependencies();
const secretIssues = await scanner.scanSecrets('./src');

const allIssues = [...depIssues, ...secretIssues];
console.log(scanner.generateReport(allIssues));

最佳实践

✅ 执行

  • 在 CI/CD 中运行 linters
  • 使用预提交钩子
  • 配置 IDE 集成
  • 逐步修复问题
  • 文档化自定义规则
  • 跨团队共享配置
  • 自动化安全扫描

❌ 不要

  • 忽略所有警告
  • 跳过 linter 设置
  • 提交 lint 违规
  • 最初使用过于严格的规则
  • 跳过安全扫描
  • 无理由禁用规则

工具

  • JavaScript/TypeScript: ESLint, TSLint, Prettier
  • Python: Pylint, Flake8, Black, Bandit
  • Java: PMD, Checkstyle, SpotBugs
  • 安全: Snyk, Semgrep, Bandit, TruffleHog
  • 多语言: SonarQube, CodeQL

资源