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, Fix, Replacement},
};

struct DynamicAccessFixData {
    insert_span: nu_protocol::Span,
}

const fn is_dynamic_key(expr: &Expression) -> bool {
    matches!(
        &expr.expr,
        Expr::Var(_)
            | Expr::FullCellPath(_)
            | Expr::StringInterpolation(..)
            | Expr::Subexpression(_)
    )
}

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

    if call.get_call_name(ctx) != "get" {
        return None;
    }

    if call.has_named_flag("optional") || call.has_named_flag("o") {
        return None;
    }

    let key_arg = call.get_first_positional_arg()?;

    if !is_dynamic_key(key_arg) {
        return None;
    }

    let get_keyword_end = call.head.end;
    let insert_span = nu_protocol::Span::new(get_keyword_end, get_keyword_end);

    let violation = Detection::from_global_span(
        "Dynamic record access without -o flag may silently fail",
        call.head,
    )
    .with_primary_label("add -o flag for safe access")
    .with_extra_label("dynamic key", key_arg.span);

    let fix_data = DynamicAccessFixData { insert_span };

    Some((violation, fix_data))
}

struct UnsafeDynamicRecordAccess;

impl DetectFix for UnsafeDynamicRecordAccess {
    type FixInput<'a> = DynamicAccessFixData;

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

    fn short_description(&self) -> &'static str {
        "Use 'get -o' for dynamic keys to handle missing keys safely"
    }

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

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

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

    fn fix(&self, _context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
        Some(Fix {
            explanation: "Add -o flag for safe optional access".into(),
            replacements: vec![Replacement::new(fix_data.insert_span, " -o")],
        })
    }
}

pub static RULE: &dyn Rule = &UnsafeDynamicRecordAccess;

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