ecformat 0.2.0

command line tool to keep files correct in respect of your EditorConfig
Documentation
// SPDX-FileCopyrightText: Contributors to ecformat project <https://codeberg.org/BaumiCoder/ecformat>
//
// SPDX-License-Identifier: BlueOak-1.0.0

//! Error types related to EditorConfig.

use std::{collections::HashMap, fmt, path::PathBuf};

use ec4rs::{language_tags, property};
use itertools::Itertools;
use snafu::Snafu;

/// Type of check error (i.e., which EditorConfig property is violated).
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum CheckErrorType {
    /// The charset in a file does not respect the EditorConfig property `charset`.
    #[snafu(display("charset {actual_charset} instead of {expected_charset}"))]
    CharsetError {
        /// The name of the charset which was determined by inspecting the files content.
        actual_charset: String,
        /// The charset specified for the file with EditorConfig.
        expected_charset: property::Charset,
    },
    /// The end of line characters does not respect the EditorConfig property `end_of_line`.
    #[snafu(display(
        "{} instead of only using end of line '{expected_end_of_line}'",
        wrong_end_of_lines
            .iter()
            .map(|(end_of_line, count)| format!("end of line '{end_of_line}' occurres {count} time(s)"))
            .join(" and ")
    ))]
    EndOfLineError {
        /// The end of line character(s) specified for the file with EditorConfig.
        expected_end_of_line: property::EndOfLine,
        /// The wrong end of line characters and how often they occurred in the file.
        wrong_end_of_lines: HashMap<property::EndOfLine, usize>,
    },
    #[snafu(display("{lines_with_trailing_whitespace} lines with trailing whitespace"))]
    TrimTrailingWhitespaceError {
        /// Number of lines with trailing whitespace
        lines_with_trailing_whitespace: usize,
    },
    #[snafu(display(
        "Wrong indentation in the following lines: {}",
        lines_with_wrong_indentation
            .iter()
            .map(|line| format!("{}", line + 1))
            .join(", ")
    ))]
    IndentationError {
        /// Indices (zero based) of the lines with wrong indentation
        lines_with_wrong_indentation: Vec<usize>,
    },
    #[snafu(display("file has no final newline"))]
    InsertFinalNewlineError {},
}

/// A check on a specific file found a violation of a EditorConfig property.
#[derive(Debug, Snafu)]
#[snafu(display("{}: {error_type}", path.display()), visibility(pub))]
pub struct CheckError {
    path: PathBuf,
    error_type: CheckErrorType,
}

impl CheckError {
    /// Path to the file with the violation of a EditorConfig property.
    pub fn path(&self) -> &PathBuf {
        &self.path
    }
    pub fn error_type(&self) -> &CheckErrorType {
        &self.error_type
    }
}

/// Type of check config error
/// (i.e., which EditorConfig property has an error in a `.editorconfig` file).
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum CheckConfigErrorType {
    #[snafu(display("No valid language tag for the spelling_language: {validation_error}"))]
    SpellingLanguageError {
        validation_error: language_tags::ValidationError,
    },
    #[snafu(display("'{value}' cannot be parsed as a {property_name}"))]
    ParsingError {
        value: String,
        property_name: &'static str,
    },
}

/// A check on a specific `.editorconfig` file found an error in this config file.
#[derive(Debug, Snafu)]
#[snafu(display("{} section #{}: {error_type}", path.display(), section + 1), visibility(pub))]
pub struct CheckConfigError {
    path: PathBuf,
    section: usize,
    error_type: CheckConfigErrorType,
}

impl CheckConfigError {
    /// Path to the `.editorconfig` file with an error in it.
    pub fn path(&self) -> &PathBuf {
        &self.path
    }
    /// The zero-based index of the section with the error.
    pub fn section(&self) -> usize {
        self.section
    }
    pub fn error_type(&self) -> &CheckConfigErrorType {
        &self.error_type
    }
}

/// Enum for the types of EditorConfig errors that a check can found.
#[derive(Debug)]
pub enum EditorConfigError {
    /// See [`CheckError`] for details
    FileError(CheckError),
    /// See [`CheckConfigError`] for details
    ConfigError(CheckConfigError),
}

impl EditorConfigError {
    /// Path of the contained [`CheckError`](CheckError::path)
    /// or [`CheckConfigError`](CheckConfigError::path), respectively.
    pub fn path(&self) -> &PathBuf {
        match self {
            Self::FileError(ce) => &ce.path,
            Self::ConfigError(cce) => &cce.path,
        }
    }
}

impl fmt::Display for EditorConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            EditorConfigError::FileError(ce) => write!(f, "{ce}"),
            EditorConfigError::ConfigError(cce) => write!(f, "{cce}"),
        }
    }
}

/// List of [`CheckError`] instances which occurred in a [`crate::check`] call.
/// Itself is display as a message about the number of check errors,
/// which was logged as errors when they occurred.
///
/// You can consider the single errors via [`CheckErrorList::errors`]
#[derive(Debug, Snafu)]
#[snafu(display("Check failed with {} errors", errors.len()), visibility(pub))]
pub struct CheckErrorList {
    errors: Vec<EditorConfigError>,
}

impl CheckErrorList {
    /// Returns the list of the errors occurred during the check.
    pub fn errors(&self) -> &Vec<EditorConfigError> {
        &self.errors
    }
}

impl<'a> IntoIterator for &'a CheckErrorList {
    type Item = &'a EditorConfigError;

    type IntoIter = std::slice::Iter<'a, EditorConfigError>;

    fn into_iter(self) -> Self::IntoIter {
        self.errors.iter()
    }
}

/// Error in [`crate::status`] if no EditorConfig could be found.
#[derive(Debug, Snafu)]
#[snafu(display("No file named .editorconfig found"), visibility(pub))]
pub struct NoEditorConfigFound {}