Debtmap is a code complexity and technical debt analyzer written in Rust. It analyzes Rust, Python, JavaScript, and TypeScript codebases to identify complexity patterns, technical debt, and architectural issues.
- Functional-first design - Pure functions with minimal side effects
- Immutable data structures - Use
imcrate for persistent collections - Composable analysis pipelines - Chain pure transformations
- Incremental progress over big bangs - Small, testable changes
- Type-driven development - Let the type system guide correctness
- Performance through parallelism - Use
rayonfor data parallelism
- Prefer iterators over loops - Use
.map(),.filter(),.fold()for transformations - Leverage trait composition - Build complex behavior through trait bounds
- Minimize mutable state - Use
Cow<'_, T>or immutable builders - Pure computation cores - Separate parsing, analysis, and I/O concerns
- Result chains - Use
?operator and monadic patterns for error handling
- Zero-copy where possible - Use string slices and borrowed data
- Parallel processing - Use
rayon::par_iter()for CPU-intensive analysis - Lazy evaluation - Compute only what's needed when needed
- Efficient collections - Use
dashmap::DashMapfor concurrent access
- Phantom types - Encode state transitions in types
- Builder patterns - Ensure correct construction through types
- Exhaustive pattern matching - Handle all enum variants explicitly
- Newtype wrappers - Prevent mixing incompatible values
src/
├── core/ # Core data types and utilities
├── analyzers/ # Language-specific parsers and analyzers
├── analysis/ # Advanced analysis algorithms
├── debt/ # Technical debt detection patterns
├── complexity/ # Complexity calculation algorithms
├── risk/ # Risk assessment and prioritization
├── io/ # Input/output formatting and handling
└── testing/ # Test analysis and quality metrics
- Parser Layer - Language-specific AST generation (pure)
- Analysis Layer - Metric computation and pattern detection (pure)
- Aggregation Layer - Combine results across files (functional)
- Output Layer - Format and emit results (I/O boundary)
// Good: Functional pipeline
fn analyze_project(config: &Config) -> Result<AnalysisResults> {
discover_files(config)?
.into_par_iter()
.map(|path| parse_file(&path))
.collect::<Result<Vec<_>>>()?
.into_iter()
.map(|ast| analyze_complexity(&ast))
.fold(AnalysisResults::empty(), |acc, result| acc.merge(result))
}
// Avoid: Imperative with mutation
fn analyze_project_bad(config: &Config) -> Result<AnalysisResults> {
let mut results = AnalysisResults::new();
for file in discover_files(config)? {
let ast = parse_file(&file)?;
let metrics = analyze_complexity(&ast);
results.add_metrics(metrics); // Mutation!
}
Ok(results)
}- Maximum function length: 20 lines (prefer 5-10)
- Maximum cyclomatic complexity: 5
- Maximum nesting depth: 2 levels
- Maximum parameters: 3-4 (use structs for more)
// Good: Pure function with clear input/output
fn calculate_cognitive_complexity(ast: &Ast) -> u32 {
ast.functions()
.map(|func| func.cognitive_weight())
.sum()
}
// Good: Pure transformation
fn normalize_path(path: &Path) -> PathBuf {
path.components()
.filter(|c| !matches!(c, Component::CurDir))
.collect()
}
// Avoid: Side effects mixed with computation
fn calculate_complexity_bad(ast: &Ast) -> u32 {
println!("Calculating complexity..."); // Side effect!
let mut total = 0;
for func in ast.functions() {
total += func.cognitive_weight(); // Mutation!
}
total
}use anyhow::{Context, Result};
// Good: Functional error handling
fn parse_and_analyze(content: &str, path: &Path) -> Result<FileMetrics> {
parse_content(content, path)
.context("Failed to parse file")?
.analyze()
.context("Analysis failed")
}
// Good: Monadic chaining with custom extension
fn process_file(path: &Path) -> Result<AnalysisResult> {
read_file(path)?
.map_ok(|content| preprocess_content(&content))
.and_then_async(|content| parse_content(&content, path))
.map_ok(|ast| analyze_ast(&ast))
}- Unit tests: In same file as implementation using
#[cfg(test)] - Integration tests: In
tests/directory for cross-module scenarios - Property tests: Use
proptestfor invariant verification - Benchmarks: Use
criterionfor performance-critical code
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use pretty_assertions::assert_eq;
#[test]
fn pure_function_deterministic() {
let input = create_test_ast();
let result1 = calculate_complexity(&input);
let result2 = calculate_complexity(&input);
assert_eq!(result1, result2);
}
proptest! {
#[test]
fn complexity_never_negative(
functions in prop::collection::vec(any::<FunctionMetrics>(), 0..100)
) {
let total = calculate_total_complexity(&functions);
prop_assert!(total >= 0);
}
}
}- Functions under 20 lines with single responsibility
- Pure functions separated from I/O operations
- Comprehensive test coverage (aim for 85%+)
- No clippy warnings with
#![deny(clippy::all)] - Formatted with
cargo fmt - Documented public APIs with examples
- Benchmarks for performance-critical paths
- Integration tests for user-facing features
# This command must pass before any commit
just fmt
just test- Multi-platform testing - Ubuntu and macOS
- All feature combinations - Test with/without optional features
- Security auditing -
cargo denyfor vulnerabilities - Documentation generation - Ensure docs build without warnings
- New code follows functional patterns
- Side effects isolated to I/O boundaries
- Data transformations are pure and testable
- Error handling uses
Resultchains appropriately - Performance implications considered (parallel vs sequential)
- Appropriate use of
Clonevs borrowing - Memory allocations minimized in hot paths
- Proper
Send + Syncbounds for parallel code - No unnecessary
.unwrap()calls (use?or proper error handling) - Lifetime annotations only where necessary
// Good: Parallel analysis of independent files
files.into_par_iter()
.map(|file| analyze_file_complexity(&file))
.collect::<Result<Vec<_>>>()
// Good: Parallel aggregation with reduce
results.par_iter()
.map(|result| extract_metrics(result))
.reduce(|| Metrics::default(), |a, b| a.merge(b))// Good: Streaming processing for large datasets
fn process_large_codebase(files: impl Iterator<Item = PathBuf>) -> impl Iterator<Item = FileMetrics> {
files
.map(|path| analyze_file(&path))
.filter_map(Result::ok)
}
// Avoid: Loading everything into memory
fn process_large_codebase_bad(files: Vec<PathBuf>) -> Vec<FileMetrics> {
files.into_iter()
.map(|path| analyze_file(&path).unwrap())
.collect() // Memory explosion!
}pub struct AnalysisConfigBuilder<State> {
config: AnalysisConfig,
_state: PhantomData<State>,
}
impl AnalysisConfigBuilder<Incomplete> {
pub fn complexity_threshold(mut self, threshold: u32) -> AnalysisConfigBuilder<HasThreshold> {
self.config.complexity_threshold = threshold;
AnalysisConfigBuilder {
config: self.config,
_state: PhantomData,
}
}
}
impl AnalysisConfigBuilder<HasThreshold> {
pub fn build(self) -> AnalysisConfig {
self.config
}
}// Good: Functional update pattern
fn update_metrics(metrics: FileMetrics, new_function: FunctionMetrics) -> FileMetrics {
FileMetrics {
functions: metrics.functions.update(new_function),
..metrics
}
}
// Good: Using im crate for persistent collections
fn add_debt_item(report: TechnicalDebtReport, item: DebtItem) -> TechnicalDebtReport {
TechnicalDebtReport {
items: report.items.push_back(item),
..report
}
}- Module-level docs - Explain purpose and usage patterns
- Function docs - Include examples for public APIs
- Error docs - Document error conditions and recovery
- Architecture docs - Maintain ARCHITECTURE.md for high-level design
- rustfmt - Code formatting (
cargo fmt) - clippy - Linting and best practices (
cargo clippy) - cargo-deny - License and security auditing
- criterion - Benchmarking for performance-critical code
- proptest - Property-based testing
Recommended VS Code extensions:
- rust-analyzer
- Better TOML
- GitLens
- Thunder Client (for API testing)
Use these agents proactively when working on debtmap:
- refactor-assistant: Extract complex analysis functions into pure, composable pieces
- test-runner: Ensure all tests pass before commits, especially integration tests
- code-reviewer: Review functional programming adherence and Rust idioms
- file-ops: Organize analysis output files and manage test data
- git-ops: Handle feature branches and ensure clean commit history
NEVER:
- Mix computation with I/O in the same function
- Use
.unwrap()in production code paths - Ignore clippy warnings (fix or explicitly allow with justification)
- Commit failing tests or non-compiling code
- Introduce performance regressions without benchmarking
ALWAYS:
- Write tests for pure functions (they're easy to test!)
- Use
Result<T>for fallible operations - Prefer iterator chains over manual loops
- Document complex analysis algorithms
- Run full test suite before pushing (
just test) - Use appropriate parallel processing for CPU-intensive analysis
This project exemplifies functional programming principles in Rust while maintaining high performance for code analysis tasks.
- don't use release build only debug build for testing changes locally