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 with helper functions for test code.

use std::{
    fs,
    path::{self, PathBuf},
};

use ec4rs::PropertyKey;

/// Module with helper functions for the tests regarding the `charset` property of EditorConfig.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub(crate) mod charset {
    use std::path::Path;

    use ec4rs::property::Charset;

    /// Determine the charset used in a test file from its name.
    /// The file names (without extension) have to be exactly the name of the charset.
    pub fn get_charset_from_file_name(file_path: &Path) -> Result<Charset, String> {
        match file_path.file_stem().unwrap().to_str().unwrap() {
            "utf-8" => Ok(Charset::Utf8),
            "latin1" => Ok(Charset::Latin1),
            "utf-16le" => Ok(Charset::Utf16Le),
            "utf-16be" => Ok(Charset::Utf16Be),
            "utf-8-bom" => Ok(Charset::Utf8Bom),
            other => Err(String::from(other)),
        }
    }
}

/// Module with helper functions for the tests regarding the `end_of_line` property of EditorConfig.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub(crate) mod end_of_line {
    use std::path::Path;

    use ec4rs::property::EndOfLine;

    /// Determine the end of line characters used in a test file from its name.
    /// The file names (without extension) have to contain a "-" separated list
    /// of end of line shorthands.
    pub fn get_end_of_line_from_file_name(file_path: &Path) -> Vec<EndOfLine> {
        file_path
            .file_stem()
            .unwrap()
            .to_str()
            .unwrap()
            .split("-")
            .map(|name| match name {
                "LF" => EndOfLine::Lf,
                "CRLF" => EndOfLine::CrLf,
                "CR" => EndOfLine::Cr,
                other => panic!(
                    "Unknown end of line character short hand '{other}' found in file name of {}",
                    file_path.display()
                ),
            })
            .collect()
    }
}

/// Module with helper functions for the tests regarding
/// the indentation related property of EditorConfig,
/// i.e., `indent_style`, `indent_size` and `tab_width`
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub(crate) mod indentation {
    use std::path::Path;

    use ec4rs::{PropertyValue, property::IndentStyle, rawvalue::RawValue};

    /// Determine the indentation properties of a test file from its name.
    /// The file names (without extension) have to contain a "-" separated list.
    /// Its first entry is for a descriptive name.
    /// Next comes the `indent_style`, followed by `indent_size` and `tab_width` (both integers).
    /// Finally, there comes the ascending sorted list of line numbers with indentation errors.
    /// The function returns the parts of the "-" separated list in the same order as tuple.
    pub fn get_infos_from_file_name(file_path: &Path) -> (IndentStyle, usize, usize, Vec<usize>) {
        let mut name_parts = file_path
            .file_stem()
            .unwrap()
            .to_str()
            .unwrap()
            .split("-")
            .skip(1); // First part is the descriptive name
        let indent_style = IndentStyle::parse(&RawValue::from(
            name_parts
                .next()
                .expect("There have to be a second name part")
                .to_string(),
        ))
        .unwrap_or_else(|_| {
            panic!(
                "Second name part of the file name '{}' needs to be an indent_style",
                file_path.display()
            )
        });
        let indent_size = name_parts
            .next()
            .expect("There have to be a third name part")
            .parse()
            .unwrap_or_else(|_| {
                panic!(
                    "Third part of the file name '{}' is no valid indent_size",
                    file_path.display()
                )
            });
        let tab_width = name_parts
            .next()
            .expect("There have to be a fourth name part")
            .parse()
            .unwrap_or_else(|_| {
                panic!(
                    "Fourth part of the file name '{}' is no valid tab_width",
                    file_path.display()
                )
            });
        let lines = name_parts
            .map(|expected_line| {
                expected_line.parse().unwrap_or_else(|_| {
                    panic!(
                        "'{expected_line}' is no valid line number in file name of {}",
                        file_path.display()
                    )
                })
            })
            .collect();
        (indent_style, indent_size, tab_width, lines)
    }
}

/// Module with helper functions for the tests regarding
/// the `insert_final_newline` property of EditorConfig.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub(crate) mod insert_final_newline {
    use std::path::Path;

    use itertools::Itertools;

    /// Determine if there is a need for a final newline in test file from its name.
    /// The file names (without extension) have to contain an indicator after a "-"
    /// for whether they are with or without a final newline
    /// (i.e. needs_final_newline or needs_no_final_newline).
    /// Before this indicator the file names have to contain some descriptive name.
    pub fn get_needs_final_newline_from_file_name(file_path: &Path) -> bool {
        let file_name = file_path.file_stem().unwrap().to_str().unwrap();
        if let Some((_, indicator)) = file_name.split("-").collect_tuple() {
            match indicator {
                "needs_final_newline" => true,
                "needs_no_final_newline" => false,
                _ => panic!("'{indicator}' is no supported indicator"),
            }
        } else {
            panic!("File name '{file_name}' has no two parts separated by '-'")
        }
    }
}

/// Module with helper functions for the tests regarding
/// the `spelling_language` property of EditorConfig.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub(crate) mod spelling_language {
    use std::path::Path;

    use itertools::Itertools;

    /// Determine if there is a faulty `spelling_language`
    /// in the given test EditorConfig file from its name.
    /// The file names (without extension) have to contain an indicator after a "-"
    /// for whether they are `correct` or `faulty`.
    /// Before this indicator the file names have to contain some descriptive name.
    pub fn is_faulty(file_path: &Path) -> bool {
        let file_name = file_path.file_stem().unwrap().to_str().unwrap();
        if let Some((_, indicator)) = file_name.split("-").collect_tuple() {
            match indicator {
                "faulty" => true,
                "correct" => false,
                _ => panic!("'{indicator}' is no supported indicator"),
            }
        } else {
            panic!("File name '{file_name}' has no two parts separated by '-'")
        }
    }
}

/// Module with helper functions for the tests regarding
/// the `trim_trailing_whitespace` property of EditorConfig.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub(crate) mod trim_trailing_whitespace {
    use std::path::Path;

    /// Determine the lines with trailing whitespace in test file from its name.
    /// The file names (without extension) have to contain a "-" separated list
    /// of line numbers (sorted ascending), but the first entry is for a descriptive name.
    pub fn get_lines_with_trailing_whitespace_from_file_name(file_path: &Path) -> Vec<usize> {
        file_path
            .file_stem()
            .unwrap()
            .to_str()
            .unwrap()
            .split("-")
            .skip(1) // first part is descriptive name
            .map(|expected_line| {
                expected_line.parse().unwrap_or_else(|_| panic!(
                    "'{expected_line}' is no valid line number in file name of {}",
                    file_path.display()
                ))
            })
            .collect()
    }
}

/// Helper to access test files
pub(crate) struct TestFileHelper {
    dir_name: &'static str,
}

impl TestFileHelper {
    /// Creates a test file helper for the given EditorConfig property
    pub(crate) fn new<T: PropertyKey>() -> TestFileHelper {
        TestFileHelper { dir_name: T::key() }
    }

    /// Provides an iterator over all files of the test file helper
    /// without a `.editorconfig` or `.license` extension.
    #[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
    pub(crate) fn get_test_file_paths(&self) -> impl Iterator<Item = PathBuf> {
        self.get_test_dir_content()
            .filter(|path| {
                path.extension()
                    .is_none_or(|ext| ext != "editorconfig" && ext != "license")
            })
            .inspect(|path| println!("Test file {}", path.display()))
    }

    /// Provides an iterator over all files with the `.editorconfig` extension
    /// of the test file helper.
    #[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
    pub(crate) fn get_test_editorconfig_paths(&self) -> impl Iterator<Item = PathBuf> {
        self.get_test_dir_content()
            .filter(|path| path.extension().is_some_and(|ext| ext == "editorconfig"))
            .inspect(|path| println!("Test .editorconfig file {}", path.display()))
    }

    /// Returns the directory for the test files of the test file helper
    pub(crate) fn get_test_dir(&self) -> PathBuf {
        ["tests", "resources", self.dir_name].into_iter().collect()
    }

    /// Provides an iterator over all files in the test directory of the test file helper.
    fn get_test_dir_content(&self) -> impl Iterator<Item = PathBuf> {
        fs::read_dir(self.get_test_dir())
            .expect("tests resources have to exist")
            .map(|entry| {
                entry
                    .expect("content of test directory have to be accessible")
                    .path()
            })
    }
}

/// Provides the path to the ecformat repository itself,
/// as an absolute path, which is the most suitable form.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub fn get_repo() -> PathBuf {
    path::absolute(get_repo_relative()).expect("Absolute path to repository have to be available")
}

/// Provides the path to the ecformat repository itself, as a relative path.
/// Be aware that the output paths of ecformat are absolute.
/// Therefore, using [`get_repo`] is more suitable in most cases.
#[allow(dead_code)] // this specific code is not used by all Integration Tests (= crates)
pub fn get_repo_relative() -> PathBuf {
    PathBuf::from("./")
}