Loading acorn-lib/src/analyzer/mod.rs +28 −24 Original line number Diff line number Diff line Loading @@ -3,20 +3,18 @@ //! This is where we keep functions and interfaces necessary to execute ACORN's automated editorial style guide as well as content readability analyzer. //! use crate::analyzer::vale::{ValeOutput, ValeOutputItem}; use crate::cmd; use crate::io::{command_exists, download_binary, extract_zip, file_checksum, make_executable, standard_project_folder}; #[cfg(feature = "std")] use crate::io::{get, InputOutput}; use crate::prelude; use crate::prelude::{create_dir_all, remove_file, Command, CommandOutput, Error, File, HashMap, PathBuf, Write}; use crate::prelude::{self, create_dir_all, remove_file, Command, CommandOutput, Error, File, HashMap, PathBuf, Write}; use crate::schema::research_activity::ResearchActivity; use crate::schema::{ImageObject, MediaObject, Organization, ProgrammingLanguage, VideoObject, Website}; use crate::skip; use crate::util::constants::{ APPLICATION, CUSTOM_VALE_PACKAGE_NAME, DEFAULT_VALE_PACKAGE_URL, DEFAULT_VALE_ROOT, DISABLED_VALE_RULES, ENABLED_VALE_PACKAGES, ORGANIZATION, VALE_RELEASES_URL, VALE_VERSION, }; use crate::util::{suffix, Constant, Label, SemanticVersion, ToAbsoluteString, ToMarkdown}; use crate::{cmd, skip}; use crate::{Location, Repository}; use ariadne::{Config, Report, Source}; use async_trait::async_trait; Loading Loading @@ -516,26 +514,32 @@ impl StaticAnalyzer<ValeConfig> for Vale { match &self.config { | Some(config) => { let result = match output { | Some(value) => Command::new(binary) .arg("--no-wrap") .arg("--config") .arg(config.clone().path) .arg("--output") .arg(value) .arg(path.clone()) .arg("--ext") .arg(".md") .arg("--no-exit") .output(), | None => Command::new(binary) .arg("--no-wrap") .arg("--config") .arg(config.clone().path) .arg(path.clone()) .arg("--ext") .arg(".md") .arg("--no-exit") .output(), | Some(value) => cmd!( binary.to_absolute_string(), [ "--no-wrap", "--config", &config.clone().path.to_absolute_string(), "--output", value.as_str(), &path.clone().to_absolute_string(), "--ext", ".md", "--no-exit" ] ), | None => cmd!( binary.to_absolute_string(), [ "--no-wrap", "--config", &config.clone().path.to_absolute_string(), &path.clone().to_absolute_string(), "--ext", ".md", "--no-exit" ] ), }; match result { | Ok(output) if output.status.success() => { Loading acorn-lib/src/analyzer/tests/mod.rs +226 −1 Original line number Diff line number Diff line use crate::analyzer::vale::{ValeOutput, ValeOutputItem}; use crate::analyzer::vale::{preprocess_vale_output, ValeOutput, ValeOutputItem, ValeOutputItemSeverity}; use crate::analyzer::{checks_to_dataframe, Check, CheckCategory}; use crate::prelude::PathBuf; #[cfg(test)] Loading Loading @@ -75,3 +75,228 @@ fn test_parse_vale_output() { let parsed: Vec<ValeOutputItem> = ValeOutput::parse(data, PathBuf::from(path)); assert_eq!(parsed.len(), 3); } #[test] #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] fn test_preprocess_vale_output_unix() { let path = PathBuf::from("/tmp/acorn/cache/check/test-file"); let input = r#"{ "/tmp/acorn/cache/check/test-file": [ { "Action": { "Name": "", "Params": null }, "Span": [192, 254], "Check": "Google.OxfordComma", "Description": "", "Link": "https://developers.google.com/style/commas", "Message": "Use the Oxford comma in 'Once created, there is often no version control, stewardship or'.", "Severity": "warning", "Match": "Once created, there is often no version control, stewardship or", "Line": 38 } ] }"#; let result = preprocess_vale_output(path, input); assert!(result.contains("\"items\":")); assert!(!result.contains("/tmp/acorn/cache/check/test-file")); // Verify the JSON is still valid after preprocessing let parsed: serde_json::Result<serde_json::Value> = serde_json::from_str(&result); assert!(parsed.is_ok(), "Preprocessed output should be valid JSON"); } #[test] #[cfg(windows)] fn test_preprocess_vale_output_windows() { let path = PathBuf::from(r"C:\Users\jason\AppData\Local\ornl\acorn\cache\check\cyzdN-Q4Nt\invalid-project-a"); let input = r#"{ "\\\\?\\C:\\Users\\jason\\AppData\\Local\\ornl\\acorn\\cache\\check\\cyzdN-Q4Nt\\invalid-project-a": [ { "Action": { "Name": "", "Params": null }, "Span": [192, 254], "Check": "Google.OxfordComma", "Description": "", "Link": "https://developers.google.com/style/commas", "Message": "Use the Oxford comma in 'Once created, there is often no version control, stewardship or'.", "Severity": "warning", "Match": "Once created, there is often no version control, stewardship or", "Line": 38 } ] }"#; let result = preprocess_vale_output(path.clone(), input); assert!(result.contains("\"items\":"), "Result should contain 'items' key"); assert!(!result.contains("\\\\"), "Result should not contain double backslashes"); assert!(!result.contains("C:/Users/jason"), "Result should not contain the path"); assert!(!result.contains("//?/"), "Result should not contain extended path prefix"); // Verify the JSON is still valid after preprocessing let parsed: serde_json::Result<serde_json::Value> = serde_json::from_str(&result); assert!(parsed.is_ok(), "Preprocessed output should be valid JSON"); } #[test] #[cfg(windows)] fn test_preprocess_vale_output_windows_full_example() { let path = PathBuf::from(r"C:\Users\jason\AppData\Local\ornl\acorn\cache\check\cyzdN-Q4Nt\invalid-project-a"); let input = r#"{ "\\\\?\\C:\\Users\\jason\\AppData\\Local\\ornl\\acorn\\cache\\check\\cyzdN-Q4Nt\\invalid-project-a": [ { "Action": { "Name": "", "Params": null }, "Span": [192, 254], "Check": "Google.OxfordComma", "Description": "", "Link": "https://developers.google.com/style/commas", "Message": "Use the Oxford comma in 'Once created, there is often no version control, stewardship or'.", "Severity": "warning", "Match": "Once created, there is often no version control, stewardship or", "Line": 38 }, { "Action": { "Name": "suggest", "Params": ["spellings"] }, "Span": [23, 31], "Check": "Vale.Spelling", "Description": "", "Link": "", "Message": "Did you really mean 'Indexable'?", "Severity": "error", "Match": "Indexable", "Line": 47 }, { "Action": { "Name": "", "Params": null }, "Span": [80, 82], "Check": "Google.Acronyms", "Description": "", "Link": "https://developers.google.com/style/abbreviations", "Message": "Spell out 'MNA', if it's unfamiliar to the audience.", "Severity": "suggestion", "Match": "MNA", "Line": 48 }, { "Action": { "Name": "", "Params": null }, "Span": [84, 87], "Check": "Google.Will", "Description": "", "Link": "https://developers.google.com/style/tense", "Message": "Avoid using 'will'.", "Severity": "warning", "Match": "will", "Line": 48 }, { "Action": { "Name": "replace", "Params": ["geospatial"] }, "Span": [36, 46], "Check": "Science.use-instead", "Description": "", "Link": "", "Message": "Use 'geospatial' instead of 'geo-spatial'.", "Severity": "error", "Match": "geo-spatial", "Line": 49 }, { "Action": { "Name": "suggest", "Params": ["spellings"] }, "Span": [9, 15], "Check": "Vale.Spelling", "Description": "", "Link": "", "Message": "Did you really mean 'Jasdrey'?", "Severity": "error", "Match": "Jasdrey", "Line": 61 }, { "Action": { "Name": "suggest", "Params": ["spellings"] }, "Span": [17, 23], "Check": "Vale.Spelling", "Description": "", "Link": "", "Message": "Did you really mean 'Wohlson'?", "Severity": "error", "Match": "Wohlson", "Line": 61 }, { "Action": { "Name": "replace", "Params": ["email"] }, "Span": [3, 7], "Check": "Google.WordList", "Description": "", "Link": "https://developers.google.com/style/word-list", "Message": "Use 'email' instead of 'Email'.", "Severity": "warning", "Match": "Email", "Line": 62 } ] }"#; let result = preprocess_vale_output(path, input); // Verify the JSON is valid and can be parsed as ValeOutput let parsed: serde_json::Result<ValeOutput> = serde_json::from_str(&result); assert!(parsed.is_ok(), "Should parse as valid ValeOutput"); if let Ok(vale_output) = parsed { assert_eq!(vale_output.items.len(), 8, "Should contain 8 items"); // Verify specific items assert_eq!(vale_output.items[0].check, "Google.OxfordComma"); assert_eq!(vale_output.items[1].check, "Vale.Spelling"); assert_eq!(vale_output.items[1].line, 47); assert_eq!(vale_output.items[7].check, "Google.WordList"); } } #[test] fn test_vale_output_parse_empty() { let path = PathBuf::from("test-file"); let empty_output = "{}"; let result = ValeOutput::parse(empty_output, path); assert_eq!(result.len(), 0, "Empty output should return empty vector"); } #[test] fn test_vale_output_severity_colored() { let warning = ValeOutputItemSeverity::Warning; let error = ValeOutputItemSeverity::Error; let suggestion = ValeOutputItemSeverity::Suggestion; assert!(!warning.colored().is_empty()); assert!(!error.colored().is_empty()); assert!(!suggestion.colored().is_empty()); } acorn-lib/src/analyzer/vale.rs +4 −1 Original line number Diff line number Diff line Loading @@ -166,5 +166,8 @@ pub(crate) fn preprocess_vale_output(path: PathBuf, output: &str) -> String { #[cfg(windows)] pub(crate) fn preprocess_vale_output(path: PathBuf, output: &str) -> String { let input = path.as_path().display().to_string().replace("\\", "/"); output.replace("\\\\", "/").replace(&input, "items") // First replace double backslashes with forward slashes let normalized = output.replace("\\\\", "/"); // Then handle the extended-length path prefix and replace the full path normalized.replace(&format!("//?/{}", input), "items").replace(&input, "items") } acorn-lib/src/io/mod.rs +1 −2 Original line number Diff line number Diff line Loading @@ -19,8 +19,6 @@ //! write_file(PathBuf::from("/path/to/that/file"), contents); //! ``` //! use crate::cmd; use crate::fail; use crate::prelude::CommandOutput; use crate::prelude::{canonicalize, create_dir_all, io, var, BufReader, Cursor, Error, File, PathBuf, Read, Write}; #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] Loading @@ -29,6 +27,7 @@ use crate::util::constants::{APPLICATION, LARGE_FILE_THRESHOLD_BYTES, ORGANIZATI #[cfg(windows)] use crate::util::file_extension; use crate::util::{generate_guid, suffix, Label, MimeType, SemanticVersion, ToAbsoluteString, ToStrings}; use crate::{cmd, fail}; use core::time::Duration; use data_encoding::HEXUPPER; use directories::ProjectDirs; Loading acorn-lib/src/util/macros.rs +120 −120 Original line number Diff line number Diff line //! Macros for logging and other utilities //! Macros /// Execute a command and capture its output. /// /// Simplifies `Command::new(binary).args(args).output()` patterns. /// Thread safe — `Command` is `Send + Sync`. /// /// Use the `status` prefix to call `.status()` instead of `.output()`. /// Use the `sh` prefix to parse a single string as a shell command (splits on whitespace). /// /// # Syntax /// /// ```ignore /// // Parse a single string (supports format! interpolation) /// cmd!(sh "git diff-tree --no-commit-id --name-only") /// cmd!(sh format!("git diff --name-only {}", branch)) /// /// // CLI-style with string literals (returns Result<Output, io::Error>) /// cmd!("git" "diff-tree" "--no-commit-id" "--name-only") /// /// // Capture output with array or variable /// cmd!("git", ["branch", "--show-current"]) /// cmd!("git", args) /// /// // Check exit status only (returns Result<ExitStatus, io::Error>) /// cmd!(sh status "vale --config .vale.ini sync") /// cmd!(status "vale" "--config" ".vale.ini" "sync") /// cmd!(status "vale", ["--config", path, "sync"]) /// cmd!(status "vale", args) /// ``` /// /// # Examples /// /// ```ignore /// use acorn::cmd; /// use acorn::prelude::CommandOutput; /// /// // Parse a full command string — great for interpolated values /// let branch = "main"; /// match cmd!(sh format!("git diff --name-only {branch}")) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // CLI-style — space-separated string literals /// match cmd!("git" "branch" "--show-current") { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// Ok(output) => eprintln!("{}", output.stderr()), /// Err(why) => eprintln!("Error: {}", why), /// } /// /// // Array-style — for dynamic or mixed-type args /// let args = vec!["diff", "--name-only", "main"]; /// match cmd!("git", args) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // Status-only check /// match cmd!(sh status "vale --config .vale.ini sync") { /// Ok(status) if status.success() => println!("synced"), /// _ => eprintln!("sync failed"), /// } /// ``` #[macro_export] macro_rules! cmd { // Parse string and capture output: cmd!(sh "git diff --name-only") (sh $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).output() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Parse string and check status: cmd!(sh status "vale --config .vale.ini sync") (sh status $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).status() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Status with array literal: cmd!(status binary, [arg1, arg2]) (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Status with args variable: cmd!(status binary, args) (status $binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).status() }}; // Status CLI-style: cmd!(status "vale" "--config" ".vale.ini" "sync") (status $binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Output with array literal: cmd!(binary, [arg1, arg2]) ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; // Output with args variable: cmd!(binary, args) ($binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).output() }}; // CLI-style: cmd!("git" "diff-tree" "--no-commit-id" "--name-only") ($binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; } /// Logging macro for failures #[macro_export] macro_rules! fail { Loading Loading @@ -130,122 +249,3 @@ macro_rules! param { .with_key($name) }; } /// Execute a command and capture its output. /// /// Simplifies `Command::new(binary).args(args).output()` patterns. /// Thread safe — `Command` is `Send + Sync`. /// /// Use the `status` prefix to call `.status()` instead of `.output()`. /// Use the `sh` prefix to parse a single string as a shell command (splits on whitespace). /// /// # Syntax /// /// ```ignore /// // Parse a single string (supports format! interpolation) /// cmd!(sh "git diff-tree --no-commit-id --name-only") /// cmd!(sh format!("git diff --name-only {}", branch)) /// /// // CLI-style with string literals (returns Result<Output, io::Error>) /// cmd!("git" "diff-tree" "--no-commit-id" "--name-only") /// /// // Capture output with array or variable /// cmd!("git", ["branch", "--show-current"]) /// cmd!("git", args) /// /// // Check exit status only (returns Result<ExitStatus, io::Error>) /// cmd!(sh status "vale --config .vale.ini sync") /// cmd!(status "vale" "--config" ".vale.ini" "sync") /// cmd!(status "vale", ["--config", path, "sync"]) /// cmd!(status "vale", args) /// ``` /// /// # Examples /// /// ```ignore /// use acorn::cmd; /// use acorn::prelude::CommandOutput; /// /// // Parse a full command string — great for interpolated values /// let branch = "main"; /// match cmd!(sh format!("git diff --name-only {branch}")) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // CLI-style — space-separated string literals /// match cmd!("git" "branch" "--show-current") { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// Ok(output) => eprintln!("{}", output.stderr()), /// Err(why) => eprintln!("Error: {}", why), /// } /// /// // Array-style — for dynamic or mixed-type args /// let args = vec!["diff", "--name-only", "main"]; /// match cmd!("git", args) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // Status-only check /// match cmd!(sh status "vale --config .vale.ini sync") { /// Ok(status) if status.success() => println!("synced"), /// _ => eprintln!("sync failed"), /// } /// ``` #[macro_export] macro_rules! cmd { // Parse string and capture output: cmd!(sh "git diff --name-only") (sh $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).output() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Parse string and check status: cmd!(sh status "vale --config .vale.ini sync") (sh status $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).status() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Status with array literal: cmd!(status binary, [arg1, arg2]) (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Status with args variable: cmd!(status binary, args) (status $binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).status() }}; // Status CLI-style: cmd!(status "vale" "--config" ".vale.ini" "sync") (status $binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Output with array literal: cmd!(binary, [arg1, arg2]) ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; // Output with args variable: cmd!(binary, args) ($binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).output() }}; // CLI-style: cmd!("git" "diff-tree" "--no-commit-id" "--name-only") ($binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; } Loading
acorn-lib/src/analyzer/mod.rs +28 −24 Original line number Diff line number Diff line Loading @@ -3,20 +3,18 @@ //! This is where we keep functions and interfaces necessary to execute ACORN's automated editorial style guide as well as content readability analyzer. //! use crate::analyzer::vale::{ValeOutput, ValeOutputItem}; use crate::cmd; use crate::io::{command_exists, download_binary, extract_zip, file_checksum, make_executable, standard_project_folder}; #[cfg(feature = "std")] use crate::io::{get, InputOutput}; use crate::prelude; use crate::prelude::{create_dir_all, remove_file, Command, CommandOutput, Error, File, HashMap, PathBuf, Write}; use crate::prelude::{self, create_dir_all, remove_file, Command, CommandOutput, Error, File, HashMap, PathBuf, Write}; use crate::schema::research_activity::ResearchActivity; use crate::schema::{ImageObject, MediaObject, Organization, ProgrammingLanguage, VideoObject, Website}; use crate::skip; use crate::util::constants::{ APPLICATION, CUSTOM_VALE_PACKAGE_NAME, DEFAULT_VALE_PACKAGE_URL, DEFAULT_VALE_ROOT, DISABLED_VALE_RULES, ENABLED_VALE_PACKAGES, ORGANIZATION, VALE_RELEASES_URL, VALE_VERSION, }; use crate::util::{suffix, Constant, Label, SemanticVersion, ToAbsoluteString, ToMarkdown}; use crate::{cmd, skip}; use crate::{Location, Repository}; use ariadne::{Config, Report, Source}; use async_trait::async_trait; Loading Loading @@ -516,26 +514,32 @@ impl StaticAnalyzer<ValeConfig> for Vale { match &self.config { | Some(config) => { let result = match output { | Some(value) => Command::new(binary) .arg("--no-wrap") .arg("--config") .arg(config.clone().path) .arg("--output") .arg(value) .arg(path.clone()) .arg("--ext") .arg(".md") .arg("--no-exit") .output(), | None => Command::new(binary) .arg("--no-wrap") .arg("--config") .arg(config.clone().path) .arg(path.clone()) .arg("--ext") .arg(".md") .arg("--no-exit") .output(), | Some(value) => cmd!( binary.to_absolute_string(), [ "--no-wrap", "--config", &config.clone().path.to_absolute_string(), "--output", value.as_str(), &path.clone().to_absolute_string(), "--ext", ".md", "--no-exit" ] ), | None => cmd!( binary.to_absolute_string(), [ "--no-wrap", "--config", &config.clone().path.to_absolute_string(), &path.clone().to_absolute_string(), "--ext", ".md", "--no-exit" ] ), }; match result { | Ok(output) if output.status.success() => { Loading
acorn-lib/src/analyzer/tests/mod.rs +226 −1 Original line number Diff line number Diff line use crate::analyzer::vale::{ValeOutput, ValeOutputItem}; use crate::analyzer::vale::{preprocess_vale_output, ValeOutput, ValeOutputItem, ValeOutputItemSeverity}; use crate::analyzer::{checks_to_dataframe, Check, CheckCategory}; use crate::prelude::PathBuf; #[cfg(test)] Loading Loading @@ -75,3 +75,228 @@ fn test_parse_vale_output() { let parsed: Vec<ValeOutputItem> = ValeOutput::parse(data, PathBuf::from(path)); assert_eq!(parsed.len(), 3); } #[test] #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] fn test_preprocess_vale_output_unix() { let path = PathBuf::from("/tmp/acorn/cache/check/test-file"); let input = r#"{ "/tmp/acorn/cache/check/test-file": [ { "Action": { "Name": "", "Params": null }, "Span": [192, 254], "Check": "Google.OxfordComma", "Description": "", "Link": "https://developers.google.com/style/commas", "Message": "Use the Oxford comma in 'Once created, there is often no version control, stewardship or'.", "Severity": "warning", "Match": "Once created, there is often no version control, stewardship or", "Line": 38 } ] }"#; let result = preprocess_vale_output(path, input); assert!(result.contains("\"items\":")); assert!(!result.contains("/tmp/acorn/cache/check/test-file")); // Verify the JSON is still valid after preprocessing let parsed: serde_json::Result<serde_json::Value> = serde_json::from_str(&result); assert!(parsed.is_ok(), "Preprocessed output should be valid JSON"); } #[test] #[cfg(windows)] fn test_preprocess_vale_output_windows() { let path = PathBuf::from(r"C:\Users\jason\AppData\Local\ornl\acorn\cache\check\cyzdN-Q4Nt\invalid-project-a"); let input = r#"{ "\\\\?\\C:\\Users\\jason\\AppData\\Local\\ornl\\acorn\\cache\\check\\cyzdN-Q4Nt\\invalid-project-a": [ { "Action": { "Name": "", "Params": null }, "Span": [192, 254], "Check": "Google.OxfordComma", "Description": "", "Link": "https://developers.google.com/style/commas", "Message": "Use the Oxford comma in 'Once created, there is often no version control, stewardship or'.", "Severity": "warning", "Match": "Once created, there is often no version control, stewardship or", "Line": 38 } ] }"#; let result = preprocess_vale_output(path.clone(), input); assert!(result.contains("\"items\":"), "Result should contain 'items' key"); assert!(!result.contains("\\\\"), "Result should not contain double backslashes"); assert!(!result.contains("C:/Users/jason"), "Result should not contain the path"); assert!(!result.contains("//?/"), "Result should not contain extended path prefix"); // Verify the JSON is still valid after preprocessing let parsed: serde_json::Result<serde_json::Value> = serde_json::from_str(&result); assert!(parsed.is_ok(), "Preprocessed output should be valid JSON"); } #[test] #[cfg(windows)] fn test_preprocess_vale_output_windows_full_example() { let path = PathBuf::from(r"C:\Users\jason\AppData\Local\ornl\acorn\cache\check\cyzdN-Q4Nt\invalid-project-a"); let input = r#"{ "\\\\?\\C:\\Users\\jason\\AppData\\Local\\ornl\\acorn\\cache\\check\\cyzdN-Q4Nt\\invalid-project-a": [ { "Action": { "Name": "", "Params": null }, "Span": [192, 254], "Check": "Google.OxfordComma", "Description": "", "Link": "https://developers.google.com/style/commas", "Message": "Use the Oxford comma in 'Once created, there is often no version control, stewardship or'.", "Severity": "warning", "Match": "Once created, there is often no version control, stewardship or", "Line": 38 }, { "Action": { "Name": "suggest", "Params": ["spellings"] }, "Span": [23, 31], "Check": "Vale.Spelling", "Description": "", "Link": "", "Message": "Did you really mean 'Indexable'?", "Severity": "error", "Match": "Indexable", "Line": 47 }, { "Action": { "Name": "", "Params": null }, "Span": [80, 82], "Check": "Google.Acronyms", "Description": "", "Link": "https://developers.google.com/style/abbreviations", "Message": "Spell out 'MNA', if it's unfamiliar to the audience.", "Severity": "suggestion", "Match": "MNA", "Line": 48 }, { "Action": { "Name": "", "Params": null }, "Span": [84, 87], "Check": "Google.Will", "Description": "", "Link": "https://developers.google.com/style/tense", "Message": "Avoid using 'will'.", "Severity": "warning", "Match": "will", "Line": 48 }, { "Action": { "Name": "replace", "Params": ["geospatial"] }, "Span": [36, 46], "Check": "Science.use-instead", "Description": "", "Link": "", "Message": "Use 'geospatial' instead of 'geo-spatial'.", "Severity": "error", "Match": "geo-spatial", "Line": 49 }, { "Action": { "Name": "suggest", "Params": ["spellings"] }, "Span": [9, 15], "Check": "Vale.Spelling", "Description": "", "Link": "", "Message": "Did you really mean 'Jasdrey'?", "Severity": "error", "Match": "Jasdrey", "Line": 61 }, { "Action": { "Name": "suggest", "Params": ["spellings"] }, "Span": [17, 23], "Check": "Vale.Spelling", "Description": "", "Link": "", "Message": "Did you really mean 'Wohlson'?", "Severity": "error", "Match": "Wohlson", "Line": 61 }, { "Action": { "Name": "replace", "Params": ["email"] }, "Span": [3, 7], "Check": "Google.WordList", "Description": "", "Link": "https://developers.google.com/style/word-list", "Message": "Use 'email' instead of 'Email'.", "Severity": "warning", "Match": "Email", "Line": 62 } ] }"#; let result = preprocess_vale_output(path, input); // Verify the JSON is valid and can be parsed as ValeOutput let parsed: serde_json::Result<ValeOutput> = serde_json::from_str(&result); assert!(parsed.is_ok(), "Should parse as valid ValeOutput"); if let Ok(vale_output) = parsed { assert_eq!(vale_output.items.len(), 8, "Should contain 8 items"); // Verify specific items assert_eq!(vale_output.items[0].check, "Google.OxfordComma"); assert_eq!(vale_output.items[1].check, "Vale.Spelling"); assert_eq!(vale_output.items[1].line, 47); assert_eq!(vale_output.items[7].check, "Google.WordList"); } } #[test] fn test_vale_output_parse_empty() { let path = PathBuf::from("test-file"); let empty_output = "{}"; let result = ValeOutput::parse(empty_output, path); assert_eq!(result.len(), 0, "Empty output should return empty vector"); } #[test] fn test_vale_output_severity_colored() { let warning = ValeOutputItemSeverity::Warning; let error = ValeOutputItemSeverity::Error; let suggestion = ValeOutputItemSeverity::Suggestion; assert!(!warning.colored().is_empty()); assert!(!error.colored().is_empty()); assert!(!suggestion.colored().is_empty()); }
acorn-lib/src/analyzer/vale.rs +4 −1 Original line number Diff line number Diff line Loading @@ -166,5 +166,8 @@ pub(crate) fn preprocess_vale_output(path: PathBuf, output: &str) -> String { #[cfg(windows)] pub(crate) fn preprocess_vale_output(path: PathBuf, output: &str) -> String { let input = path.as_path().display().to_string().replace("\\", "/"); output.replace("\\\\", "/").replace(&input, "items") // First replace double backslashes with forward slashes let normalized = output.replace("\\\\", "/"); // Then handle the extended-length path prefix and replace the full path normalized.replace(&format!("//?/{}", input), "items").replace(&input, "items") }
acorn-lib/src/io/mod.rs +1 −2 Original line number Diff line number Diff line Loading @@ -19,8 +19,6 @@ //! write_file(PathBuf::from("/path/to/that/file"), contents); //! ``` //! use crate::cmd; use crate::fail; use crate::prelude::CommandOutput; use crate::prelude::{canonicalize, create_dir_all, io, var, BufReader, Cursor, Error, File, PathBuf, Read, Write}; #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] Loading @@ -29,6 +27,7 @@ use crate::util::constants::{APPLICATION, LARGE_FILE_THRESHOLD_BYTES, ORGANIZATI #[cfg(windows)] use crate::util::file_extension; use crate::util::{generate_guid, suffix, Label, MimeType, SemanticVersion, ToAbsoluteString, ToStrings}; use crate::{cmd, fail}; use core::time::Duration; use data_encoding::HEXUPPER; use directories::ProjectDirs; Loading
acorn-lib/src/util/macros.rs +120 −120 Original line number Diff line number Diff line //! Macros for logging and other utilities //! Macros /// Execute a command and capture its output. /// /// Simplifies `Command::new(binary).args(args).output()` patterns. /// Thread safe — `Command` is `Send + Sync`. /// /// Use the `status` prefix to call `.status()` instead of `.output()`. /// Use the `sh` prefix to parse a single string as a shell command (splits on whitespace). /// /// # Syntax /// /// ```ignore /// // Parse a single string (supports format! interpolation) /// cmd!(sh "git diff-tree --no-commit-id --name-only") /// cmd!(sh format!("git diff --name-only {}", branch)) /// /// // CLI-style with string literals (returns Result<Output, io::Error>) /// cmd!("git" "diff-tree" "--no-commit-id" "--name-only") /// /// // Capture output with array or variable /// cmd!("git", ["branch", "--show-current"]) /// cmd!("git", args) /// /// // Check exit status only (returns Result<ExitStatus, io::Error>) /// cmd!(sh status "vale --config .vale.ini sync") /// cmd!(status "vale" "--config" ".vale.ini" "sync") /// cmd!(status "vale", ["--config", path, "sync"]) /// cmd!(status "vale", args) /// ``` /// /// # Examples /// /// ```ignore /// use acorn::cmd; /// use acorn::prelude::CommandOutput; /// /// // Parse a full command string — great for interpolated values /// let branch = "main"; /// match cmd!(sh format!("git diff --name-only {branch}")) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // CLI-style — space-separated string literals /// match cmd!("git" "branch" "--show-current") { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// Ok(output) => eprintln!("{}", output.stderr()), /// Err(why) => eprintln!("Error: {}", why), /// } /// /// // Array-style — for dynamic or mixed-type args /// let args = vec!["diff", "--name-only", "main"]; /// match cmd!("git", args) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // Status-only check /// match cmd!(sh status "vale --config .vale.ini sync") { /// Ok(status) if status.success() => println!("synced"), /// _ => eprintln!("sync failed"), /// } /// ``` #[macro_export] macro_rules! cmd { // Parse string and capture output: cmd!(sh "git diff --name-only") (sh $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).output() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Parse string and check status: cmd!(sh status "vale --config .vale.ini sync") (sh status $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).status() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Status with array literal: cmd!(status binary, [arg1, arg2]) (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Status with args variable: cmd!(status binary, args) (status $binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).status() }}; // Status CLI-style: cmd!(status "vale" "--config" ".vale.ini" "sync") (status $binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Output with array literal: cmd!(binary, [arg1, arg2]) ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; // Output with args variable: cmd!(binary, args) ($binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).output() }}; // CLI-style: cmd!("git" "diff-tree" "--no-commit-id" "--name-only") ($binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; } /// Logging macro for failures #[macro_export] macro_rules! fail { Loading Loading @@ -130,122 +249,3 @@ macro_rules! param { .with_key($name) }; } /// Execute a command and capture its output. /// /// Simplifies `Command::new(binary).args(args).output()` patterns. /// Thread safe — `Command` is `Send + Sync`. /// /// Use the `status` prefix to call `.status()` instead of `.output()`. /// Use the `sh` prefix to parse a single string as a shell command (splits on whitespace). /// /// # Syntax /// /// ```ignore /// // Parse a single string (supports format! interpolation) /// cmd!(sh "git diff-tree --no-commit-id --name-only") /// cmd!(sh format!("git diff --name-only {}", branch)) /// /// // CLI-style with string literals (returns Result<Output, io::Error>) /// cmd!("git" "diff-tree" "--no-commit-id" "--name-only") /// /// // Capture output with array or variable /// cmd!("git", ["branch", "--show-current"]) /// cmd!("git", args) /// /// // Check exit status only (returns Result<ExitStatus, io::Error>) /// cmd!(sh status "vale --config .vale.ini sync") /// cmd!(status "vale" "--config" ".vale.ini" "sync") /// cmd!(status "vale", ["--config", path, "sync"]) /// cmd!(status "vale", args) /// ``` /// /// # Examples /// /// ```ignore /// use acorn::cmd; /// use acorn::prelude::CommandOutput; /// /// // Parse a full command string — great for interpolated values /// let branch = "main"; /// match cmd!(sh format!("git diff --name-only {branch}")) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // CLI-style — space-separated string literals /// match cmd!("git" "branch" "--show-current") { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// Ok(output) => eprintln!("{}", output.stderr()), /// Err(why) => eprintln!("Error: {}", why), /// } /// /// // Array-style — for dynamic or mixed-type args /// let args = vec!["diff", "--name-only", "main"]; /// match cmd!("git", args) { /// Ok(output) if output.status.success() => { /// println!("{}", output.stdout()); /// } /// _ => {}, /// } /// /// // Status-only check /// match cmd!(sh status "vale --config .vale.ini sync") { /// Ok(status) if status.success() => println!("synced"), /// _ => eprintln!("sync failed"), /// } /// ``` #[macro_export] macro_rules! cmd { // Parse string and capture output: cmd!(sh "git diff --name-only") (sh $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).output() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Parse string and check status: cmd!(sh status "vale --config .vale.ini sync") (sh status $cmd:expr) => {{ let cmd_str = $cmd.to_string(); let mut parts = cmd_str.split_whitespace(); match parts.next() { | Some(binary) => { let args: Vec<&str> = parts.collect(); $crate::prelude::Command::new(binary).args(args).status() } | None => Err($crate::prelude::io::Error::other("empty command string")), } }}; // Status with array literal: cmd!(status binary, [arg1, arg2]) (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Status with args variable: cmd!(status binary, args) (status $binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).status() }}; // Status CLI-style: cmd!(status "vale" "--config" ".vale.ini" "sync") (status $binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).status() }}; // Output with array literal: cmd!(binary, [arg1, arg2]) ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; // Output with args variable: cmd!(binary, args) ($binary:expr, $args:expr) => {{ $crate::prelude::Command::new($binary).args($args).output() }}; // CLI-style: cmd!("git" "diff-tree" "--no-commit-id" "--name-only") ($binary:literal $($arg:literal)*) => {{ $crate::prelude::Command::new($binary).args([$($arg),*]).output() }}; }