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