ecformat 0.1.1

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

//! Module for the `end_of_line` property of EditorConfig.

use std::collections::HashMap;

use ec4rs::property::{self, EndOfLine};
use line_ending::LineEnding;
use snafu::ensure;

use crate::{errors, files};

use super::{PropertyHandler, charset};

/// Handles the `end_of_line` property for a single file.
pub struct EndOfLineHandler {
    charset: property::Charset,
    end_of_line: property::EndOfLine,
}

impl PropertyHandler for EndOfLineHandler {
    fn check(&self, file_path: &std::path::Path) -> anyhow::Result<()> {
        let content = files::read_file(file_path, &self.charset)?;
        let wrong_end_of_lines: HashMap<property::EndOfLine, usize> = self
            .get_wrong_line_endings(&content)
            .map(|(line_ending, count)| {
                (
                    // EditorConfig type for inside the errors passed to the caller of ecformat
                    Self::line_ending_as_end_of_line(&line_ending),
                    count,
                )
            })
            .collect();

        ensure!(
            wrong_end_of_lines.is_empty(),
            errors::EndOfLineSnafu {
                expected_end_of_line: self.end_of_line,
                wrong_end_of_lines,
            }
        );
        Ok(())
    }

    fn fix(&self, file_path: &std::path::Path) -> anyhow::Result<()> {
        let mut content = files::read_file(file_path, &self.charset)?;
        if self.fix_wrong_line_endings(&mut content) {
            files::overwrite_file(file_path, &self.charset, &content)?;
        }
        Ok(())
    }
}

impl EndOfLineHandler {
    /// Creates a [`EndOfLineHandler`] for the given properties,
    /// if a handler is necessary for these properties.
    pub fn build(properties: &ec4rs::Properties) -> Option<EndOfLineHandler> {
        match properties.get::<property::EndOfLine>() {
            Ok(end_of_line) => Some(EndOfLineHandler {
                charset: charset::get_charset(properties),
                end_of_line,
            }),
            Err(_) => None, // no end_of_line property set
        }
    }

    /// Returns an iterator over the wrong line endings in the given content
    /// incl. how often they occur
    fn get_wrong_line_endings(&self, content: &str) -> impl Iterator<Item = (LineEnding, usize)> {
        let expected_line_ending = Self::end_of_line_as_line_ending(&self.end_of_line);
        let scores = LineEnding::score_mixed_types(content);
        scores
            .into_iter()
            .filter(move |(line_ending, count)| *count != 0 && *line_ending != expected_line_ending)
    }

    /// Fixed wrong line endings, if necessary.
    /// Returns true if a fix was performed (i.e., content was changed).
    fn fix_wrong_line_endings(&self, content: &mut String) -> bool {
        let wrong_line_endings = self.get_wrong_line_endings(content).any(|_| true);
        if wrong_line_endings {
            *content = Self::end_of_line_as_line_ending(&self.end_of_line)
                .denormalize(&LineEnding::normalize(content));
        }
        wrong_line_endings
    }

    /// Returns the [`line_ending::LineEnding`] for the given
    /// EditorConfig property [`ec4rs::property::EndOfLine`].
    fn end_of_line_as_line_ending(end_of_line: &EndOfLine) -> LineEnding {
        match *end_of_line {
            EndOfLine::Lf => LineEnding::LF,
            EndOfLine::CrLf => LineEnding::CRLF,
            EndOfLine::Cr => LineEnding::CR,
        }
    }

    /// Returns the EditorConfig property [`ec4rs::property::EndOfLine`]
    /// for the given [`line_ending::LineEnding`].
    fn line_ending_as_end_of_line(line_ending: &LineEnding) -> EndOfLine {
        match *line_ending {
            LineEnding::LF => EndOfLine::Lf,
            LineEnding::CRLF => EndOfLine::CrLf,
            LineEnding::CR => EndOfLine::Cr,
        }
    }
}

#[cfg(test)]
mod tests;