rscheck-cli 0.1.0-alpha.3

CLI frontend for the rscheck policy engine.
Documentation
use serde::{Deserialize, Serialize};

use crate::config::Level;
use crate::rules::{RuleBackend, RuleFamily};
use crate::span::Span;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    Info,
    Warn,
    Deny,
}

impl Severity {
    #[must_use]
    pub fn exit_code(self) -> i32 {
        match self {
            Self::Info => 0,
            Self::Warn => 1,
            Self::Deny => 2,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FixSafety {
    Safe,
    Unsafe,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FindingLabelKind {
    Primary,
    Secondary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FindingLabel {
    pub kind: FindingLabelKind,
    pub span: Span,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FindingNoteKind {
    Note,
    Help,
    Info,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FindingNote {
    pub kind: FindingNoteKind,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextEdit {
    pub file: String,
    pub byte_start: u32,
    pub byte_end: u32,
    pub replacement: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fix {
    pub id: String,
    pub safety: FixSafety,
    pub message: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub edits: Vec<TextEdit>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
    pub rule_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub family: Option<RuleFamily>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub engine: Option<RuleBackend>,
    pub severity: Severity,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub primary: Option<Span>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub secondary: Vec<Span>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub help: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub evidence: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub confidence: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub labels: Vec<FindingLabel>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub notes: Vec<FindingNote>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub fixes: Vec<Fix>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleCatalogEntry {
    pub id: String,
    pub family: RuleFamily,
    pub backend: RuleBackend,
    pub default_level: Level,
    pub summary: String,
    #[serde(default)]
    pub fixable: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdapterRun {
    pub name: String,
    pub enabled: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub toolchain: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolchainSummary {
    pub requested: String,
    pub resolved: String,
    pub semantic: String,
    #[serde(default)]
    pub nightly_available: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RunSummary {
    #[serde(default)]
    pub engine_used: Vec<RuleBackend>,
    #[serde(default)]
    pub semantic_backend_available: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_backend_reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub toolchain: Option<ToolchainSummary>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub skipped_rules: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub adapter_runs: Vec<AdapterRun>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Metrics {
    #[serde(default)]
    pub per_file: Vec<FileMetrics>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetrics {
    pub path: String,
    #[serde(default)]
    pub cyclomatic_sum: u32,
    #[serde(default)]
    pub cyclomatic_max_fn: u32,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Report {
    #[serde(default)]
    pub findings: Vec<Finding>,
    #[serde(default)]
    pub metrics: Metrics,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub rule_catalog: Vec<RuleCatalogEntry>,
    #[serde(default)]
    pub summary: RunSummary,
}

impl Report {
    pub fn worst_severity(&self) -> Severity {
        self.findings
            .iter()
            .map(|f| f.severity)
            .max_by_key(|s| s.exit_code())
            .unwrap_or(Severity::Info)
    }
}