nu-lint 1.2.0

Linter for Nu shell scripts that helpfully suggests improvements
Documentation
use crate::{
    LintLevel,
    context::{ExternalCmdFixData, LintContext},
    rule::{DetectFix, Rule},
    violation::{Detection, Fix, Replacement},
};

const NOTE: &str = "Use 'sys mem' to get structured memory information. Nu's sys mem returns a \
                    record with total, free, used, and available memory fields that you can \
                    easily filter and manipulate.";

#[derive(Default)]
struct FreeOptions {
    human_readable: bool,
    bytes: bool,
    kilobytes: bool,
    megabytes: bool,
    gigabytes: bool,
    wide: bool,
    total: bool,
}

impl FreeOptions {
    fn parse<'a>(args: impl IntoIterator<Item = &'a str>) -> Self {
        let mut opts = Self::default();

        for text in args {
            Self::parse_arg(&mut opts, text);
        }

        opts
    }

    fn parse_arg(opts: &mut Self, text: &str) {
        match text {
            "-h" | "--human" => opts.human_readable = true,
            "-b" | "--bytes" => opts.bytes = true,
            "-k" | "--kibi" | "--kilo" => opts.kilobytes = true,
            "-m" | "--mebi" | "--mega" => opts.megabytes = true,
            "-g" | "--gibi" | "--giga" => opts.gigabytes = true,
            "-w" | "--wide" => opts.wide = true,
            "-t" | "--total" => opts.total = true,
            _ => {}
        }
    }

    fn to_nushell(&self) -> (String, String) {
        let base = "sys mem";

        let (replacement, description) =
            if self.bytes || self.kilobytes || self.megabytes || self.gigabytes {
                let unit = if self.bytes {
                    "bytes"
                } else if self.kilobytes {
                    "KB"
                } else if self.megabytes {
                    "MB"
                } else {
                    "GB"
                };

                let replacement = base.to_string();
                let description = format!(
                    "Use 'sys mem' to get memory info. All fields are already in human-readable \
                     format. To convert to {unit}, use field conversion like: sys mem | get total \
                     | into int"
                );
                (replacement, description)
            } else if self.total {
                let replacement = base.to_string();
                let description = "Use 'sys mem' to get total memory information. All totals are \
                                   included in the returned record."
                    .to_string();
                (replacement, description)
            } else {
                let replacement = base.to_string();
                let description = "Use 'sys mem' to get structured memory information with fields \
                                   like 'total', 'free', 'used', and 'available'. Much easier to \
                                   work with than parsing free's text output."
                    .to_string();
                (replacement, description)
            };

        (replacement, description)
    }
}

struct UseSysMemInsteadOfFree;

impl DetectFix for UseSysMemInsteadOfFree {
    type FixInput<'a> = ExternalCmdFixData<'a>;

    fn id(&self) -> &'static str {
        "free_to_sys_mem"
    }

    fn short_description(&self) -> &'static str {
        "`free` replaceable with `sys mem` for memory info"
    }

    fn source_link(&self) -> Option<&'static str> {
        Some("https://www.nushell.sh/commands/docs/sys_mem.html")
    }

    fn level(&self) -> LintLevel {
        LintLevel::Warning
    }

    fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
        context.detect_external_with_validation("free", |_, _, _| Some(NOTE))
    }

    fn fix(&self, context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
        let opts = FreeOptions::parse(fix_data.arg_texts(context));
        let (replacement, description) = opts.to_nushell();

        Some(Fix {
            explanation: description.into(),
            replacements: vec![Replacement {
                span: fix_data.expr_span.into(),
                replacement_text: replacement.into(),
            }],
        })
    }
}

pub static RULE: &dyn Rule = &UseSysMemInsteadOfFree;

#[cfg(test)]
mod detect_bad;
#[cfg(test)]
mod generated_fix;
#[cfg(test)]
mod ignore_good;