#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::colors as c;
use crate::cli::commands::{QddCodeType, QddCommands, QddOutputFormat, QddQualityProfile};
use crate::qdd::{
CodeType, CreateSpec, Parameter, QddOperation, QddResult, QddTool, QualityProfile, RefactorSpec,
};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_qdd_command(command: QddCommands) -> Result<()> {
match command {
QddCommands::Create {
code_type,
name,
purpose,
profile,
input,
output,
output_file,
} => {
handle_qdd_create(
code_type,
name,
purpose,
profile,
input,
output,
output_file,
)
.await
}
QddCommands::Refactor {
file,
function,
profile,
max_complexity,
min_coverage,
output,
dry_run,
} => {
handle_qdd_refactor(
file,
function,
profile,
max_complexity,
min_coverage,
output,
dry_run,
)
.await
}
QddCommands::Validate {
path,
profile,
format,
output,
strict,
} => handle_qdd_validate(path, profile, format, output, strict).await,
}
}
async fn handle_qdd_create(
code_type: QddCodeType,
name: String,
purpose: String,
profile: QddQualityProfile,
inputs: Vec<(String, String)>,
output_type: String,
output_file: Option<PathBuf>,
) -> Result<()> {
let qdd_code_type = convert_code_type(code_type);
let quality_profile = convert_quality_profile(profile);
let parameters = convert_parameters(inputs);
let create_spec = build_create_spec(qdd_code_type, name, purpose, parameters, output_type);
let result = execute_create_operation(quality_profile, create_spec).await?;
display_create_results(profile, &result);
output_generated_code(output_file, &result)?;
Ok(())
}
fn convert_code_type(code_type: QddCodeType) -> CodeType {
match code_type {
QddCodeType::Function => CodeType::Function,
QddCodeType::Module => CodeType::Module,
QddCodeType::Service => CodeType::Service,
QddCodeType::Test => CodeType::Test,
}
}
fn convert_quality_profile(profile: QddQualityProfile) -> QualityProfile {
match profile {
QddQualityProfile::Extreme => QualityProfile::extreme(),
QddQualityProfile::Standard => QualityProfile::standard(),
QddQualityProfile::Relaxed => QualityProfile::relaxed(),
}
}
fn convert_parameters(inputs: Vec<(String, String)>) -> Vec<Parameter> {
inputs
.into_iter()
.map(|(param_type, param_name)| Parameter {
name: param_name,
param_type,
description: None,
})
.collect()
}
fn build_create_spec(
code_type: CodeType,
name: String,
purpose: String,
inputs: Vec<Parameter>,
output_type: String,
) -> CreateSpec {
CreateSpec {
code_type,
name,
purpose,
inputs,
outputs: Parameter {
name: "result".to_string(),
param_type: output_type,
description: Some("Function output".to_string()),
},
}
}
async fn execute_create_operation(
quality_profile: QualityProfile,
create_spec: CreateSpec,
) -> Result<QddResult> {
let qdd_tool = QddTool::with_profile(quality_profile);
let operation = QddOperation::Create(create_spec);
qdd_tool.execute(operation).await
}
fn display_create_results(profile: QddQualityProfile, result: &QddResult) {
println!("{}", c::header("QDD Template Generated"));
println!("{}", c::pass(&format!("Quality Profile: {profile:?}")));
let is_stub = result.code.contains("todo!");
if is_stub {
println!(
" {}",
c::warn("Template only: the generated body is `todo!()` — nothing is implemented yet")
);
}
println!(
" {} {} {}",
c::label("Template complexity (estimated):"),
c::number(&format!("{}", result.quality_score.complexity)),
c::dim("(keyword heuristic over the template)")
);
println!(
" {} {} {}",
c::label("Template quality score (estimated):"),
c::number(&format!("{:.1}", result.quality_score.overall)),
c::dim("(derived from the estimate above, not an analysis of your code)")
);
println!(
" {} {}",
c::label("Coverage:"),
c::dim("not measured (no tests were executed)")
);
println!(" {} {}", c::label("TDG Score:"), c::dim("not measured"));
println!();
}
fn output_generated_code(output_file: Option<PathBuf>, result: &QddResult) -> Result<()> {
if let Some(output_path) = output_file {
let source = format!("{}\n\n{}\n", result.code, result.tests);
std::fs::write(&output_path, source)?;
println!(
"{}",
c::pass(&format!(
"Generated code written to: {}",
c::path(&output_path.display().to_string())
))
);
if !result.documentation.trim().is_empty() {
let mut doc_path = output_path.with_extension("md");
if doc_path == output_path {
doc_path = output_path.with_extension("doc.md");
}
std::fs::write(&doc_path, format!("{}\n", result.documentation))?;
println!(
"{}",
c::pass(&format!(
"Generated documentation written to: {}",
c::path(&doc_path.display().to_string())
))
);
}
} else {
println!("{}", c::subheader("Generated Code:"));
println!("{}", result.code);
println!("\n{}", c::subheader("Generated Tests:"));
println!("{}", result.tests);
println!("\n{}", c::subheader("Generated Documentation:"));
println!("{}", result.documentation);
}
Ok(())
}
async fn handle_qdd_refactor(
file: PathBuf,
function: Option<String>,
profile: QddQualityProfile,
max_complexity: Option<u32>,
min_coverage: Option<u32>,
output: Option<PathBuf>,
dry_run: bool,
) -> Result<()> {
validate_file_exists(&file)?;
let quality_profile = create_quality_profile(profile, max_complexity, min_coverage);
let refactor_spec = create_refactor_spec(&file, function.clone(), &quality_profile);
if dry_run {
return handle_dry_run(&file, &function, profile, &quality_profile);
}
let result = execute_refactoring(quality_profile, refactor_spec).await?;
display_refactor_results(&file, function, profile, &result);
save_refactored_code(&output.unwrap_or(file), &result.code)?;
display_rollback_info(&result);
Ok(())
}
fn validate_file_exists(file: &Path) -> Result<()> {
if !file.exists() {
return Err(anyhow::anyhow!("File does not exist: {}", file.display()));
}
Ok(())
}
fn create_quality_profile(
profile: QddQualityProfile,
max_complexity: Option<u32>,
min_coverage: Option<u32>,
) -> QualityProfile {
let mut quality_profile = match profile {
QddQualityProfile::Extreme => QualityProfile::extreme(),
QddQualityProfile::Standard => QualityProfile::standard(),
QddQualityProfile::Relaxed => QualityProfile::relaxed(),
};
if let Some(complexity) = max_complexity {
quality_profile.thresholds.max_complexity = complexity;
}
if let Some(coverage) = min_coverage {
quality_profile.thresholds.min_coverage = coverage;
}
quality_profile
}
fn create_refactor_spec(
file: &Path,
function: Option<String>,
quality_profile: &QualityProfile,
) -> RefactorSpec {
RefactorSpec {
file_path: file.to_path_buf(),
function_name: function,
target_metrics: quality_profile.thresholds.clone(),
}
}
fn handle_dry_run(
file: &Path,
function: &Option<String>,
profile: QddQualityProfile,
quality_profile: &QualityProfile,
) -> Result<()> {
println!(
"{}",
c::dim(&format!(
"DRY RUN: Would refactor file: {}",
c::path(&file.display().to_string())
))
);
if let Some(func) = function {
println!(" {} {}", c::label("Target function:"), func);
}
println!(" {} {profile:?}", c::label("Quality profile:"));
println!(
" {} {}",
c::label("Max complexity:"),
c::number(&format!("{}", quality_profile.thresholds.max_complexity))
);
println!(
" {} {}",
c::label("Min coverage:"),
c::pct(quality_profile.thresholds.min_coverage as f64, 80.0, 60.0)
);
println!(
"{}",
c::warn("Use without --dry-run to execute refactoring")
);
Ok(())
}
async fn execute_refactoring(
quality_profile: QualityProfile,
refactor_spec: RefactorSpec,
) -> Result<QddResult> {
let qdd_tool = QddTool::with_profile(quality_profile);
let operation = QddOperation::Refactor(refactor_spec);
qdd_tool.execute(operation).await
}
fn display_refactor_results(
file: &Path,
function: Option<String>,
profile: QddQualityProfile,
result: &QddResult,
) {
print!(
"{}",
format_refactor_results(file, function, profile, result)
);
}
fn format_refactor_results(
file: &Path,
function: Option<String>,
profile: QddQualityProfile,
result: &QddResult,
) -> String {
let mut out = String::new();
out.push_str(&format!("{}\n", c::header("QDD Refactoring Successful!")));
out.push_str(&format!(
" {} {}\n",
c::label("File:"),
c::path(&file.display().to_string())
));
if let Some(func) = function {
out.push_str(&format!(" {} {}\n", c::label("Function:"), func));
}
out.push_str(&format!(
"{}\n",
c::pass(&format!("Quality Profile: {profile:?}"))
));
out.push_str(&format!(
" {} {} {}\n",
c::label("Quality score (estimated):"),
c::number(&format!("{:.1}", result.quality_score.overall)),
c::dim("(derived from the estimates below, not an analysis run)")
));
out.push_str(&format!(
" {} {} {}\n",
c::label("Complexity (estimated):"),
c::number(&format!("{}", result.quality_score.complexity)),
c::dim("(keyword heuristic over the refactored text)")
));
out.push_str(&format!(
" {} {}\n",
c::label("Coverage:"),
c::dim("not measured (no tests were executed)")
));
out.push_str(&format!(
" {} {}\n\n",
c::label("TDG Score:"),
c::dim("not measured")
));
out
}
fn save_refactored_code(output_path: &Path, code: &str) -> Result<()> {
std::fs::write(output_path, code)?;
println!(
"{}",
c::pass(&format!(
"Refactored code written to: {}",
c::path(&output_path.display().to_string())
))
);
Ok(())
}
fn display_rollback_info(result: &QddResult) {
if !result.rollback_plan.checkpoints.is_empty() {
println!(
" {} {} rollback checkpoints available",
c::label("Rollback:"),
c::number(&format!("{}", result.rollback_plan.checkpoints.len()))
);
}
}
async fn handle_qdd_validate(
path: PathBuf,
profile: QddQualityProfile,
format: QddOutputFormat,
output: Option<PathBuf>,
strict: bool,
) -> Result<()> {
crate::cli::ensure_analysis_path_exists(&path)?;
let quality_profile = match profile {
QddQualityProfile::Extreme => QualityProfile::extreme(),
QddQualityProfile::Standard => QualityProfile::standard(),
QddQualityProfile::Relaxed => QualityProfile::relaxed(),
};
let is_json = matches!(format, QddOutputFormat::Json);
if !is_json {
print_validation_header(&path, profile, &quality_profile);
}
let outcome = run_validation_checks(&path, &quality_profile).await;
let validation_passed = outcome.passed();
match format {
QddOutputFormat::Summary => {
println!("\n{}", c::subheader("Validation Summary:"));
println!("{}", render_status(&outcome));
for (name, check) in &outcome.checks {
println!(" {} {}", c::label(&format!("{name}:")), check.describe());
}
}
QddOutputFormat::Detailed => {
println!("\n{}", c::subheader("Detailed Validation Results:"));
for (name, check) in &outcome.checks {
println!("{}", check.render(name));
}
println!("{}", render_status(&outcome));
}
QddOutputFormat::Json => {
let json_result = build_validation_json(&outcome, profile, &path);
println!("{}", serde_json::to_string_pretty(&json_result)?);
}
QddOutputFormat::Markdown => {
println!("# QDD Validation Report");
println!();
println!("**Status:** {}", markdown_status(&outcome));
println!("**Profile:** {profile:?}");
println!("**Path:** {}", path.display());
println!(
"**Date:** {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
);
println!();
for (name, check) in &outcome.checks {
println!("- **{name}:** {}", check.describe());
}
}
}
if let Some(output_path) = output {
let report =
serde_json::to_string_pretty(&build_validation_json(&outcome, profile, &path))?;
std::fs::write(&output_path, report)
.with_context(|| format!("Failed to write report: {}", output_path.display()))?;
let message = c::pass(&format!(
"Validation report written to: {}",
c::path(&output_path.display().to_string())
));
if is_json {
eprintln!("{message}");
} else {
println!("\n{message}");
}
}
if strict && !validation_passed {
return Err(anyhow::anyhow!(
"Quality validation did not pass (strict mode): {}",
outcome.strict_reason()
));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CheckOutcome {
Passed(String),
Failed(String),
NotMeasured(String),
}
impl CheckOutcome {
fn describe(&self) -> String {
match self {
Self::Passed(detail) | Self::Failed(detail) | Self::NotMeasured(detail) => {
detail.clone()
}
}
}
fn verdict(&self) -> &'static str {
match self {
Self::Passed(_) => "passed",
Self::Failed(_) => "failed",
Self::NotMeasured(_) => "not measured",
}
}
fn render(&self, name: &str) -> String {
let line = format!(
"{name}: {} ({})",
self.verdict().to_uppercase(),
self.describe()
);
match self {
Self::Passed(_) => c::pass(&line),
Self::Failed(_) => c::fail(&line),
Self::NotMeasured(_) => c::warn(&line),
}
}
}
struct ValidationOutcome {
checks: Vec<(&'static str, CheckOutcome)>,
}
impl ValidationOutcome {
fn status(&self) -> &'static str {
if self.has(|c| matches!(c, CheckOutcome::Failed(_))) {
"failed"
} else if self.has(|c| matches!(c, CheckOutcome::NotMeasured(_))) {
"incomplete"
} else {
"passed"
}
}
fn has(&self, pred: impl Fn(&CheckOutcome) -> bool) -> bool {
self.checks.iter().any(|(_, c)| pred(c))
}
fn passed(&self) -> bool {
self.status() == "passed"
}
fn strict_reason(&self) -> String {
self.checks
.iter()
.filter(|(_, c)| !matches!(c, CheckOutcome::Passed(_)))
.map(|(name, c)| format!("{name} {}: {}", c.verdict(), c.describe()))
.collect::<Vec<_>>()
.join("; ")
}
fn violations(&self) -> Vec<serde_json::Value> {
self.checks
.iter()
.filter(|(_, c)| matches!(c, CheckOutcome::Failed(_)))
.map(|(name, c)| serde_json::json!({ "check": name, "detail": c.describe() }))
.collect()
}
fn unmeasured(&self) -> Vec<serde_json::Value> {
self.checks
.iter()
.filter(|(_, c)| matches!(c, CheckOutcome::NotMeasured(_)))
.map(|(name, c)| serde_json::json!({ "check": name, "reason": c.describe() }))
.collect()
}
}
fn render_status(outcome: &ValidationOutcome) -> String {
match outcome.status() {
"passed" => c::pass("PASSED"),
"failed" => c::fail("FAILED"),
other => c::warn(&other.to_uppercase()),
}
}
fn markdown_status(outcome: &ValidationOutcome) -> &'static str {
match outcome.status() {
"passed" => "✅ PASSED",
"failed" => "❌ FAILED",
_ => "⚠️ INCOMPLETE (some thresholds were not measured)",
}
}
async fn run_validation_checks(path: &Path, profile: &QualityProfile) -> ValidationOutcome {
let thresholds = &profile.thresholds;
ValidationOutcome {
checks: vec![
("complexity", check_complexity(path, thresholds.max_complexity).await),
("technical debt", check_satd(path, thresholds.zero_satd).await),
(
"coverage",
CheckOutcome::NotMeasured(format!(
"min {}% required; coverage needs an instrumented test run (cargo llvm-cov), which this command does not perform",
thresholds.min_coverage
)),
),
(
"tdg",
CheckOutcome::NotMeasured(format!(
"max {} allowed; run `pmat tdg` — this command does not compute TDG",
thresholds.max_tdg
)),
),
],
}
}
fn collect_analysable_files(path: &Path) -> Vec<PathBuf> {
use crate::cli::language_analyzer::Language;
let candidates = if path.is_file() {
vec![path.to_path_buf()]
} else {
crate::services::file_discovery::ProjectFileDiscovery::new(path.to_path_buf())
.discover_files()
.unwrap_or_default()
};
candidates
.into_iter()
.filter(|p| {
!matches!(
Language::from_path(p),
Language::Unknown | Language::Markdown | Language::Yaml
)
})
.collect()
}
async fn check_complexity(path: &Path, max_complexity: u32) -> CheckOutcome {
let files = collect_analysable_files(path);
let mut worst: Option<(String, u16)> = None;
let mut analyzed = 0usize;
for file in &files {
let Ok(metrics) =
crate::services::complexity::analyze_file_complexity_uncached(file, None).await
else {
continue;
};
analyzed += 1;
for func in &metrics.functions {
if worst
.as_ref()
.is_none_or(|(_, c)| func.metrics.cyclomatic > *c)
{
worst = Some((
format!("{}::{}", metrics.path, func.name),
func.metrics.cyclomatic,
));
}
}
}
match worst {
None => CheckOutcome::NotMeasured(format!(
"no functions were read under {} ({analyzed} file(s) analysed)",
path.display()
)),
Some((name, cyclomatic)) if u32::from(cyclomatic) > max_complexity => CheckOutcome::Failed(
format!("{name} has cyclomatic complexity {cyclomatic}, over the limit of {max_complexity} ({analyzed} file(s) analysed)"),
),
Some((name, cyclomatic)) => CheckOutcome::Passed(format!(
"worst function {name} at cyclomatic {cyclomatic}, within {max_complexity} ({analyzed} file(s) analysed)"
)),
}
}
async fn check_satd(path: &Path, zero_satd: bool) -> CheckOutcome {
use crate::services::satd_detector::SATDDetector;
if !zero_satd {
return CheckOutcome::Passed("this profile does not require zero SATD".to_string());
}
let detector = SATDDetector::new();
let debts = if path.is_file() {
match std::fs::read_to_string(path) {
Ok(content) => detector.extract_from_content(&content, path).ok(),
Err(_) => None,
}
} else {
detector.analyze_directory(path).await.ok()
};
match debts {
None => CheckOutcome::NotMeasured(format!("could not scan {} for SATD", path.display())),
Some(debts) if debts.is_empty() => {
CheckOutcome::Passed("no self-admitted technical debt found".to_string())
}
Some(debts) => {
let first = debts
.first()
.map(|d| format!("{}:{}", d.file.display(), d.line))
.unwrap_or_default();
CheckOutcome::Failed(format!(
"{} self-admitted debt marker(s), first at {first}",
debts.len()
))
}
}
}
fn print_validation_header(
path: &Path,
profile: QddQualityProfile,
quality_profile: &QualityProfile,
) {
println!("{}", c::header("QDD Quality Validation"));
println!(
" {} {}",
c::label("Path:"),
c::path(&path.display().to_string())
);
println!("{}", c::pass(&format!("Quality Profile: {profile:?}")));
println!("{}", c::subheader("Thresholds:"));
println!(
" {} {}",
c::label("Max Complexity:"),
c::number(&format!("{}", quality_profile.thresholds.max_complexity))
);
println!(
" {} {}",
c::label("Min Coverage:"),
c::pct(quality_profile.thresholds.min_coverage as f64, 80.0, 60.0)
);
println!(
" {} {}",
c::label("Max TDG:"),
c::number(&format!("{}", quality_profile.thresholds.max_tdg))
);
println!(
" {} {}",
c::label("Zero SATD:"),
c::number(&format!("{}", quality_profile.thresholds.zero_satd))
);
}
fn build_validation_json(
outcome: &ValidationOutcome,
profile: QddQualityProfile,
path: &Path,
) -> serde_json::Value {
serde_json::json!({
"status": outcome.status(),
"profile": format!("{profile:?}").to_lowercase(),
"path": path.display().to_string(),
"checks": outcome.checks.iter().map(|(name, check)| serde_json::json!({
"check": name,
"result": check.verdict(),
"detail": check.describe(),
})).collect::<Vec<_>>(),
"violations": outcome.violations(),
"not_measured": outcome.unmeasured(),
"validation_time": chrono::Utc::now().to_rfc3339()
})
}
include!("qdd_handlers_tests.rs");
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod qdd_output_tests {
use super::*;
use crate::qdd::{QualityMetrics, QualityScore, RollbackPlan};
fn sample_result() -> QddResult {
QddResult {
code:
"pub fn add_two(a: i32, b: i32) -> i32 {\n todo!(\"Implementation needed\")\n}\n"
.to_string(),
tests: "#[cfg(test)]\nmod tests {\n use super::*;\n}\n".to_string(),
documentation:
"# add_two\n\nadd two numbers\n\n## Returns\n\n```rust\nlet x = 1;\n```\n"
.to_string(),
quality_score: QualityScore {
overall: 98.0,
complexity: 1,
coverage: 100.0,
tdg: 1,
},
metrics: QualityMetrics::default(),
rollback_plan: RollbackPlan {
original: String::new(),
checkpoints: vec![],
},
}
}
fn plain(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\u{1b}' {
for c in chars.by_ref() {
if c == 'm' {
break;
}
}
} else {
out.push(ch);
}
}
out
}
#[test]
fn refactor_results_do_not_print_a_coverage_measurement() {
let mut result = sample_result();
result.quality_score.coverage = 80.0;
result.quality_score.tdg = 0;
let text = plain(&format_refactor_results(
Path::new("src/lib.rs"),
None,
QddQualityProfile::Standard,
&result,
));
assert!(
text.contains("Coverage: not measured"),
"coverage must be reported as not measured, got:\n{text}"
);
assert!(
text.contains("TDG Score: not measured"),
"TDG must be reported as not measured, got:\n{text}"
);
assert!(
!text.contains("80.0%"),
"the coverage guess must not be printed as a percentage:\n{text}"
);
assert!(
text.contains("Complexity (estimated):"),
"the complexity estimate must say it is an estimate:\n{text}"
);
}
#[test]
fn test_documentation_is_not_appended_to_the_rust_file() {
let tmp = tempfile::TempDir::new().unwrap();
let rs_path = tmp.path().join("add.rs");
output_generated_code(Some(rs_path.clone()), &sample_result()).unwrap();
let source = std::fs::read_to_string(&rs_path).unwrap();
assert!(source.contains("pub fn add_two"), "code missing: {source}");
assert!(source.contains("mod tests"), "tests missing: {source}");
assert!(
!source.contains("# add_two"),
"markdown heading leaked into the .rs: {source}"
);
assert!(
!source.contains("## Returns"),
"markdown heading leaked into the .rs: {source}"
);
assert!(
!source.contains("```"),
"markdown fence leaked into the .rs: {source}"
);
syn::parse_file(&source).expect("generated .rs must parse as Rust");
let doc = std::fs::read_to_string(tmp.path().join("add.md")).unwrap();
assert!(doc.contains("## Returns"), "docs missing: {doc}");
}
}