#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::analysis_utilities::DefectPredictionReport;
use anyhow::Result;
use std::fmt::Write;
pub trait DefectReportFormatter {
fn format(&self, report: &DefectPredictionReport, top_files: usize) -> Result<String>;
}
pub struct FullReportFormatter;
impl DefectReportFormatter for FullReportFormatter {
fn format(&self, report: &DefectPredictionReport, top_files: usize) -> Result<String> {
let mut output = String::new();
self.write_header(&mut output)?;
self.write_summary_statistics(&mut output, report)?;
self.write_detailed_predictions(&mut output, report, top_files)?;
Ok(output)
}
}
impl FullReportFormatter {
fn write_header(&self, output: &mut String) -> Result<()> {
writeln!(output, "# Defect Prediction Analysis - Full Report\n")?;
Ok(())
}
fn write_summary_statistics(
&self,
output: &mut String,
report: &DefectPredictionReport,
) -> Result<()> {
writeln!(output, "## Summary Statistics")?;
let total = report.total_files as f32;
writeln!(output, "- Total files analyzed: {}", report.total_files)?;
self.write_risk_category(output, "High", report.high_risk_files, total)?;
self.write_risk_category(output, "Medium", report.medium_risk_files, total)?;
self.write_risk_category(output, "Low", report.low_risk_files, total)?;
writeln!(output)?;
Ok(())
}
fn write_risk_category(
&self,
output: &mut String,
level: &str,
count: usize,
total: f32,
) -> Result<()> {
let percentage = (count as f32 / total) * 100.0;
writeln!(output, "- {level} risk files: {count} ({percentage:.1}%)")?;
Ok(())
}
fn write_detailed_predictions(
&self,
output: &mut String,
report: &DefectPredictionReport,
top_files: usize,
) -> Result<()> {
writeln!(output, "## Detailed File Predictions\n")?;
let files_to_show = if top_files == 0 {
report.file_predictions.len()
} else {
top_files.min(report.file_predictions.len())
};
for (i, prediction) in report
.file_predictions
.iter()
.take(files_to_show)
.enumerate()
{
self.write_file_prediction(output, i + 1, prediction)?;
}
Ok(())
}
fn write_file_prediction(
&self,
output: &mut String,
index: usize,
prediction: &crate::cli::analysis_utilities::FilePrediction,
) -> Result<()> {
writeln!(output, "### {}. {}", index, prediction.file_path)?;
writeln!(
output,
"- **Risk Score**: {:.1}%",
prediction.risk_score * 100.0
)?;
writeln!(output, "- **Risk Level**: {}", prediction.risk_level)?;
writeln!(output, "- **Risk Factors**:")?;
for factor in &prediction.factors {
writeln!(output, " - {factor}")?;
}
writeln!(output)?;
Ok(())
}
}
pub struct SarifFormatter;
impl DefectReportFormatter for SarifFormatter {
fn format(&self, report: &DefectPredictionReport, _top_files: usize) -> Result<String> {
let sarif = self.build_sarif_report(report);
Ok(serde_json::to_string_pretty(&sarif)?)
}
}
impl SarifFormatter {
fn build_sarif_report(&self, report: &DefectPredictionReport) -> serde_json::Value {
serde_json::json!({
"version": "2.1.0",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"runs": [{
"tool": self.build_tool_info(),
"results": self.build_results(report)
}]
})
}
fn build_tool_info(&self) -> serde_json::Value {
serde_json::json!({
"driver": {
"name": "pmat-defect-prediction",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit"
}
})
}
fn build_results(&self, report: &DefectPredictionReport) -> Vec<serde_json::Value> {
report
.file_predictions
.iter()
.filter(|p| p.risk_level == "high")
.map(|prediction| self.build_result(prediction))
.collect()
}
fn build_result(
&self,
prediction: &crate::cli::analysis_utilities::FilePrediction,
) -> serde_json::Value {
serde_json::json!({
"ruleId": "high-defect-risk",
"level": "warning",
"message": {
"text": format!(
"High defect risk ({:.1}%) - Factors: {}",
prediction.risk_score * 100.0,
prediction.factors.join(", ")
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": prediction.file_path
}
}
}]
})
}
}
pub struct CsvFormatter;
impl DefectReportFormatter for CsvFormatter {
fn format(&self, report: &DefectPredictionReport, _top_files: usize) -> Result<String> {
let mut output = String::new();
self.write_header(&mut output);
self.write_rows(&mut output, report);
Ok(output)
}
}
impl CsvFormatter {
fn write_header(&self, output: &mut String) {
output.push_str("file_path,risk_score,risk_level,factors\n");
}
fn write_rows(&self, output: &mut String, report: &DefectPredictionReport) {
for prediction in &report.file_predictions {
self.write_row(output, prediction);
}
}
fn write_row(
&self,
output: &mut String,
prediction: &crate::cli::analysis_utilities::FilePrediction,
) {
output.push_str(&format!(
"\"{}\",{:.4},{},\"{}\"\n",
prediction.file_path,
prediction.risk_score,
prediction.risk_level,
prediction.factors.join("; ")
));
}
}
pub struct DefectFormatterFactory;
impl DefectFormatterFactory {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
#[must_use]
pub fn create(format: &str) -> Box<dyn DefectReportFormatter> {
match format {
"sarif" => Box::new(SarifFormatter),
"csv" => Box::new(CsvFormatter),
_ => Box::new(FullReportFormatter),
}
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn format_defect_report(
report: &DefectPredictionReport,
format: &str,
top_files: usize,
) -> Result<String> {
let formatter = DefectFormatterFactory::create(format);
formatter.format(report, top_files)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
fn create_test_report() -> DefectPredictionReport {
DefectPredictionReport {
total_files: 100,
high_risk_files: 10,
medium_risk_files: 20,
low_risk_files: 70,
file_predictions: vec![crate::cli::analysis_utilities::FilePrediction {
file_path: "src/main.rs".to_string(),
risk_score: 0.85,
risk_level: "high".to_string(),
factors: vec!["High complexity".to_string(), "Recent changes".to_string()],
}],
}
}
#[test]
fn test_full_formatter() {
let formatter = FullReportFormatter;
let report = create_test_report();
let result = formatter.format(&report, 10).unwrap();
assert!(result.contains("# Defect Prediction Analysis"));
assert!(result.contains("Total files analyzed: 100"));
assert!(result.contains("High risk files: 10 (10.0%)"));
}
#[test]
fn test_csv_formatter() {
let formatter = CsvFormatter;
let report = create_test_report();
let result = formatter.format(&report, 10).unwrap();
assert!(result.contains("file_path,risk_score,risk_level,factors"));
assert!(result.contains("\"src/main.rs\",0.8500,high,"));
}
#[test]
fn test_sarif_formatter() {
let formatter = SarifFormatter;
let report = create_test_report();
let result = formatter.format(&report, 10).unwrap();
assert!(result.contains("\"version\": \"2.1.0\""));
assert!(result.contains("high-defect-risk"));
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}