acorn_lib/analyzer/
mod.rs

1use crate::constants::{APPLICATION, VALE_RELEASES_URL, VALE_VERSION};
2use crate::util::{
3    download_binary, get_checksum, get_command_folder, get_extension, make_executable, path_to_string, pretty_print, test_command, to_string,
4    Constant, Label, ProgrammingLanguage, SemanticVersion,
5};
6use color_eyre::owo_colors::OwoColorize;
7use duct::cmd;
8use flate2::read::GzDecoder;
9use ini::Ini;
10use std::collections::HashMap;
11use std::fs::File;
12use std::fs::{create_dir_all, remove_file};
13use std::io::prelude::*;
14use std::path::PathBuf;
15use tar::Archive;
16use titlecase::Titlecase;
17use tracing::{debug, error, info, trace};
18use which::which;
19
20pub mod readability;
21pub mod vale;
22
23use vale::{parse_vale_output, print_vale_output, Vale, ValeConfig};
24
25pub trait StaticAnalyzer {
26    fn command(self) -> String;
27    fn analyze(&self, id: String, content: String, output: Option<String>) -> usize;
28    fn download(self, config: Option<ValeConfig>) -> Self;
29    fn download_checksums(self) -> Result<HashMap<String, String>, String>;
30    fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf;
31    fn sync(self) -> Result<(), std::io::Error>;
32    fn with_binary(self, path: PathBuf) -> Self;
33    fn with_config(self, value: ValeConfig) -> Self;
34    fn with_system_command(self) -> Self;
35    fn with_version(self, value: String) -> Self;
36}
37pub trait StaticAnalyzerConfig {
38    fn default() -> ValeConfig;
39    fn ini(self) -> Ini;
40    fn save(self) -> ValeConfig;
41}
42impl StaticAnalyzer for Vale {
43    fn command(self) -> String {
44        "vale".to_string()
45    }
46    fn analyze(&self, id: String, content: String, output: Option<String>) -> usize {
47        let root = get_command_folder("check", None);
48        match create_dir_all(root.clone()) {
49            | Ok(_) => {}
50            | Err(why) => error!(path = path_to_string(root.clone()), "=> {} Create - {}", Label::fail(), why),
51        }
52        let path = root.join(&id);
53        let mut file = match File::create(&path) {
54            | Ok(file) => file,
55            | Err(why) => panic!("=> {} Create file {} - {}", Label::fail(), path.display(), why),
56        };
57        file.write_all(content.as_bytes())
58            .expect("Unable to write to cache directory project file");
59        let binary = match &self.binary {
60            | Some(value) => value,
61            | None => {
62                error!("=> {} {} binary", Label::not_found(), self.clone().command());
63                std::process::exit(exitcode::UNAVAILABLE);
64            }
65        };
66        match &self.config {
67            | Some(config) => {
68                let result = match output {
69                    | Some(value) => cmd!(
70                        binary,
71                        "--no-wrap",
72                        "--config",
73                        config.clone().path,
74                        "--output",
75                        value,
76                        path.clone(),
77                        "--ext",
78                        ".md",
79                        "--no-exit",
80                    )
81                    .read(),
82                    | None => cmd!(
83                        binary,
84                        "--no-wrap",
85                        "--config",
86                        config.clone().path,
87                        path.clone(),
88                        "--ext",
89                        ".md",
90                        "--no-exit"
91                    )
92                    .read(),
93                };
94                match result {
95                    | Ok(output) => {
96                        let parsed = parse_vale_output(path, &output);
97                        if parsed.is_empty() {
98                            let message = format!("=> {} {} has {}", Label::pass(), id.to_string().underline(), "no prose issues".green(),);
99                            info!("{}", message);
100                            0
101                        } else {
102                            error!("=> {} {} issues found in {}", Label::fail(), parsed.len(), id.to_string().underline());
103                            print_vale_output(parsed.clone());
104                            let highlight = parsed.clone().into_iter().map(|item| item.line as usize).collect::<Vec<_>>();
105                            println!();
106                            pretty_print(&content, ProgrammingLanguage::Markdown, highlight);
107                            println!("\n");
108                            parsed.len()
109                        }
110                    }
111                    | Err(output) => {
112                        error!("=> {} Analyze - {}", Label::fail(), output);
113                        1
114                    }
115                }
116            }
117            | None => {
118                let title = self.clone().command().titlecase();
119                error!("=> {} {} configuration", Label::not_found(), title);
120                std::process::exit(exitcode::UNAVAILABLE);
121            }
122        }
123    }
124    // TODO: Check if binary has already been downloaded
125    fn download(self, config: Option<ValeConfig>) -> Vale {
126        // https://doc.rust-lang.org/std/env/consts/constant.OS.html
127        let os = std::env::consts::OS.to_lowercase();
128        let platform = match os.as_str() {
129            | "linux" => "Linux_64-bit.tar.gz",
130            | "macos" | "apple" => "macOS_64-bit.tar.gz",
131            | "windows" => "Windows_64-bit.zip",
132            | _ => {
133                error!(os, "=> {}", Label::not_found());
134                std::process::exit(exitcode::UNAVAILABLE);
135            }
136        };
137        let release = match self.version {
138            | Some(value) => value,
139            | None => SemanticVersion::from_string(VALE_VERSION),
140        };
141        let url = format!(
142            "{}/download/v{}/{}_{}_{}",
143            VALE_RELEASES_URL,
144            release,
145            self.clone().command(),
146            release,
147            platform
148        );
149        info!(url, "=> {} Vale release v{}", Label::using(), release);
150        let binary = match download_binary(url.clone(), PathBuf::from(".")) {
151            | Ok(path) => {
152                let checksum = match self.clone().download_checksums() {
153                    | Ok(value) => value.get(platform).unwrap().to_string(),
154                    | Err(_) => "".to_string(),
155                };
156                let calculated = get_checksum(path.clone());
157                if !checksum.eq(&calculated) {
158                    error!(checksum, calculated, "=> {}", Label::invalid());
159                    let _cleanup = remove_file(path);
160                    std::process::exit(exitcode::USAGE);
161                } else {
162                    info!(checksum, "=> {}", Label::pass());
163                }
164                // TODO: Provide option to save to cache project directory
165                let destination = match config.clone() {
166                    | Some(value) => value.path.parent().unwrap().to_path_buf(),
167                    | None => PathBuf::from("./.vale/"),
168                };
169                let binary = self.clone().extract(path.clone(), Some(destination));
170                if make_executable(&binary) {
171                    let _cleanup = remove_file(path);
172                    Some(binary)
173                } else {
174                    error!("=> {} {} not executable", Label::fail(), self.command());
175                    None
176                }
177            }
178            | Err(error) => {
179                error!(error, url, "=> {} {} download", Label::fail(), self.command());
180                None
181            }
182        };
183        let builder = Vale::init().version(release).maybe_binary(binary);
184        match config {
185            | Some(value) => builder.config(value).build(),
186            | None => {
187                let config = ValeConfig::default();
188                builder.config(config).build()
189            }
190        }
191    }
192    fn download_checksums(self) -> Result<HashMap<String, String>, String> {
193        let release = match self.version {
194            | Some(value) => value,
195            | None => SemanticVersion::from_string(VALE_VERSION),
196        };
197        let url = format!(
198            "{}/download/v{}/{}_{}_checksums.txt",
199            VALE_RELEASES_URL,
200            release,
201            self.clone().command(),
202            release
203        );
204        let client = reqwest::blocking::Client::new();
205        let response = client.get(url).send().unwrap();
206        let content = response.text().unwrap();
207        let checksums = content.lines().clone().fold(HashMap::new(), |mut acc: HashMap<String, String>, line| {
208            let mut values = line.split("  ").collect::<Vec<&str>>();
209            let key = values.pop().unwrap()["vale_#.#.#_".len()..].to_string();
210            let value = values.pop().unwrap().to_string();
211            acc.insert(key, value);
212            acc
213        });
214        debug!(
215            "=> {} {} checksums {:#?}",
216            Label::using(),
217            self.command().titlecase(),
218            checksums.dimmed().cyan()
219        );
220        Ok(checksums)
221    }
222    fn extract(self, path: PathBuf, destination: Option<PathBuf>) -> PathBuf {
223        match get_extension(&path).as_str() {
224            | "zip" => unimplemented!(),
225            | _ => {
226                let tar_gz = File::open(path).unwrap();
227                let tar = GzDecoder::new(tar_gz);
228                let mut archive = Archive::new(tar);
229                let parent = match destination {
230                    | Some(value) => path_to_string(value),
231                    | None => "./.vale/".to_string(),
232                };
233                let message = format!("Unable to extract {} binary", self.clone().command());
234                archive.unpack(parent.clone()).expect(&message);
235                debug!(parent, "=> {} Extracted {} binary", Label::using(), self.command());
236                PathBuf::from(format!("{}/vale", parent))
237            }
238        }
239    }
240    fn sync(self) -> Result<(), std::io::Error> {
241        let path = match self.binary {
242            | Some(value) => value,
243            | None => {
244                error!("=> {} {} binary", Label::not_found(), self.command());
245                std::process::exit(exitcode::UNAVAILABLE);
246            }
247        };
248        let config_path = self.config.unwrap().path;
249        let result = cmd!(path.clone(), "--config", config_path.clone(), "sync").run();
250        match result {
251            | Ok(_) => {
252                let parent = format!("{}/styles/config/vocabularies/{}", config_path.parent().unwrap().display(), APPLICATION);
253                debug!(parent, "=> {} Vocabularies", Label::using());
254                match create_dir_all(parent.clone()) {
255                    | Ok(_) => {}
256                    | Err(why) => error!(directory = parent, "=> {} Create - {}", Label::fail(), why),
257                }
258                match File::create(format!("{}/accept.txt", parent)) {
259                    | Ok(mut file) => {
260                        // TODO: Concatenate organization alternative names to accept file
261                        let acronyms = Constant::get_last_values("acronyms");
262                        let partners = Constant::get_last_values("partners");
263                        let sponsors = Constant::get_last_values("sponsors");
264                        let words = Constant::read_lines("accept.txt");
265                        let content = acronyms.chain(partners).chain(sponsors).chain(words).collect::<Vec<String>>().join("\n");
266                        file.write_all(content.as_bytes()).expect("Unable to write to accept.txt");
267                    }
268                    | Err(why) => panic!("=> {} Create accept.txt - {}", Label::fail(), why),
269                }
270                match File::create(format!("{}/reject.txt", parent)) {
271                    | Ok(mut file) => {
272                        let content = Constant::read_lines("reject.txt").join("\n");
273                        file.write_all(content.as_bytes()).expect("Unable to write to reject.txt");
274                    }
275                    | Err(why) => panic!("=> {} Create reject.txt - {}", Label::fail(), why),
276                }
277                Ok(())
278            }
279            | Err(why) => {
280                error!("=> {} Vale sync - {}", Label::fail(), why);
281                std::process::exit(exitcode::SOFTWARE);
282            }
283        }
284    }
285    fn with_binary(mut self, path: PathBuf) -> Self {
286        self.binary = Some(path);
287        self
288    }
289    fn with_config(mut self, value: ValeConfig) -> Self {
290        self.config = Some(value);
291        self
292    }
293    fn with_system_command(mut self) -> Self {
294        let name = self.clone().command();
295        if test_command(name.clone()) {
296            let path = which(name.clone()).unwrap().to_path_buf();
297            self.binary = Some(path.clone());
298            let offset = "vale version ".len();
299            let version = cmd!(name.clone(), "--version").read().unwrap()[offset..].to_string();
300            self.version = Some(SemanticVersion::from_string(&version));
301            debug!(
302                path = path_to_string(path),
303                "=> {} System {} (v{}) command",
304                Label::using(),
305                name.green().bold(),
306                version
307            );
308        }
309        self
310    }
311    fn with_version(mut self, value: String) -> Self {
312        self.version = Some(SemanticVersion::from_string(&value));
313        self
314    }
315}
316impl StaticAnalyzerConfig for ValeConfig {
317    fn default() -> Self {
318        let config = ValeConfig::init()
319            .packages(to_string(vec!["Google", "proselint", "write-good", "Joblint"]))
320            .vocabularies(to_string(vec![APPLICATION]))
321            .disabled(to_string(vec![
322                "Vale.Terms",
323                "Google.EmDash",
324                "Google.Contractions",
325                "Google.GenderBias",
326                "Google.Headings",
327                "Google.Parens",
328                "Google.Quotes",
329                "Google.We",
330                "Joblint.Competitive",
331                "proselint.GenderBias",
332                "write-good.E-Prime",
333                "write-good.Passive",
334                "write-good.TooWordy",
335                "write-good.Weasel",
336            ]))
337            .build();
338        trace!("=> {} Default - {:#?}", Label::using(), config.dimmed().cyan());
339        config
340    }
341    fn ini(self) -> Ini {
342        let ValeConfig {
343            packages,
344            vocabularies,
345            disabled,
346            ..
347        } = self;
348        let mut conf = Ini::new();
349        // CAUTION: Order of attributes in INI file matter. "StylesPath" must come before "Vocab"
350        conf.with_section::<String>(None)
351            .set("StylesPath", "styles")
352            .set("Vocab", vocabularies.join(", "))
353            .set("Packages", packages.join(", "));
354        conf.with_section(Some("*"))
355            .set("BasedOnStyles", format!("Vale, {}", packages.join(", ")));
356        disabled.iter().for_each(|rule| {
357            conf.with_section(Some("*")).set(rule, "NO");
358        });
359        conf
360    }
361    fn save(self) -> ValeConfig {
362        let path = self.clone().path;
363        let parent = path.parent().unwrap().to_path_buf();
364        match create_dir_all(parent.clone()) {
365            | Ok(_) => {}
366            | Err(why) => error!(directory = path_to_string(parent), "=> {} Create - {}", Label::fail(), why),
367        }
368        match self.clone().ini().write_to_file(path.clone()) {
369            | Ok(_) => {
370                debug!(path = path_to_string(path), "=> {} Saved configuration", Label::using());
371            }
372            | Err(why) => {
373                error!("=> {} Save configuration - {}", Label::fail(), why);
374                std::process::exit(exitcode::SOFTWARE);
375            }
376        }
377        self
378    }
379}
380
381#[cfg(test)]
382mod tests;