nu-lint 1.3.0

Linter for Nu shell scripts that helpfully suggests improvements
Documentation
use nu_protocol::ast::{Expr, Expression};

use crate::{
    LintLevel,
    ast::call::CallExt,
    context::LintContext,
    rule::{DetectFix, Rule},
    violation::Detection,
};

fn check_call(expr: &Expression, context: &LintContext) -> Option<Detection> {
    let Expr::Call(call) = &expr.expr else {
        return None;
    };

    if call.get_call_name(context) != "default" {
        return None;
    }

    let arg = call.get_first_positional_arg()?;
    let is_empty_string = matches!(&arg.expr, Expr::String(s) | Expr::RawString(s) if s.is_empty());

    if !is_empty_string {
        return None;
    }

    Some(
        Detection::from_global_span(
            r#"'default ""' turns a missing value into an indistinguishable empty string"#,
            expr.span,
        )
        .with_primary_label("masks a missing value as an empty string"),
    )
}

struct DefaultEmptyStringMasksMissing;

impl DetectFix for DefaultEmptyStringMasksMissing {
    type FixInput<'a> = ();

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

    fn short_description(&self) -> &'static str {
        r#"Avoid '| default ""'; it masks a missing value as an empty string"#
    }

    fn long_description(&self) -> Option<&'static str> {
        Some(
            "'default' (without '--empty') only replaces 'null' values, so 'default \"\"' \
             silently turns a missing value into an indistinguishable empty string. Handle the \
             null case directly instead, e.g. 'match $x { null => ... _ => ... }' or 'if $x == \
             null { error make {...} }'.",
        )
    }

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

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

    fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
        Self::no_fix(context.detect(|expr, ctx| check_call(expr, ctx).into_iter().collect()))
    }
}

pub static RULE: &dyn Rule = &DefaultEmptyStringMasksMissing;

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