use super::converter::SarifConverter;
use crate::core::stats::AggregatedStats;
use crate::core::types::{CodeStats, FileStats};
use crate::utils::errors::Result;
use std::fs;
use std::path::Path;
pub struct SarifReporter {
converter: SarifConverter,
}
impl SarifReporter {
pub fn new() -> Self {
Self {
converter: SarifConverter::new(),
}
}
pub fn generate_report(
&self,
stats: &CodeStats,
individual_files: &[(String, FileStats)],
output_path: &Path,
) -> Result<()> {
let sarif_log = self
.converter
.convert_basic_analysis(stats, individual_files)?;
let sarif_content = serde_json::to_string_pretty(&sarif_log).map_err(|e| {
crate::utils::errors::HowManyError::display(format!(
"SARIF serialization failed: {}",
e
))
})?;
fs::write(output_path, sarif_content).map_err(|e| {
crate::utils::errors::HowManyError::file_processing(format!(
"Failed to write SARIF file: {}",
e
))
})?;
Ok(())
}
pub fn generate_comprehensive_report(
&self,
aggregated_stats: &AggregatedStats,
individual_files: &[(String, FileStats)],
output_path: &Path,
) -> Result<()> {
let sarif_log = self
.converter
.convert_comprehensive_analysis(aggregated_stats, individual_files)?;
let sarif_content = serde_json::to_string_pretty(&sarif_log).map_err(|e| {
crate::utils::errors::HowManyError::display(format!(
"SARIF serialization failed: {}",
e
))
})?;
fs::write(output_path, sarif_content).map_err(|e| {
crate::utils::errors::HowManyError::file_processing(format!(
"Failed to write SARIF file: {}",
e
))
})?;
Ok(())
}
pub fn generate_auto_report(
&self,
stats: Option<&CodeStats>,
aggregated_stats: Option<&AggregatedStats>,
individual_files: &[(String, FileStats)],
output_path: &Path,
) -> Result<()> {
match (stats, aggregated_stats) {
(_, Some(agg_stats)) => {
self.generate_comprehensive_report(agg_stats, individual_files, output_path)
}
(Some(basic_stats), None) => {
self.generate_report(basic_stats, individual_files, output_path)
}
(None, None) => Err(crate::utils::errors::HowManyError::invalid_config(
"No statistics provided for SARIF report generation".to_string(),
)),
}
}
pub fn generate_sarif_string(
&self,
stats: Option<&CodeStats>,
aggregated_stats: Option<&AggregatedStats>,
individual_files: &[(String, FileStats)],
) -> Result<String> {
let sarif_log = match (stats, aggregated_stats) {
(_, Some(agg_stats)) => self
.converter
.convert_comprehensive_analysis(agg_stats, individual_files)?,
(Some(basic_stats), None) => self
.converter
.convert_basic_analysis(basic_stats, individual_files)?,
(None, None) => {
return Err(crate::utils::errors::HowManyError::invalid_config(
"No statistics provided for SARIF generation".to_string(),
));
}
};
serde_json::to_string_pretty(&sarif_log).map_err(|e| {
crate::utils::errors::HowManyError::display(format!(
"SARIF serialization failed: {}",
e
))
})
}
pub fn validate_sarif_output(&self, sarif_content: &str) -> Result<()> {
let invalid = |detail: &str| {
crate::utils::errors::HowManyError::invalid_config(format!("Invalid SARIF: {detail}"))
};
let log: serde_json::Value = serde_json::from_str(sarif_content)
.map_err(|e| invalid(&format!("not valid JSON: {e}")))?;
let expected = super::SARIF_VERSION;
if log.get("version").and_then(|v| v.as_str()) != Some(expected) {
return Err(invalid(&format!("version must be {expected}")));
}
if !log
.get("$schema")
.and_then(|v| v.as_str())
.is_some_and(|s| s.contains("sarif"))
{
return Err(invalid("missing $schema"));
}
let runs = log
.get("runs")
.and_then(|v| v.as_array())
.ok_or_else(|| invalid("missing runs array"))?;
if runs.is_empty() {
return Err(invalid("a log must contain at least one run"));
}
for run in runs {
if run
.pointer("/tool/driver/name")
.and_then(|v| v.as_str())
.is_none_or(str::is_empty)
{
return Err(invalid("a run does not name the tool that produced it"));
}
}
Ok(())
}
pub fn get_file_extension() -> &'static str {
"sarif"
}
pub fn get_mime_type() -> &'static str {
"application/sarif+json"
}
pub fn default_filename() -> String {
use chrono::Utc;
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
format!("howmany-report-{}.sarif", timestamp)
}
}
impl Default for SarifReporter {
fn default() -> Self {
Self::new()
}
}