pine-diagnostics 0.2.2

Diagnostics for the Pine Script toolchain.
Documentation
//! A single diagnostic type shared across the Pine toolchain.
//!
//! Both `pine-sema` (which reports errors) and `pine-lint` (which reports
//! warnings) emit this same [`Diagnostic`], so a consumer can collect, sort,
//! and render findings from every phase through one code path. `Severity`
//! — not the crate that produced it — decides how a finding is treated.

use std::fmt;

/// How serious a finding is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
    /// The program is invalid; it should not be executed.
    Error,
    /// Legal but suspect or non-idiomatic; does not block execution.
    Warning,
}

impl fmt::Display for Severity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Severity::Error => f.write_str("error"),
            Severity::Warning => f.write_str("warning"),
        }
    }
}

/// A single finding from any analysis phase.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
    pub rule: &'static str,
    pub severity: Severity,
    pub message: String,
    pub pos: Option<(u32, u32)>,
    pub file: Option<String>,
}

impl Diagnostic {
    pub fn new(
        rule: &'static str,
        severity: Severity,
        pos: Option<(u32, u32)>,
        message: impl Into<String>,
    ) -> Self {
        Self {
            rule,
            severity,
            message: message.into(),
            pos,
            file: None,
        }
    }

    /// Attribute this finding to `file` (an imported library). `None` leaves it
    /// on the main script.
    pub fn in_file(mut self, file: Option<String>) -> Self {
        self.file = file;
        self
    }

    pub fn error(rule: &'static str, pos: Option<(u32, u32)>, message: impl Into<String>) -> Self {
        Self::new(rule, Severity::Error, pos, message)
    }

    pub fn warning(
        rule: &'static str,
        pos: Option<(u32, u32)>,
        message: impl Into<String>,
    ) -> Self {
        Self::new(rule, Severity::Warning, pos, message)
    }

    /// 1-based line, if located.
    pub fn line(&self) -> Option<u32> {
        self.pos.map(|(line, _)| line)
    }

    /// 1-based column, if located.
    pub fn column(&self) -> Option<u32> {
        self.pos.map(|(_, col)| col)
    }
}

impl fmt::Display for Diagnostic {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let sev = self.severity;
        let rule = self.rule;
        let msg = &self.message;
        match (&self.file, self.pos) {
            (None, Some((line, col))) => write!(f, "{sev} [{rule}] {line}:{col}: {msg}"),
            (None, None) => write!(f, "{sev} [{rule}]: {msg}"),
            (Some(path), Some((line, col))) => {
                write!(f, "{sev} [{rule}] {path}:{line}:{col}: {msg}")
            }
            (Some(path), None) => write!(f, "{sev} [{rule}] {path}: {msg}"),
        }
    }
}