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 fn download(self, config: Option<ValeConfig>) -> Vale {
126 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 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 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 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;