use super::sections;
use std::path::Path;
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 prepare_init_content(path: &Path) -> String {
let files = crate::adapters::source::filesystem::collect_rust_files(path);
if files.is_empty() {
return generate_default_config().to_string();
}
let metrics = compute_project_metrics(&files, path);
generate_tailored_config(&metrics)
}
fn compute_project_metrics(files: &[std::path::PathBuf], path: &Path) -> ProjectMetrics {
use crate::adapters::analyzers::iosp::{scope::ProjectScope, Analyzer};
use crate::adapters::source::filesystem::read_and_parse_files;
use crate::config::Config;
let parsed = read_and_parse_files(files, path);
let default_config = Config::default();
let scope_refs: Vec<(&str, &syn::File)> =
parsed.iter().map(|(p, _, f)| (p.as_str(), f)).collect();
let scope = ProjectScope::from_files(&scope_refs);
let analyzer = Analyzer::new(&default_config, &scope);
let all_results: Vec<_> = parsed
.iter()
.flat_map(|(p, _, syntax)| analyzer.analyze_file(syntax, p))
.collect();
extract_init_metrics(files.len(), &all_results)
}
pub fn extract_init_metrics(
file_count: usize,
results: &[crate::adapters::analyzers::iosp::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 = 2
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_quality]
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.22
complexity = 0.18
dry = 0.13
srp = 0.18
coupling = 0.09
test_quality = 0.10
architecture = 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 = 2
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_quality]
enabled = true
# coverage_file = "lcov.info"
# ── Quality Score Weights ──────────────────────────────────────────────
# Must sum to approximately 1.0.
[weights]
iosp = 0.22
complexity = 0.18
dry = 0.13
srp = 0.18
coupling = 0.09
test_quality = 0.10
architecture = 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)
}