main-spex 0.21.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::fmt;
use std::path::{Path, PathBuf};

use crate::rule::Severity;
use crate::support::fs::relative_to;

#[cfg(test)]
#[path = "finding_test.rs"]
mod finding_test;

pub(super) struct Finding {
    pub(super) severity: Severity,
    pub(super) source: Source,
    pub(super) location: Option<Location>,
    pub(super) message: String,
}

pub(super) enum Source {
    Rule { rule_id: String, check: String },
    Spec { spec_id: String, check: String },
    Internal { id: String, check: String },
    UnknownRule { rule_id: String },
    UnreachablePackage { name: String },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Location {
    pub(super) file: PathBuf,
    pub(super) line: Option<usize>,
}

impl Finding {
    pub(super) fn new(
        severity: Severity,
        source: Source,
        location: Option<Location>,
        message: String,
    ) -> Self {
        Self {
            severity,
            source,
            location,
            message,
        }
    }

    pub(super) fn error(source: Source, location: Option<Location>, message: String) -> Self {
        Self::new(Severity::Error, source, location, message)
    }

    pub(super) fn warn(source: Source, location: Option<Location>, message: String) -> Self {
        Self::new(Severity::Warn, source, location, message)
    }

    pub(super) fn relative_to(mut self, base: &Path) -> Self {
        self.location = self.location.map(|location| location.relative_to(base));
        self
    }
}

impl Source {
    pub(super) fn rule(rule_id: &str, check: &str) -> Self {
        Self::Rule {
            rule_id: rule_id.to_string(),
            check: check.to_string(),
        }
    }

    pub(super) fn spec(spec_id: &str, check: &str) -> Self {
        Self::Spec {
            spec_id: spec_id.to_string(),
            check: check.to_string(),
        }
    }

    pub(super) fn internal(id: &str, check: &str) -> Self {
        Self::Internal {
            id: id.to_string(),
            check: check.to_string(),
        }
    }

    pub(super) fn baseline_key(&self) -> Option<(&str, &str)> {
        match self {
            Self::Rule {
                rule_id: owner,
                check,
            }
            | Self::Spec {
                spec_id: owner,
                check,
            }
            | Self::Internal { id: owner, check } => Some((owner, check)),
            Self::UnknownRule { .. } | Self::UnreachablePackage { .. } => None,
        }
    }
}

impl fmt::Display for Source {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Rule {
                rule_id: owner,
                check,
            }
            | Self::Spec {
                spec_id: owner,
                check,
            }
            | Self::Internal { id: owner, check } => write!(formatter, "{owner}/{check}"),
            Self::UnknownRule { rule_id } => write!(formatter, "{rule_id}"),
            Self::UnreachablePackage { name } => write!(formatter, "{name}"),
        }
    }
}

impl Location {
    pub(super) fn in_file(file: impl Into<PathBuf>) -> Self {
        Self {
            file: file.into(),
            line: None,
        }
    }

    pub(super) fn at_line(file: impl Into<PathBuf>, line: usize) -> Self {
        Self {
            file: file.into(),
            line: Some(line),
        }
    }

    pub(super) fn parse(text: &str) -> Self {
        if let Some((file, line)) = text.rsplit_once(':')
            && line.bytes().all(|byte| byte.is_ascii_digit())
            && let Ok(line) = line.parse()
        {
            return Self::at_line(file, line);
        }
        Self::in_file(text)
    }

    pub(super) fn relative_to(self, base: &Path) -> Self {
        Self {
            file: relative_to(&self.file, base).to_path_buf(),
            line: self.line,
        }
    }
}

impl fmt::Display for Location {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "{}", self.file.display())?;
        match self.line {
            Some(line) => write!(formatter, ":{line}"),
            None => Ok(()),
        }
    }
}