howmany 3.0.0

A blazingly fast, intelligent code analysis tool with parallel processing, caching, and beautiful visualizations
Documentation
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(),
        }
    }

    /// Generate SARIF report from basic CodeStats
    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(())
    }

    /// Generate comprehensive SARIF report from AggregatedStats
    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(())
    }

    /// Auto-detect and generate the best possible SARIF report
    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(),
            )),
        }
    }

    /// Generate SARIF content as string without writing to file
    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
            ))
        })
    }

    /// Check that `sarif_content` is a SARIF log a consumer will accept.
    ///
    /// Valid JSON is not enough: a CI system reading the report looks for the
    /// version, the schema and at least one run with a named tool. Accepting
    /// `{}` here meant the check passed for output nothing could consume.
    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(())
    }

    /// Get the recommended file extension for SARIF files
    pub fn get_file_extension() -> &'static str {
        "sarif"
    }

    /// Get the MIME type for SARIF files
    pub fn get_mime_type() -> &'static str {
        "application/sarif+json"
    }

    /// Create a default output filename for SARIF reports
    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()
    }
}