xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
//! Core domain value objects.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Stable language identifier (not tied to any toolchain).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct LanguageId(pub String);

impl LanguageId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn rust() -> Self {
        Self("rust".into())
    }
}

impl std::fmt::Display for LanguageId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    Info,
    Low,
    Medium,
    High,
    Critical,
}

impl Severity {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Info => "info",
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::Critical => "critical",
        }
    }

    pub fn parse(s: &str) -> Option<Self> {
        match s.to_ascii_lowercase().as_str() {
            "info" => Some(Self::Info),
            "low" => Some(Self::Low),
            "medium" | "med" => Some(Self::Medium),
            "high" => Some(Self::High),
            "critical" | "crit" => Some(Self::Critical),
            _ => None,
        }
    }
}

impl std::fmt::Display for Severity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// How certain the analyzer is about the finding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
    Low,
    Medium,
    High,
    Definite,
}

impl Confidence {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::Definite => "definite",
        }
    }

    /// Definite bugs vs review-required practice.
    pub fn is_definite_bug(self) -> bool {
        matches!(self, Self::Definite)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLocation {
    pub file: PathBuf,
    pub start_line: u32,
    pub start_column: u32,
    pub end_line: u32,
    pub end_column: u32,
}

impl SourceLocation {
    pub fn single_line(file: impl Into<PathBuf>, line: u32, column: u32) -> Self {
        Self {
            file: file.into(),
            start_line: line,
            start_column: column,
            end_line: line,
            end_column: column.saturating_add(1),
        }
    }
}

/// Snippet / surrounding lines for a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CodeContext {
    pub before: Vec<String>,
    pub highlight: String,
    pub after: Vec<String>,
}

/// Structured diagnostic message attached to a finding.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
    pub message: String,
    pub location: SourceLocation,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub context: Option<CodeContext>,
}

/// Evidence that led to a finding (language-agnostic).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DetectionEvidence {
    pub kind: String,
    pub detail: String,
}

/// One analysis finding.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
    pub id: String,
    pub rule_id: String,
    pub severity: Severity,
    pub confidence: Confidence,
    pub language: LanguageId,
    pub location: SourceLocation,
    pub explanation: String,
    pub failure_scenario: String,
    pub remediation: String,
    pub evidence: Vec<DetectionEvidence>,
    /// True when the tool treats this as a definite bug; false = review-required practice.
    pub definite_bug: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub context: Option<CodeContext>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fix_hint: Option<String>,
}

impl Finding {
    pub fn builder(rule_id: impl Into<String>, language: LanguageId) -> FindingBuilder {
        FindingBuilder {
            rule_id: rule_id.into(),
            language,
            severity: Severity::Medium,
            confidence: Confidence::Medium,
            location: None,
            explanation: String::new(),
            failure_scenario: String::new(),
            remediation: String::new(),
            evidence: Vec::new(),
            context: None,
            fix_hint: None,
            id: None,
        }
    }
}

pub struct FindingBuilder {
    rule_id: String,
    language: LanguageId,
    severity: Severity,
    confidence: Confidence,
    location: Option<SourceLocation>,
    explanation: String,
    failure_scenario: String,
    remediation: String,
    evidence: Vec<DetectionEvidence>,
    context: Option<CodeContext>,
    fix_hint: Option<String>,
    id: Option<String>,
}

impl FindingBuilder {
    pub fn severity(mut self, s: Severity) -> Self {
        self.severity = s;
        self
    }
    pub fn confidence(mut self, c: Confidence) -> Self {
        self.confidence = c;
        self
    }
    pub fn location(mut self, loc: SourceLocation) -> Self {
        self.location = Some(loc);
        self
    }
    pub fn explanation(mut self, e: impl Into<String>) -> Self {
        self.explanation = e.into();
        self
    }
    pub fn failure_scenario(mut self, e: impl Into<String>) -> Self {
        self.failure_scenario = e.into();
        self
    }
    pub fn remediation(mut self, e: impl Into<String>) -> Self {
        self.remediation = e.into();
        self
    }
    pub fn evidence(mut self, kind: impl Into<String>, detail: impl Into<String>) -> Self {
        self.evidence.push(DetectionEvidence {
            kind: kind.into(),
            detail: detail.into(),
        });
        self
    }
    pub fn context(mut self, c: CodeContext) -> Self {
        self.context = Some(c);
        self
    }
    pub fn fix_hint(mut self, h: impl Into<String>) -> Self {
        self.fix_hint = Some(h.into());
        self
    }
    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }
    pub fn build(self) -> Finding {
        let location = self.location.expect("Finding requires location");
        let definite_bug = self.confidence.is_definite_bug();
        let id = self.id.unwrap_or_else(|| {
            format!(
                "{}:{}:{}:{}",
                self.rule_id,
                location.file.display(),
                location.start_line,
                location.start_column
            )
        });
        Finding {
            id,
            rule_id: self.rule_id,
            severity: self.severity,
            confidence: self.confidence,
            language: self.language,
            location,
            explanation: self.explanation,
            failure_scenario: self.failure_scenario,
            remediation: self.remediation,
            evidence: self.evidence,
            definite_bug,
            context: self.context,
            fix_hint: self.fix_hint,
        }
    }
}

/// Suppression entry (inline comment or config).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Suppression {
    pub rule_id: Option<String>,
    pub file: Option<PathBuf>,
    pub line: Option<u32>,
    pub reason: Option<String>,
    pub pattern: Option<String>,
}

/// Approved exceptional pattern (config allowlist).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovedPattern {
    pub rule_id: Option<String>,
    pub path_glob: Option<String>,
    pub code_contains: Option<String>,
    pub reason: String,
}

/// Source file payload (path + UTF-8 text). Domain-level; loading is an outbound port.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceFile {
    pub path: PathBuf,
    pub content: String,
    pub language: LanguageId,
}

/// Control-flow sketch for error paths (language-agnostic).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ControlFlow {
    pub nodes: Vec<String>,
    pub edges: Vec<(usize, usize)>,
}

/// Ordered path from an error signal to a sink (ignore, log, terminate, recover).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ErrorPath {
    pub steps: Vec<String>,
    pub terminates: bool,
    pub recovers: bool,
    pub context_lost: bool,
}