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

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

use ec4rs::property::{self, TrimTrailingWs};
use itertools::Itertools;
use line_ending::LineEnding;
use snafu::ensure;

use crate::{errors, files};

use super::{PropertyHandler, charset, end_of_line};

/// Handles the `trim_trailing_whitespace` property for a single file.
pub struct TrimTrailingWhitespaceHandler {
    charset: property::Charset,
    line_ending: LineEnding,
}

impl PropertyHandler for TrimTrailingWhitespaceHandler {
    fn check(&self, file_path: &std::path::Path) -> anyhow::Result<()> {
        let content = files::read_file(file_path, &self.charset)?;
        let lines_with_trailing_whitespace =
            self.get_lines_with_trailing_whitespace(&content).count();

        ensure!(
            lines_with_trailing_whitespace == 0,
            errors::TrimTrailingWhitespaceSnafu {
                lines_with_trailing_whitespace,
            }
        );
        Ok(())
    }

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

impl TrimTrailingWhitespaceHandler {
    /// Creates a [`TrimTrailingWhitespaceHandler`] for the given properties,
    /// if a handler is necessary for these properties.
    pub fn build(properties: &ec4rs::Properties) -> Option<TrimTrailingWhitespaceHandler> {
        match properties.get::<property::TrimTrailingWs>() {
            Ok(TrimTrailingWs::Value(true)) => Some(TrimTrailingWhitespaceHandler {
                charset: charset::get_charset(properties),
                line_ending: end_of_line::get_line_ending(properties),
            }),
            // trim_trailing_whitespace is false or property is not set
            Ok(TrimTrailingWs::Value(false)) | Err(_) => None,
        }
    }

    /// Returns an iterator over the indices of lines with a trailing whitespace
    fn get_lines_with_trailing_whitespace(&self, content: &str) -> impl Iterator<Item = usize> {
        content
            .split(self.line_ending.as_str())
            .enumerate()
            .filter_map(|(index, line)| {
                line.chars().rev().nth(0).and_then(|last_char| {
                    if last_char.is_whitespace() {
                        Some(index)
                    } else {
                        None
                    }
                })
            })
    }

    /// Remove all trailing whitespace, if necessary.
    /// Returns true if trailing whitespace were removed (i.e., content was changed).
    fn remove_trailing_whitespace(&self, content: &mut String) -> bool {
        let mut lines_with_trailing_whitespace =
            self.get_lines_with_trailing_whitespace(content).peekable();
        let trailing_whitespace = lines_with_trailing_whitespace.peek().is_some();
        if trailing_whitespace {
            let mut lines = content.split(self.line_ending.as_str()).collect_vec();

            for index in lines_with_trailing_whitespace {
                lines[index] = lines[index].trim_end();
            }
            *content = lines.join(self.line_ending.as_str());
        }
        trailing_whitespace
    }
}

#[cfg(test)]
mod tests;