use super::sections;
const HEADROOM_FACTOR: f64 = 1.2;
pub struct ProjectMetrics {
pub file_count: usize,
pub function_count: usize,
pub max_cognitive: usize,
pub max_cyclomatic: usize,
pub max_nesting_depth: usize,
pub max_function_lines: usize,
}
pub fn extract_init_metrics(
file_count: usize,
results: &[crate::analyzer::FunctionAnalysis],
) -> ProjectMetrics {
let mut max_cognitive = 0usize;
let mut max_cyclomatic = 0usize;
let mut max_nesting_depth = 0usize;
let mut max_function_lines = 0usize;
for r in results {
if let Some(ref cx) = r.complexity {
max_cognitive = max_cognitive.max(cx.cognitive_complexity);
max_cyclomatic = max_cyclomatic.max(cx.cyclomatic_complexity);
max_nesting_depth = max_nesting_depth.max(cx.max_nesting);
max_function_lines = max_function_lines.max(cx.function_lines);
}
}
ProjectMetrics {
file_count,
function_count: results.len(),
max_cognitive,
max_cyclomatic,
max_nesting_depth,
max_function_lines,
}
}
pub fn generate_default_config() -> &'static str {
r#"# rustqual.toml — Configuration for the rustqual code quality analyzer
#
# Place this file in your project root.
# Run `rustqual --init` to generate this file.
# ── Function Classification ──────────────────────────────────────────────
# Function names (or glob patterns) to exclude from analysis.
# Examples: "main", "test_*", "visit_*"
ignore_functions = [
"main",
"test_*",
]
# Glob patterns for files to exclude from analysis.
# Examples: "generated/**", "tests/**"
exclude_files = []
# If true, closures count as "logic" even when passed to iterator adaptors.
# Default: false (lenient — closures inside .map()/.filter() are ignored).
strict_closures = false
# If true, iterator chains (.map, .filter, .fold, ...) count as own calls.
# Default: false.
strict_iterator_chains = false
# If true, recursive calls (function calling itself) are allowed and don't
# count as violations. Default: false.
allow_recursion = false
# If true, the ? operator counts as logic (implicit control flow).
# Default: false.
strict_error_propagation = false
# ── Suppression Health ───────────────────────────────────────────────────
# Maximum ratio of suppressed functions before a warning is emitted.
# Default: 0.05 (5%).
max_suppression_ratio = 0.05
# If true, exit with code 1 when warnings are present (e.g. suppression ratio exceeded).
# Default: false. Use --fail-on-warnings CLI flag to enable.
fail_on_warnings = false
# ── Complexity Analysis ──────────────────────────────────────────────────
[complexity]
enabled = true
max_cognitive = 15
max_cyclomatic = 10
include_nesting_penalty = true
detect_magic_numbers = true
allowed_magic_numbers = ["0", "1", "-1", "2"]
# ── DRY / Duplicate Detection ───────────────────────────────────────────
[duplicates]
enabled = true
similarity_threshold = 0.85
min_tokens = 30
min_lines = 5
min_statements = 3
ignore_tests = true
ignore_trait_impls = true
detect_dead_code = true
detect_wildcard_imports = true
detect_repeated_matches = true
# ── Boilerplate Detection ───────────────────────────────────────────────
[boilerplate]
enabled = true
# Optional: limit to specific patterns (empty = all patterns).
# patterns = ["BP-001", "BP-003"]
suggest_crates = true
# ── SRP (Single Responsibility) ─────────────────────────────────────────
[srp]
enabled = true
smell_threshold = 0.6
max_fields = 12
max_methods = 20
max_fan_out = 10
lcom4_threshold = 2
weights = [0.4, 0.25, 0.15, 0.2]
file_length_baseline = 300
file_length_ceiling = 800
max_independent_clusters = 3
min_cluster_statements = 5
# Maximum number of parameters before a function triggers SRP-004.
max_parameters = 5
# ── Coupling Analysis ───────────────────────────────────────────────────
[coupling]
enabled = true
max_instability = 0.8
max_fan_in = 15
max_fan_out = 12
# Check Stable Dependencies Principle (stable modules should not depend on unstable ones).
check_sdp = true
# ── Test Quality Analysis ──────────────────────────────────────────────
[test]
enabled = true
# Optional: path to LCOV coverage file for TQ-004/TQ-005 checks.
# coverage_file = "lcov.info"
# ── Quality Score Weights ──────────────────────────────────────────────
# Weights for each dimension in the overall quality score.
# Must sum to approximately 1.0.
[weights]
iosp = 0.25
complexity = 0.20
dry = 0.15
srp = 0.20
coupling = 0.10
test = 0.10
"#
}
fn compute_tailored_thresholds(m: &ProjectMetrics) -> [usize; 4] {
let cognitive = ((m.max_cognitive as f64 * HEADROOM_FACTOR).ceil() as usize)
.max(sections::DEFAULT_MAX_COGNITIVE);
let cyclomatic = ((m.max_cyclomatic as f64 * HEADROOM_FACTOR).ceil() as usize)
.max(sections::DEFAULT_MAX_CYCLOMATIC);
let nesting = ((m.max_nesting_depth as f64 * HEADROOM_FACTOR).ceil() as usize)
.max(sections::DEFAULT_MAX_NESTING_DEPTH);
let function_lines = ((m.max_function_lines as f64 * HEADROOM_FACTOR).ceil() as usize)
.max(sections::DEFAULT_MAX_FUNCTION_LINES);
[cognitive, cyclomatic, nesting, function_lines]
}
fn format_tailored_config(m: &ProjectMetrics, thresholds: &[usize; 4]) -> String {
let [cognitive, cyclomatic, nesting, function_lines] = *thresholds;
format!(
r#"# rustqual.toml — Tailored configuration for your project
# Generated from analysis of {file_count} file(s), {function_count} function(s).
#
# Thresholds are set to your current maximums + 20% headroom.
# Tighten them over time as you improve code quality.
# ── Function Classification ──────────────────────────────────────────────
ignore_functions = ["main", "test_*"]
exclude_files = []
strict_closures = false
strict_iterator_chains = false
allow_recursion = false
strict_error_propagation = false
# ── Suppression Health ───────────────────────────────────────────────────
max_suppression_ratio = 0.05
fail_on_warnings = false
# ── Complexity Analysis ──────────────────────────────────────────────────
[complexity]
enabled = true
max_cognitive = {cognitive} # current max: {max_cog}
max_cyclomatic = {cyclomatic} # current max: {max_cyc}
max_nesting_depth = {nesting} # current max: {max_nest}
max_function_lines = {function_lines} # current max: {max_lines}
include_nesting_penalty = true
detect_magic_numbers = true
detect_unsafe = true
detect_error_handling = true
allowed_magic_numbers = ["0", "1", "-1", "2"]
# ── DRY / Duplicate Detection ───────────────────────────────────────────
[duplicates]
enabled = true
similarity_threshold = 0.85
min_tokens = 30
min_lines = 5
min_statements = 3
ignore_tests = true
ignore_trait_impls = true
detect_dead_code = true
detect_wildcard_imports = true
detect_repeated_matches = true
# ── Boilerplate Detection ───────────────────────────────────────────────
[boilerplate]
enabled = true
suggest_crates = true
# ── SRP (Single Responsibility) ─────────────────────────────────────────
[srp]
enabled = true
smell_threshold = 0.6
max_fields = 12
max_methods = 20
max_fan_out = 10
lcom4_threshold = 2
weights = [0.4, 0.25, 0.15, 0.2]
file_length_baseline = 300
file_length_ceiling = 800
max_independent_clusters = 3
min_cluster_statements = 5
max_parameters = 5
# ── Coupling Analysis ───────────────────────────────────────────────────
[coupling]
enabled = true
max_instability = 0.8
max_fan_in = 15
max_fan_out = 12
check_sdp = true
# ── Test Quality Analysis ──────────────────────────────────────────────
[test]
enabled = true
# coverage_file = "lcov.info"
# ── Quality Score Weights ──────────────────────────────────────────────
# Must sum to approximately 1.0.
[weights]
iosp = 0.25
complexity = 0.20
dry = 0.15
srp = 0.20
coupling = 0.10
test = 0.10
"#,
file_count = m.file_count,
function_count = m.function_count,
max_cog = m.max_cognitive,
max_cyc = m.max_cyclomatic,
max_nest = m.max_nesting_depth,
max_lines = m.max_function_lines,
)
}
pub fn generate_tailored_config(m: &ProjectMetrics) -> String {
let thresholds = compute_tailored_thresholds(m);
format_tailored_config(m, &thresholds)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
#[test]
fn test_generate_default_config_is_valid_toml() {
let content = generate_default_config();
let result: Result<Config, _> = toml::from_str(content);
assert!(
result.is_ok(),
"Generated config must be valid TOML: {:?}",
result.err()
);
}
#[test]
fn test_generate_default_config_contents() {
let content = generate_default_config();
assert!(content.contains("ignore_functions"));
assert!(content.contains("strict_closures"));
assert!(content.contains("allow_recursion"));
assert!(content.contains("strict_error_propagation"));
assert!(content.contains("[complexity]"));
assert!(content.contains("[duplicates]"));
assert!(content.contains("[boilerplate]"));
assert!(content.contains("[srp]"));
assert!(content.contains("[coupling]"));
assert!(content.contains("max_suppression_ratio"));
assert!(content.contains("fail_on_warnings"));
assert!(content.contains("[weights]"));
assert!(content.contains("iosp = 0.25"));
assert!(content.contains("coupling = 0.10"));
}
#[test]
fn test_generate_tailored_config_is_valid_toml() {
let metrics = ProjectMetrics {
file_count: 10,
function_count: 50,
max_cognitive: 12,
max_cyclomatic: 8,
max_nesting_depth: 3,
max_function_lines: 45,
};
let content = generate_tailored_config(&metrics);
let result: Result<Config, _> = toml::from_str(&content);
assert!(
result.is_ok(),
"Tailored config must be valid TOML: {:?}",
result.err()
);
}
#[test]
fn test_generate_tailored_config_uses_headroom() {
let metrics = ProjectMetrics {
file_count: 5,
function_count: 20,
max_cognitive: 20,
max_cyclomatic: 15,
max_nesting_depth: 5,
max_function_lines: 80,
};
let content = generate_tailored_config(&metrics);
let cfg: Config = toml::from_str(&content).unwrap();
assert_eq!(cfg.complexity.max_cognitive, 24);
assert_eq!(cfg.complexity.max_cyclomatic, 18);
assert_eq!(cfg.complexity.max_nesting_depth, 6);
assert_eq!(cfg.complexity.max_function_lines, 96);
}
#[test]
fn test_generate_tailored_config_respects_minimums() {
let metrics = ProjectMetrics {
file_count: 1,
function_count: 2,
max_cognitive: 3,
max_cyclomatic: 2,
max_nesting_depth: 1,
max_function_lines: 10,
};
let content = generate_tailored_config(&metrics);
let cfg: Config = toml::from_str(&content).unwrap();
assert_eq!(
cfg.complexity.max_cognitive,
sections::DEFAULT_MAX_COGNITIVE
);
assert_eq!(
cfg.complexity.max_cyclomatic,
sections::DEFAULT_MAX_CYCLOMATIC
);
assert_eq!(
cfg.complexity.max_nesting_depth,
sections::DEFAULT_MAX_NESTING_DEPTH
);
assert_eq!(
cfg.complexity.max_function_lines,
sections::DEFAULT_MAX_FUNCTION_LINES
);
}
#[test]
fn test_generate_tailored_config_includes_metrics_comments() {
let metrics = ProjectMetrics {
file_count: 42,
function_count: 100,
max_cognitive: 10,
max_cyclomatic: 8,
max_nesting_depth: 3,
max_function_lines: 50,
};
let content = generate_tailored_config(&metrics);
assert!(content.contains("42 file(s)"));
assert!(content.contains("100 function(s)"));
assert!(content.contains("current max: 10"));
assert!(content.contains("current max: 8"));
}
}