typos-git-commit 0.8.2

This program analyzes a json file produced with `typos` and makes commits for each correction.
Documentation
use crate::cli::{Cli, TypoType};
use crate::keyvalue::{Key, Value};
use crate::typosjsonline::TyposJsonLine;
use fluent_i18n::t;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
use std::process::exit;

// We use these Key and Value structures in a HashMap that
// can handle multiples Value for a single Key (ie a typo
// may appear more than once in more than one file)
#[derive(Debug)]
pub struct THashMap {
    hashmap: HashMap<Key, Vec<Value>>,
}

impl Default for THashMap {
    fn default() -> Self {
        Self::new()
    }
}

impl THashMap {
    fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
    where
        P: AsRef<Path>,
    {
        let file = File::open(filename)?;
        Ok(io::BufReader::new(file).lines())
    }

    #[must_use]
    pub fn new() -> Self {
        let hashmap: HashMap<Key, Vec<Value>> = HashMap::new();
        THashMap {
            hashmap,
        }
    }

    /// Here we populate the `HashMap` with a line in `TyposJsonLine` format
    /// read from typo's output JSON file.
    pub fn insert(&mut self, typojson: TyposJsonLine, cli: &Cli) {
        if !typojson.is_excluded(cli) {
            let typo = typojson.typo;
            let corrections = typojson.corrections;
            let key = Key {
                typo,
                corrections,
            };

            if let Some(v) = self.hashmap.get_mut(&key) {
                // Key does already exists so we only need to
                // add a new value for it.
                let value = Value {
                    path: typojson.path,
                    line_num: typojson.line_num,
                    byte_offset: typojson.byte_offset,
                };
                v.push(value);
            } else {
                // Key does not exist already -> create a new one
                // and add it to the HashMap
                let mut v: Vec<Value> = Vec::new();
                let value = Value {
                    path: typojson.path,
                    line_num: typojson.line_num,
                    byte_offset: typojson.byte_offset,
                };
                v.push(value);
                self.hashmap.insert(key, v);
            }
        } else if cli.debug {
            // The typo is excluded somehow and will not be corrected.
            // We print some information about it when debug mode is on.
            if typojson.is_file_excluded(cli) {
                eprintln!("{}", t!("thashmap-file-excluded", {"file" => typojson.path}));
            } else if typojson.is_typo_excluded(cli) {
                eprintln!("{}", t!("thashmap-typo-excluded", {"typo" => typojson.typo}));
            } else if typojson.is_correction_excluded(cli) {
                eprintln!(
                    "{}",
                    t!("thashmap-correction-excluded", { "correction" => format!("{:?}", typojson.corrections), "typo" => typojson.typo})
                );
            }
        }
    }

    /// Fills and returns a vector of typos that have `type_id` equal to "typo".
    ///
    /// # Errors
    ///
    /// Will return an error if the `serde_json` can not Deserialize a Json line
    pub fn read_typos_file(mut self, cli: &Cli) -> Result<Self, Box<dyn Error>> {
        match THashMap::read_lines(&cli.filename) {
            Ok(lines) => {
                // Consumes the iterator, returns an (Optional) String
                for line in lines.map_while(Result::ok) {
                    let typojson = serde_json::from_str::<TyposJsonLine>(&line)?;
                    if typojson.type_id == "typo" {
                        self.insert(typojson, cli);
                    }
                }
            }
            Err(e) => {
                eprintln!("{}", t!("thashmap-error-file", {"e" => e.to_string()}));
                exit(1);
            }
        }
        Ok(self)
    }

    // Only lists typos in a brief manner or more verbosely when details is true.
    pub fn list_typos(&self, cli: &Cli) {
        let Some(only_list_typos) = &cli.only_list_typos else {
            return;
        };

        for (key, values) in &self.hashmap {
            let files_string = t!("thashmap-file-count", {"count" => values.len()}).to_string();

            // Corrections will only occur if
            // * there is only one possible correction
            // * and the len of the typo is at least equal to the minimum specified
            let correctable = key.is_typo_correctable(cli);

            let should_print = matches!(
                (correctable, only_list_typos),
                (true, TypoType::All | TypoType::Corrected) | (false, TypoType::All | TypoType::NotCorrected)
            );

            if !should_print {
                continue;
            }

            if correctable {
                println!("'{}' -> {:?}) {}", key.typo, key.corrections, files_string);
            } else {
                println!(
                    "\t{}",
                    t!("thashmap-wont-correct", {"typo" => key.typo, "correction" => format!("{:?}",key.corrections), "files" => files_string})
                );
            }

            if cli.details {
                for v in values {
                    v.print_value_details();
                }
                println!();
            }
        }
    }

    // Corrects typos for real unless --noop has been invoked
    pub fn correct_typos(&self, cli: &Cli) {
        for (key, values) in &self.hashmap {
            // Corrections will only occur if
            // * there is only one possible correction
            // * and the len of the typo is at least equal to the minimum specified
            if key.is_typo_correctable(cli) {
                let files = values.iter().map(|v| v.path.clone()).collect();

                key.run_sed(&files, cli);
                key.run_git_commit(cli);
            } else if cli.details {
                println!();
                println!(
                    "{}\n{}",
                    t!("thashmap-typo-not-corrected", {"typo" => key.typo, "correction" => format!("{:?}",key.corrections)}),
                    t!("thashmap-typo-look-carefully")
                );
                for v in values {
                    v.print_value_details();
                }
                println!();
            }
        }
    }
}