Skip to content

Refactoring Rules

complexipy includes a clippy-inspired refactoring system that provides actionable suggestions for reducing cognitive complexity. Each rule has a unique ID, category, and applicability level.

Rule Categories

Category Icon Description
Complexity Rules that directly reduce cognitive complexity
Readability Rules that improve code readability

Applicability Levels

Level Icon Description
Safe to apply * High confidence the generated code is correct as written -- no automatic application yet, this is a confidence signal, not a promise of automation
Needs review ! May be incorrect in some cases, needs human review
Informational i Just guidance, not directly actionable

Complexity Rules

C001: Flatten Nested Conditions

  • Category: ▲ Complexity
  • Applicability: i Informational
  • Priority: High (4/5)

Flatten nested condition blocks by using guard clauses with early returns.

When does it trigger?

This rule triggers when a function has deeply nested if statements (2+ levels of nesting) that add significant complexity.

Example

Before:

def process_data(data):
    if data:
        if data.is_valid():
            if data.is_ready():
                return process(data)
    return None

After:

def process_data(data):
    if not data:
        return None
    if not data.is_valid():
        return None
    if not data.is_ready():
        return None
    return process(data)

Why this helps

Deeply nested conditions are hard to follow. Using guard clauses with early returns reduces cognitive load by keeping the main path at a lower indentation level.


C002: Loop Guards

  • Category: ▲ Complexity
  • Applicability: * Safe to apply
  • Priority: Medium (3/5)

Use continue guards at the top of loops to reduce nesting.

When does it trigger?

This rule triggers when a loop contains nested if statements that could be converted to early continue guards.

Example

Before:

def process_items(items):
    total = 0
    for item in items:
        if item.active:
            if item.ready:
                total += item.value
    return total

After:

def process_items(items):
    total = 0
    for item in items:
        if not item.active:
            continue
        if not item.ready:
            continue
        total += item.value
    return total

Why this helps

Nested conditions inside loops add unnecessary indentation. Using continue guards keeps the main logic at a lower nesting level and makes the loop easier to follow.


C003: Extract Helper Function

  • Category: ▲ Complexity
  • Applicability: i Informational
  • Priority: Low (2/5)

Extract complex code blocks into separate helper functions.

When does it trigger?

This rule triggers when a code block has high complexity (6+) and spans multiple lines (5+).

Example

Before:

def process_order(order):
    # Complex validation and processing logic
    if order.items:
        for item in order.items:
            if item.quantity > 0:
                if item.price > 0:
                    total = item.quantity * item.price
                    if total > 100:
                        apply_discount(total)
                    process_item(item)

After:

def process_order(order):
    if not order.items:
        return
    for item in order.items:
        process_order_item(item)

def process_order_item(item):
    if item.quantity <= 0 or item.price <= 0:
        return
    total = item.quantity * item.price
    if total > 100:
        apply_discount(total)
    process_item(item)

Why this helps

Complex code blocks should be extracted into named functions to improve readability and testability. The extracted function can be given a descriptive name that explains its purpose.


C004: Split Dispatcher

  • Category: ▲ Complexity
  • Applicability: i Informational
  • Priority: Low (2/5)

Split long elif chains into separate handlers.

When does it trigger?

This rule triggers when an if/elif chain has 3+ branches. match statements are intentionally excluded: complexipy's cognitive complexity model charges a match a flat cost regardless of how many case clauses it has (unlike an elif chain, where every additional branch adds to the score), so splitting a match into separate handlers would not actually reduce the measured complexity.

The suggested refactor depends on the chain's shape:

  • If every branch compares the same variable against a literal with ==, the chain can become a match statement -- this is recommended over a dispatch dict, since it needs no extra indirection and complexipy's own model scores it as free of the per-branch cost the elif chain has.
  • Otherwise (ranges, multiple variables, non-equality comparisons -- anything a plain match case <value>: can't express) a dispatch dictionary mapping cases to handler functions is suggested instead.

Example: single-variable equality → match

Before:

def handle_action(action):
    if action == "create":
        return create_resource()
    elif action == "read":
        return read_resource()
    elif action == "update":
        return update_resource()
    elif action == "delete":
        return delete_resource()
    return None

After:

def handle_action(action):
    match action:
        case "create":
            return create_resource()
        case "read":
            return read_resource()
        case "update":
            return update_resource()
        case "delete":
            return delete_resource()
    return None

Example: ranges → dispatch dictionary

Before:

def classify(score):
    if score < 60:
        return "fail"
    elif score < 70:
        return "d"
    elif score < 85:
        return "b"
    elif score < 95:
        return "a"
    return "a+"

After:

def classify(score):
    thresholds = [(60, "fail"), (70, "d"), (85, "b"), (95, "a")]
    for limit, grade in thresholds:
        if score < limit:
            return grade
    return "a+"

Why this helps

Long conditional chains are hard to maintain and extend. Splitting them into separate handlers -- a match statement when the chain is a simple equality dispatch, a dispatch dictionary otherwise -- makes each case independently testable and the dispatch logic clearer.


C011: Flatten Try/Except

  • Category: ▲ Complexity
  • Applicability: i Informational
  • Priority: Low (2/5)

Flatten nested try/except blocks by combining or restructuring.

When does it trigger?

This rule triggers when try/except blocks are nested inside each other.

Example

Before:

def read_config(path):
    try:
        with open(path) as f:
            try:
                return json.load(f)
            except json.JSONDecodeError:
                return default_config()
    except FileNotFoundError:
        return default_config()

After:

def read_config(path):
    try:
        with open(path) as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return default_config()

Why this helps

Nested try/except blocks are confusing and hard to maintain. Consider merging them or extracting the inner block into a separate function with its own error handling.


Readability Rules

C005: Extract Predicate

  • Category: ◆ Readability
  • Applicability: * Safe to apply
  • Priority: Low (2/5)

Extract complex boolean conditions into named predicate functions.

When does it trigger?

This rule triggers when a boolean condition contains 2+ logical operators (and, or, not).

Example

Before:

def is_eligible(user, order):
    if (user.is_active and user.has_subscription) or (order.total > 100 and not order.is_gift):
        return True
    return False

After:

def is_eligible(user, order):
    return has_active_subscription(user) or is_qualifying_order(order)

def has_active_subscription(user):
    return user.is_active and user.has_subscription

def is_qualifying_order(order):
    return order.total > 100 and not order.is_gift

Why this helps

Complex boolean expressions are hard to understand at a glance. Extracting them into named predicates makes the code self-documenting and easier to test.


C007: Collapsible If

  • Category: ◆ Readability
  • Applicability: * Safe to apply
  • Priority: Highest (5/5)

Merge nested if statements into a single if with combined conditions.

When does it trigger?

This rule triggers when an if statement's entire body is a single nested if with no else branch, for a chain of two or more such levels.

Example

Before:

def check_eligibility(user):
    if user.is_active:
        if user.has_permission:
            return True
    return False

After:

def check_eligibility(user):
    if user.is_active and user.has_permission:
        return True
    return False

Why this helps

Nested if statements with a single body can be merged into a single if with combined conditions using and. This reduces nesting and improves readability.


Using Refactoring Rules

Command Line

# Show refactoring suggestions
complexipy . --suggest-refactors

# Show suggestions for failing functions only
complexipy . --failed --suggest-refactors

# Export suggestions to JSON
complexipy . --output-format json --suggest-refactors

Python API

from complexipy import code_complexity

code = """
def process(data):
    if data:
        if data.is_valid():
            return process(data)
    return None
"""

result = code_complexity(code)
for func in result.functions:
    for plan in func.refactor_plans:
        print(f"[{plan.rule_id}] {plan.title}")
        print(f"  Category: {plan.category}")
        print(f"  Applicability: {plan.applicability}")
        print(f"  Reduction: -{plan.estimated_reduction} complexity")
        if plan.suggestion:
            print(f"  Suggested replacement:\n{plan.suggestion.replacement}")
        elif plan.help:
            print(f"  Help: {plan.help}")
        print(f"  Docs: {plan.doc_url}")

JSON Output

The JSON output includes all rule metadata for programmatic consumption:

{
  "rule_id": "C007",
  "kind": "collapsible_if",
  "title": "Merge nested if statements",
  "category": "Readability",
  "applicability": "MachineApplicable",
  "description": "Merge nested if statements into a single if with combined conditions",
  "line_start": 3,
  "line_end": 5,
  "column_start": 5,
  "current_complexity": 4,
  "estimated_reduction": 1,
  "estimated_complexity_after": 3,
  "reduction_is_measured": true,
  "suggestion": {
    "replacement": "    if data and data.is_valid():\n        return process(data)",
    "applicability": "MachineApplicable",
    "spliceable": true,
    "description": "Merge nested conditions into `if data and data.is_valid():`"
  },
  "help": null,
  "explanation": "Nested if statements with a single body can be merged into a single if with combined conditions using 'and'. This reduces nesting and improves readability.",
  "references": [],
  "doc_url": "https://rohaquinlop.github.io/complexipy/refactoring-rules/#c007-collapsible-if"
}

Measured vs estimated reductions

Every plan reports how much applying it lowers the complexity (estimated_reduction / estimated_complexity_after) together with a confidence flag, reduction_is_measured:

  • Measured (true): the rule spliced its suggestion into the real source, re-parsed it, and re-ran the scorer. The number is the literal answer to "apply this and re-score" — exactly what C002 and C007 report, since they carry machine-applicable replacements. The CLI shows these without a qualifier: Reduction: -2 complexity (7 -> 5).
  • Estimated (false): the number comes from the rule's hand-derived formula (help-only rules C001, C003, C004, C011, and the C005 snippet, plus any fallback when a splice cannot be re-parsed). The CLI renders these with a tilde: Estimated reduction: ~-2 complexity (7 -> 5).

A measured reduction of 0 means the suggestion does not actually lower complexity; the plan is dropped rather than shown.


Rule ID Reference

ID Name Category Applicability Priority
C001 Flatten Nested Conditions ▲ Complexity i Informational High
C002 Loop Guards ▲ Complexity * Safe to apply Medium
C003 Extract Helper Function ▲ Complexity i Informational Low
C004 Split Dispatcher ▲ Complexity i Informational Low
C005 Extract Predicate ◆ Readability * Safe to apply Low
C007 Collapsible If ◆ Readability * Safe to apply Highest
C011 Flatten Try/Except ▲ Complexity i Informational Low