typos_git_commit/
thashmap.rs

1use crate::cli::Cli;
2use crate::keyvalue::{Key, Value};
3use crate::typosjsonline::TyposJsonLine;
4use std::collections::HashMap;
5use std::error::Error;
6use std::fs::File;
7use std::io::{self, BufRead};
8use std::path::Path;
9use std::process::exit;
10
11// We use these Key and Value structures in a HashMap that
12// can handle multiples Value for a single Key (ie a typo
13// may appear more than once in more than one file)
14#[derive(Debug)]
15pub struct THashMap {
16    thm: HashMap<Key, Vec<Value>>,
17}
18
19impl THashMap {
20    fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
21    where
22        P: AsRef<Path>,
23    {
24        let file = File::open(filename)?;
25        Ok(io::BufReader::new(file).lines())
26    }
27
28    pub fn new() -> Self {
29        let thm: HashMap<Key, Vec<Value>> = HashMap::new();
30        THashMap {
31            thm,
32        }
33    }
34
35    // Here we populate the HashMap with a line in TyposJsonLine format
36    // read from typo's output JSON file.
37    pub fn insert(&mut self, typojson: TyposJsonLine, cli: &Cli) {
38        if !typojson.is_excluded(cli) {
39            let typo = typojson.typo;
40            let corrections = typojson.corrections;
41            let key = Key {
42                typo,
43                corrections,
44            };
45
46            match self.thm.get_mut(&key) {
47                Some(v) => {
48                    // Key does already exists so we only need to
49                    // add a new value for it.
50                    let value = Value {
51                        path: typojson.path,
52                        line_num: typojson.line_num,
53                        byte_offset: typojson.byte_offset,
54                    };
55                    v.push(value);
56                }
57                None => {
58                    // Key does not exist already -> create a new one
59                    // and add it to the HashMap
60                    let mut v: Vec<Value> = Vec::new();
61                    let value = Value {
62                        path: typojson.path,
63                        line_num: typojson.line_num,
64                        byte_offset: typojson.byte_offset,
65                    };
66                    v.push(value);
67                    self.thm.insert(key, v);
68                }
69            }
70        } else if cli.debug {
71            // The typo is excluded somehow and will not be corrected.
72            // We print some information about it when debug mode is on.
73            if typojson.is_file_excluded(cli) {
74                eprintln!("File {} excluded", typojson.path);
75            } else if typojson.is_typo_excluded(cli) {
76                eprintln!("Typo {} excluded.", typojson.typo);
77            } else if typojson.is_correction_excluded(cli) {
78                eprintln!("Correction {:?} for typo {} excluded.", typojson.corrections, typojson.typo);
79            }
80        }
81    }
82
83    // Fills and returns a vector of typos that have type_id equal to "typo".
84    pub fn read_typos_file(mut self, cli: &Cli) -> Result<Self, Box<dyn Error>> {
85        match THashMap::read_lines(&cli.filename) {
86            Ok(lines) => {
87                // Consumes the iterator, returns an (Optional) String
88                for line in lines.map_while(Result::ok) {
89                    let typojson = serde_json::from_str::<TyposJsonLine>(&line)?;
90                    if typojson.type_id == "typo" {
91                        self.insert(typojson, cli);
92                    }
93                }
94            }
95            Err(e) => {
96                eprintln!("Error reading file: {e}");
97                exit(1);
98            }
99        }
100        Ok(self)
101    }
102
103    // Only lists typos in a brief manner or more verbosely when details is true.
104    pub fn list_typos(&self, cli: &Cli) {
105        for (key, values) in &self.thm {
106            let values_len = values.len();
107            let files_string = match values_len {
108                1 => "(1 file)".to_string(),
109                _ => format!("({} files)", values_len),
110            };
111
112            // Corrections will only occur if
113            // * there is only one possible correction
114            // * and the len of the typo is at least equal to the minimum specified
115            if key.is_typo_correctable(cli) {
116                println!("'{}' -> {:?}) {}", key.typo, key.corrections, files_string);
117            } else {
118                println!("Won't correct '{}' -> {:?}) {}", key.typo, key.corrections, files_string);
119            }
120
121            if cli.details {
122                for v in values {
123                    println!("\t{} at line {} at offset {}", v.path, v.line_num, v.byte_offset);
124                }
125                println!();
126            }
127        }
128    }
129
130    // Corrects typos for real unless --noop has been invoked
131    pub fn correct_typos(&self, cli: &Cli) {
132        for (key, values) in &self.thm {
133            // Corrections will only occur if
134            // * there is only one possible correction
135            // * and the len of the typo is at least equal to the minimum specified
136            if key.is_typo_correctable(cli) {
137                let mut files: Vec<String> = Vec::new();
138                for v in values {
139                    files.push(v.path.clone());
140                }
141
142                key.run_sed(&files, cli);
143                key.run_git_commit(cli);
144            } else if cli.details {
145                println!();
146                println!("Typo '{}' will not be corrected into {:?}\nYou should look carefully at file(s):", key.typo, key.corrections);
147                for v in values {
148                    println!("\t{} at line {} at offset {}", v.path, v.line_num, v.byte_offset);
149                }
150                println!();
151            }
152        }
153    }
154}