跳转至

high-cyclomatic-complexity

Category: 复杂度
Severity: Configurable by threshold
Triggered by: pyscn analyze, pyscn check

检测内容

标记 McCabe 圈复杂度超过配置阈值的函数。每个 ifelifforwhileexceptmatch case 以及推导式中的布尔子句都会使计数加一。一个没有分支的函数从 1 开始计数。

pyscn 不会将 and / or 短路运算符计为单独的分支。

为什么这是一个问题

高分支数意味着:

  • 需要阅读更多路径 — 每个审查者都必须在脑中模拟每个分支才能理解函数的功能。
  • 需要测试更多路径 — 完整的分支覆盖要求每条路径都有一个测试;实际上大多数高分支函数的测试都不充分。
  • 更高的缺陷密度 — 自 McCabe(1976)以来的实证研究表明复杂度与缺陷率相关。
  • 更难安全地修改 — 在一个分支中的小改动可能会悄悄破坏另一个分支。

复杂度超过约 10 的函数通常在做多件可以命名并分离的工作。

示例

def price_for(user, cart, coupon, region):
    total = 0
    for item in cart:
        if item.category == "book":
            if region == "EU":
                total += item.price * 0.95
            elif region == "US":
                total += item.price
            else:
                total += item.price * 1.10
        elif item.category == "food":
            if user.is_student:
                total += item.price * 0.90
            else:
                total += item.price
        else:
            total += item.price
    if coupon:
        if coupon.kind == "percent":
            total *= 1 - coupon.value
        elif coupon.kind == "fixed":
            total -= coupon.value
    if total < 0:
        total = 0
    return total

圈复杂度:13。

修正示例

提取每项定价和优惠券处理逻辑,并用分派表替换嵌套的条件判断。

REGION_BOOK_MULTIPLIER = {"EU": 0.95, "US": 1.00}

def _book_price(item, region):
    return item.price * REGION_BOOK_MULTIPLIER.get(region, 1.10)

def _food_price(item, user):
    return item.price * (0.90 if user.is_student else 1.00)

PRICERS = {"book": _book_price, "food": _food_price}

def _item_price(item, user, region):
    pricer = PRICERS.get(item.category)
    return pricer(item, region) if item.category == "book" else \
           pricer(item, user)   if item.category == "food" else \
           item.price

def _apply_coupon(total, coupon):
    if coupon is None:
        return total
    if coupon.kind == "percent":
        return total * (1 - coupon.value)
    return total - coupon.value

def price_for(user, cart, coupon, region):
    subtotal = sum(_item_price(i, user, region) for i in cart)
    return max(0, _apply_coupon(subtotal, coupon))

每个辅助函数的复杂度现在只有 1-3,且各司其职。守卫子句(if coupon is None: return)展平了剩余的分支。

选项

选项 默认值 说明
complexity.max_complexity 0 pyscn check 强制执行的硬限制。0 表示在 analyze 中不强制执行;如果未设置,pyscn check --max-complexity 使用 10
complexity.low_threshold 9 等于或低于此值的函数报告为低风险。
complexity.medium_threshold 19 高于此值的函数为高风险。
complexity.min_complexity 1 低于此值的函数将从报告中省略。

相关度量

除圈复杂度以外,pyscn 还会同时计算以下两类与复杂度相关的度量。它们会出现在 HTML 报告和 JSON 输出中,但complexity.max_complexity 阈值约束:

  • 认知复杂度(Cognitive Complexity,SonarQube 风格)。相比 McCabe,认知复杂度对嵌套以及线性流程的中断给出更高的惩罚——适合用于识别那些圈复杂度并不算高、但读起来仍然难以跟随的函数。JSON 输出中以函数级 CognitiveComplexity 字段呈现。
  • 原始代码度量(按文件统计):SLOC、LLOC、注释行、空行、注释密度。详见 schema 参考中的 raw_metrics

参考

  • McCabe, T. J. A Complexity Measure. IEEE Transactions on Software Engineering, 1976.
  • 控制流图构建和圈复杂度计算:internal/analyzer/complexity.gointernal/analyzer/complexity_analyzer.gointernal/analyzer/cfg_builder.go
  • 规则目录 · too-many-constructor-parameters