Commit 291e6400 authored by Wohlgemuth, Jason's avatar Wohlgemuth, Jason
Browse files

feat: Refactor final std::io::Command usages with cmd macro

parent a42c8b2c
Loading
Loading
Loading
Loading
Loading
+28 −24
Original line number Diff line number Diff line
@@ -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;
@@ -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() => {
+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)]
@@ -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());
}
+4 −1
Original line number Diff line number Diff line
@@ -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")
}
+1 −2
Original line number Diff line number Diff line
@@ -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"))]
@@ -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;
+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 {
@@ -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