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>, orOption<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 Type | Classification |
|---|---|
Self | OwnedSelf |
Result<Self, E> | ResultSelf |
Option<Self> | OptionSelf |
&Self, &mut Self | RefSelf (builder pattern) |
| Other | Other |
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):
- EntryPoint: Main functions, CLI handlers, routes
- Debug: Functions with debug/diagnostic patterns
- Constructor: Simple object construction (enhanced detection)
- EnumConverter: Match-based enum to value conversion
- Accessor: Getters, is_, has_ methods
- DataFlow: High transformation ratio (spec 126)
- PatternMatch: Pattern matching functions
- IOWrapper: File/network I/O thin wrappers
- Orchestrator: Functions coordinating other functions
- 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):
| Role | Multiplier | Rationale |
|---|---|---|
| PureLogic | 1.2 | Core business logic deserves high test priority |
| Unknown | 1.0 | Default, no adjustment |
| EntryPoint | 0.9 | Often integration tested, slight reduction |
| Orchestrator | 0.8 | Coordinates tested functions, reduced priority |
| IOWrapper | 0.7 | Thin I/O wrappers, integration tested |
| PatternMatch | 0.6 | Simple pattern dispatch, lower priority |
| Debug | 0.3 | Diagnostic 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 Range | Method | Formula |
|---|---|---|
| 0-10 | Linear | score (unchanged) |
| 10-100 | Square root | 10.0 + sqrt(score - 10.0) × 3.33 |
| 100+ | Logarithmic | 41.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
- File-Level Scoring: Aggregate file scoring
- Role-Based Adjustments: Detailed role adjustment mechanics
- Rebalanced Scoring: Alternative scoring algorithm
- Data Flow Scoring: Purity-based adjustments