コンテンツにスキップ

high-cyclomatic-complexity

Category: Complexity
Severity: Configurable by threshold
Triggered by: pyscn analyze, pyscn check

What it does

Flags functions whose McCabe cyclomatic complexity exceeds the configured threshold. Each if, elif, for, while, except, match case, and boolean clause inside a comprehension adds one to the count. A straight-line function starts at 1.

pyscn does not count and / or short-circuit operators as separate branches.

Why is this a problem?

A high branch count means:

  • More paths to read — every reviewer has to mentally simulate each branch to understand what the function does.
  • More paths to test — full branch coverage requires one test per path; most highly-branched functions are under-tested in practice.
  • Higher defect density — empirical studies since McCabe (1976) correlate complexity with bug rate.
  • Harder to change safely — a small edit in one branch can silently break another.

Functions above ~10 are usually doing several jobs that could be named and separated.

Example

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

Cyclomatic complexity: 13.

Use instead

Extract the per-item pricing and coupon handling, and replace the nested conditional with a dispatch table.

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))

Each helper now has complexity 1–3 and a single responsibility. Guard clauses (if coupon is None: return) flatten the remaining branches.

Options

Option Default Description
complexity.max_complexity 0 Hard limit enforced by pyscn check. 0 means no enforcement in analyze; pyscn check --max-complexity uses 10 if unset.
complexity.low_threshold 9 Functions at or below this are reported as low risk.
complexity.medium_threshold 19 Above this, a function is high risk.
complexity.min_complexity 1 Functions below this value are omitted from the report.

References

  • McCabe, T. J. A Complexity Measure. IEEE Transactions on Software Engineering, 1976.
  • Control-flow graph construction and cyclomatic counting: internal/analyzer/complexity.go, internal/analyzer/complexity_analyzer.go, internal/analyzer/cfg_builder.go.
  • Rule catalog · too-many-constructor-parameters