rantlr-core 0.1.0

Rantlr core: .gr lexer, parser, linter, and in-process runtime
Documentation
use std::fmt;

use thiserror::Error;

use crate::diagnostic::{Diagnostic, DiagnosticKind};
use crate::fix_hint::FixHint;
use crate::span::Span;

/// User-facing error for lex / parse / lint failures.
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{message}")]
pub struct RantlrError {
    pub kind: ErrorKind,
    pub message: String,
    pub line: usize,
    pub column: usize,
    pub snippet: String,
    pub help: Option<String>,
    pub code: Option<String>,
    pub fix_hint: Option<FixHint>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
    Lex,
    Parse,
    Lint,
}

impl RantlrError {
    pub fn from_diagnostic(source: &str, diag: Diagnostic) -> Self {
        let kind = match diag.kind {
            DiagnosticKind::LexError => ErrorKind::Lex,
            DiagnosticKind::ParseError => ErrorKind::Parse,
            DiagnosticKind::Lint => ErrorKind::Lint,
        };
        let (line, column) = diag.span.line_col(source);
        let snippet = context_snippet(source, diag.span);
        Self {
            kind,
            message: diag.message,
            line,
            column,
            snippet,
            help: diag.help,
            code: diag.code,
            fix_hint: diag.fix,
        }
    }

    pub fn parse(source: &str, message: impl Into<String>, span: Span) -> Self {
        Self::from_diagnostic(
            source,
            Diagnostic::error(DiagnosticKind::ParseError, message, span),
        )
    }

    pub fn parse_with_help(
        source: &str,
        message: impl Into<String>,
        span: Span,
        help: impl Into<String>,
    ) -> Self {
        Self::from_diagnostic(
            source,
            Diagnostic::error(DiagnosticKind::ParseError, message, span).with_help(help),
        )
    }

    pub fn format(&self, file: &str) -> String {
        let mut out = format!(
            "{file}:{}:{}: {:?}: {}\n{}",
            self.line, self.column, self.kind, self.message, self.snippet
        );
        if let Some(help) = &self.help {
            out.push_str("\n  help: ");
            out.push_str(help);
        }
        out
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Lex => write!(f, "Lex"),
            Self::Parse => write!(f, "Parse"),
            Self::Lint => write!(f, "Lint"),
        }
    }
}

/// Build a small caret snippet around `span`.
pub fn context_snippet(source: &str, span: Span) -> String {
    let (line_no, col) = span.line_col(source);
    let line_text = source
        .lines()
        .nth(line_no.saturating_sub(1))
        .unwrap_or("");

    let gutter = format!("{line_no}");
    let pad = " ".repeat(gutter.len());
    let caret_col = col.saturating_sub(1);
    let caret = format!("{}{}", " ".repeat(caret_col), "^");

    format!("  {gutter} | {line_text}\n  {pad} | {caret}")
}