xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
//! GitHub Actions workflow command annotations.

use crate::domain::report::AnalysisReport;
use crate::domain::types::Severity;
use crate::ports::outbound::DiagnosticRenderer;

pub struct GithubActionsRenderer;

impl DiagnosticRenderer for GithubActionsRenderer {
    fn render(&self, report: &AnalysisReport) -> Result<String, String> {
        let mut out = String::new();
        for f in &report.findings {
            let level = match f.severity {
                Severity::Info | Severity::Low => "notice",
                Severity::Medium => "warning",
                Severity::High | Severity::Critical => "error",
            };
            // Escape for workflow commands
            let msg = f
                .explanation
                .replace('%', "%25")
                .replace('\r', "%0D")
                .replace('\n', "%0A");
            out.push_str(&format!(
                "::{level} file={},line={},col={},title={}::{} [{}]\n",
                f.location.file.display(),
                f.location.start_line,
                f.location.start_column,
                f.rule_id,
                msg,
                f.remediation.replace('\n', " "),
            ));
        }
        if report.findings.is_empty() {
            out.push_str("::notice::xbp audit errors found no issues\n");
        }
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::types::{
        Confidence, Finding, LanguageId, SourceLocation,
    };

    #[test]
    fn annotation_format() {
        let mut r = AnalysisReport::default();
        r.findings.push(
            Finding::builder("ignored-failure", LanguageId::rust())
                .severity(Severity::High)
                .confidence(Confidence::High)
                .location(SourceLocation::single_line("src/main.rs", 12, 4))
                .explanation("let _ = fallible()")
                .failure_scenario("x")
                .remediation("handle it")
                .build(),
        );
        let s = GithubActionsRenderer.render(&r).unwrap();
        assert!(s.starts_with("::error file="));
        assert!(s.contains("line=12"));
        assert!(s.contains("ignored-failure"));
    }
}