trixy 0.4.0

A rust crate used to generate multi-language apis for your application
Documentation
/*
* Copyright (C) 2023 - 2024:
* The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/

use core::fmt;

use thiserror::Error;

use crate::parser::{
    lexing::{error::SpannedLexingError, TokenSpan},
    parsing,
};

#[derive(Error, Debug)]
pub enum TrixyError {
    #[error(transparent)]
    Lexing(#[from] SpannedLexingError),

    #[error(transparent)]
    Parsing(#[from] parsing::unchecked::error::SpannedParsingError),

    #[error(transparent)]
    Processing(#[from] parsing::checked::error::SpannedParsingError),
}

/// The context of an Error.
#[derive(Debug, Clone)]
pub struct ErrorContext {
    /// The span of the error in the source file
    pub span: TokenSpan,
    /// The span of the error in the context line relative to the context line
    pub contexted_span: TokenSpan,
    /// The line above the error
    pub line_above: String,
    /// The line below the error
    pub line_below: String,
    /// The line in which the error occurred
    pub line: String,
    /// The line number of the main error line
    pub line_number: usize,
}

impl ErrorContext {
    pub fn from_span(span: TokenSpan, original_file: &str) -> Self {
        let line_number = original_file
            .chars()
            .take(span.start)
            .filter(|a| a == &'\n')
            .count()
            // This is here, as we are missing one newline with the method above
            + 1;

        let lines: Vec<_> = original_file.lines().collect();

        let line = (*lines
            .get(line_number - 1)
            .expect("This should work, as have *at least* one (index = 0) line"))
        .to_owned();

        // Make the span relative to the current line
        let contexted_span = {
            // `spanned_line` should be a subset of `line`
            let spanned_line = &original_file[span.start..span.end];

            let matched_line: Vec<_> = line.match_indices(&spanned_line).collect();
            assert!(!matched_line.is_empty());
            let (index, matched_line) = matched_line.first().expect("This first index should always match, as we took the line from the string in the first place");
            debug_assert_eq!(matched_line, &spanned_line);

            TokenSpan {
                start: *index,
                end: (span.end - span.start) + index,
            }
        };

        let line_above = if line_number == 1 {
            // We only have one line, so no line above
            "".to_owned()
        } else {
            (*lines
                .get((line_number - 1) - 1)
                .expect("We checked that this should work"))
            .to_owned()
        };

        let line_below = if lines.len() > line_number {
            // We have a line after the current line
            (*lines
                .get((line_number + 1) - 1)
                .expect("We checked that this should work"))
            .to_owned()
        } else {
            "".to_owned()
        };

        Self {
            span,
            contexted_span,
            line_above,
            line_below,
            line,
            line_number,
        }
    }

    pub fn from_index(start: usize, orginal_file: &str) -> Self {
        let span = TokenSpan {
            start,
            end: start + 1,
        };
        Self::from_span(span, orginal_file)
    }

    pub fn get_error_line(&self, source_error: &str) -> String {
        // deconstruct the structure
        let ErrorContext {
            contexted_span,
            line_number,
            ..
        } = self;

        let mut output = String::new();
        output.push_str("\x1b[92;1m");

        // pad to accommodate the line number printing.
        // 32 -> needs two spaces padding to print it
        line_number.to_string().chars().for_each(|_| {
            output.push(' ');
        });

        // pad to the beginning of the error
        for _ in 0..contexted_span.start {
            output.push(' ');
        }

        // push the error markers
        for _ in contexted_span.start..contexted_span.end {
            output.push('^');
        }

        // // pad until end of line
        // for _ in contexted_span.end..(line.len() - 1) {
        //     output.push('-');
        // }
        //
        // additional space to avoid having to end with a '-'
        output.push(' ');

        output.push_str("help: ");

        output.push_str(source_error);
        output.push_str("\x1b[0m");
        output
    }
}

pub trait AdditionalHelp {
    fn additional_help(&self) -> String;
}

pub trait ErrorContextDisplay: fmt::Display {
    type Error;

    fn error_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
    where
        <Self as ErrorContextDisplay>::Error: std::fmt::Display + AdditionalHelp,
    {
        let error_line = self
            .context()
            .get_error_line(&self.source().additional_help());

        writeln!(f, "\x1b[31;1merror: \x1b[37;1m{}\x1b[0m", self.source())?;

        if !self.line_above().is_empty() {
            writeln!(
                f,
                "\x1b[32;1m{} |\x1b[0m     {}",
                self.line_number() - 1,
                self.line_above()
            )?;
        }
        writeln!(
            f,
            "\x1b[36;1m{} |\x1b[0m     {}",
            self.line_number(),
            self.line()
        )?;
        writeln!(f, "       {}", error_line)?;
        if !self.line_below().is_empty() {
            writeln!(
                f,
                "\x1b[32;1m{} |\x1b[0m     {}",
                self.line_number() + 1,
                self.line_below()
            )
        } else {
            write!(f, "")
        }
    }

    // getters
    fn context(&self) -> &ErrorContext;
    fn source(&self) -> &Self::Error;
    fn line_number(&self) -> usize;
    fn line_above(&self) -> &str;
    fn line_below(&self) -> &str;
    fn line(&self) -> &str;
}