Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Function-Level Scoring

Function-level scoring identifies specific functions needing attention for targeted improvements. This subsection covers the scoring formula, constructor detection, role classification, and role multipliers.

Overview

Function-level scoring combines complexity, coverage, and dependency metrics to calculate a priority score for each function. The formula uses coverage as a multiplicative dampener rather than an additive factor, reflecting that testing gaps amplify existing complexity.

Key Principle: Untested complex code is riskier than well-tested complex code. Coverage acts as a multiplier that reduces the score for well-tested functions and preserves the full score for untested functions.

Scoring Formula

The function-level scoring formula consists of three stages:

Stage 1: Factor Calculation

Complexity Factor (src/priority/scoring/calculation.rs:55-59):

complexity_factor = raw_complexity / 2.0  (clamped to 0-10)

Complexity of 20+ maps to the maximum factor of 10.0. This provides linear scaling with a reasonable cap.

Dependency Factor (src/priority/scoring/calculation.rs:62-66):

dependency_factor = upstream_count / 2.0  (capped at 10.0)

20+ upstream dependencies map to the maximum factor of 10.0.

Stage 2: Base Score Calculation

Without Coverage Data (src/priority/scoring/calculation.rs:119-129):

base_score = (complexity_factor × 10 × 0.50) + (dependency_factor × 10 × 0.25)

Weights:

  • 50% weight on complexity
  • 25% weight on dependencies
  • 25% reserved for debt pattern adjustments

With Coverage Data (src/priority/scoring/calculation.rs:70-82):

coverage_multiplier = 1.0 - coverage_percent
base_score = base_score_no_coverage × coverage_multiplier

Coverage acts as a dampening multiplier:

  • 0% coverage (multiplier = 1.0): Full base score preserved
  • 50% coverage (multiplier = 0.5): Half the base score
  • 100% coverage (multiplier = 0.0): Near-zero score

Stage 3: Role Multiplier

The final score applies a role-based multiplier:

final_score = base_score × role_multiplier

See Role Multipliers for the specific values.

Complete Example

Function: calculate_risk_score()
  Cyclomatic: 12, Cognitive: 18
  Coverage: 20%
  Upstream dependencies: 8

Step 1: Calculate factors
  complexity_factor = (12 + 18) / 2 / 2.0 = 7.5 (capped at 10)
  dependency_factor = 8 / 2.0 = 4.0

Step 2: Base score (no coverage)
  base = (7.5 × 10 × 0.50) + (4.0 × 10 × 0.25)
  base = 37.5 + 10.0 = 47.5

Step 3: Apply coverage multiplier
  coverage_multiplier = 1.0 - 0.20 = 0.80
  score_with_coverage = 47.5 × 0.80 = 38.0

Step 4: Apply role multiplier (PureLogic = 1.2)
  final_score = 38.0 × 1.2 = 45.6

Metrics

Cyclomatic Complexity

Counts decision points (if, match, loops, boolean operators). Guides the number of test cases needed for full branch coverage.

Interpretation:

  • 1-5: Low complexity, easy to test
  • 6-10: Moderate complexity, reasonable test effort
  • 11-20: High complexity, significant test effort
  • 20+: Very high complexity, consider refactoring

Cognitive Complexity

Measures how difficult code is to understand, accounting for:

  • Nesting depth (deeper nesting = higher penalty)
  • Control flow breaks
  • Recursion

Why Cognitive Gets Higher Weight (src/config/scoring.rs:367-373):

  • Cyclomatic weight: 30%
  • Cognitive weight: 70%

Cognitive complexity correlates better with bug density because it measures comprehension difficulty, not just branching paths.

Coverage Percentage

Direct line coverage from LCOV data. Functions with 0% coverage receive maximum urgency.

Coverage Dampening (src/priority/scoring/calculation.rs:8-21):

  • Test code automatically receives 0.0 multiplier (near-zero score)
  • Production code: multiplier = 1.0 - coverage_percent

Dependency Count

Upstream callers indicate impact radius. Functions with many callers are riskier to modify.

Constructor Detection

Debtmap identifies simple constructors to prevent false positives where trivial initialization functions are flagged as critical business logic.

Detection Strategy

A function is classified as a constructor if it meets these criteria (src/analyzers/rust_constructor_detector.rs:1-50):

1. Name Pattern Match:

  • Exact: new, default, empty, zero, any
  • Prefix: from_*, with_*, create_*, make_*, build_*

2. Complexity Thresholds:

  • Cyclomatic complexity: <= 2
  • Cognitive complexity: <= 3
  • Function length: < 15 lines
  • Nesting depth: <= 1

3. AST Analysis (when enabled):

  • Return type: Self, Result<Self, E>, or Option<Self>
  • Body pattern: Struct initialization, no loops
  • No complex control flow

Return Type Classification

The AST detector classifies return types (src/analyzers/rust_constructor_detector.rs:36-42):

Return TypeClassification
SelfOwnedSelf
Result<Self, E>ResultSelf
Option<Self>OptionSelf
&Self, &mut SelfRefSelf (builder pattern)
OtherOther

Body Pattern Analysis

The constructor detector visits the function body (src/analyzers/rust_constructor_detector.rs:104-130):

#![allow(unused)]
fn main() {
// Tracks these patterns:
struct BodyPattern {
    struct_init_count: usize,  // Struct initialization expressions
    self_refs: usize,          // References to Self
    field_assignments: usize,  // Field assignment expressions
    has_if: bool,              // Contains if expression
    has_match: bool,           // Contains match expression
    has_loop: bool,            // Contains any loop
    early_returns: usize,      // Return statements
}
}

Constructor-Like Pattern (src/analyzers/rust_constructor_detector.rs:152-158):

  • Has struct initialization AND no loops, OR
  • Has Self references AND no loops AND no match AND no field assignments

Examples

Detected as Constructor (classified as IOWrapper, score reduced):

#![allow(unused)]
fn main() {
fn new() -> Self {
    Self { field: 0 }
}

fn from_config(config: Config) -> Self {
    Self {
        timeout: config.timeout,
        retries: 3,
    }
}

fn try_new(value: i32) -> Result<Self, Error> {
    if value > 0 {
        Ok(Self { value })
    } else {
        Err(Error::InvalidValue)
    }
}
}

NOT Detected as Constructor (remains PureLogic):

#![allow(unused)]
fn main() {
// Has loop - disqualified
fn process_items() -> Self {
    let mut result = Self::new();
    for item in items {
        result.add(item);
    }
    result
}

// High complexity - disqualified
fn create_complex(data: Data) -> Result<Self> {
    validate(&data)?;
    // ... 30 lines of logic
    Ok(Self { ... })
}
}

Role Classification

Functions are classified by semantic role to adjust their priority scores appropriately.

Classification Order

The classifier applies rules in precedence order (src/priority/semantic_classifier/mod.rs:47-114):

  1. EntryPoint: Main functions, CLI handlers, routes
  2. Debug: Functions with debug/diagnostic patterns
  3. Constructor: Simple object construction (enhanced detection)
  4. EnumConverter: Match-based enum to value conversion
  5. Accessor: Getters, is_, has_ methods
  6. DataFlow: High transformation ratio (spec 126)
  7. PatternMatch: Pattern matching functions
  8. IOWrapper: File/network I/O thin wrappers
  9. Orchestrator: Functions coordinating other functions
  10. PureLogic: Default for unclassified functions

Entry Point Detection

Entry points are identified by:

  • Call graph analysis: No upstream callers
  • Name patterns: main, handle_*, run_*, execute_*

Debug Function Detection

Debug/diagnostic functions are detected via (src/priority/semantic_classifier/mod.rs:59-61):

  • Name patterns: debug_*, print_*, dump_*, trace_*, *_diagnostics, *_stats
  • Low complexity threshold
  • Output-focused behavior

Accessor Detection

Accessor methods are identified when (src/priority/semantic_classifier/mod.rs:147-177):

  • Name matches accessor pattern: id, name, get_*, is_*, has_*, as_*, to_*
  • Cyclomatic complexity <= 2
  • Cognitive complexity <= 1
  • Length < 10 lines
  • (With AST) Simple field access body

Role Multipliers

Each role receives a score multiplier based on test priority importance (src/config/scoring.rs:307-333):

RoleMultiplierRationale
PureLogic1.2Core business logic deserves high test priority
Unknown1.0Default, no adjustment
EntryPoint0.9Often integration tested, slight reduction
Orchestrator0.8Coordinates tested functions, reduced priority
IOWrapper0.7Thin I/O wrappers, integration tested
PatternMatch0.6Simple pattern dispatch, lower priority
Debug0.3Diagnostic functions, lowest priority

Multiplier Rationale

PureLogic (1.2x): Business rules and algorithms should have comprehensive unit tests. They’re easy to test in isolation and contain the core value of the application.

Orchestrator (0.8x): Orchestrators coordinate other tested functions. If the delegated functions are well-tested, the orchestrator is partially covered through integration.

IOWrapper (0.7x): Thin I/O wrappers are often tested via integration tests. Unit testing them provides limited value compared to integration testing.

Debug (0.3x): Diagnostic and debug functions have the lowest test priority. They’re not production-critical and are often exercised manually during development.

Configuration

Role multipliers are configurable in .debtmap.toml:

[role_multipliers]
pure_logic = 1.2
orchestrator = 0.8
io_wrapper = 0.7
entry_point = 0.9
pattern_match = 0.6
debug = 0.3
unknown = 1.0

Role Multiplier Clamping

To prevent extreme score swings, multipliers can be clamped (src/config/scoring.rs:457-493):

[scoring.role_multiplier]
clamp_min = 0.3    # Floor for all multipliers
clamp_max = 1.8    # Ceiling for all multipliers
enable_clamping = true

Complexity Weight Configuration

The balance between cyclomatic and cognitive complexity is configurable (src/config/scoring.rs:335-381):

[complexity_weights]
cyclomatic = 0.3   # 30% weight
cognitive = 0.7    # 70% weight
max_cyclomatic = 50.0
max_cognitive = 100.0

Default Rationale:

  • Cognitive complexity (70%) correlates better with bug density
  • Cyclomatic complexity (30%) guides test case count
  • Combined weighting provides balanced assessment

Score Normalization

Raw scores undergo normalization for display (src/priority/scoring/calculation.rs:174-206):

Score RangeMethodFormula
0-10Linearscore (unchanged)
10-100Square root10.0 + sqrt(score - 10.0) × 3.33
100+Logarithmic41.59 + ln(score / 100.0) × 10.0

This multi-phase approach:

  • Preserves distinctions for low scores
  • Moderately dampens medium scores
  • Strongly dampens extreme values

See Also