nu-lint 1.2.0

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

use crate::{
    LintLevel,
    ast::call::CallExt,
    context::LintContext,
    effect::{
        CommonEffect,
        builtin::can_error,
        external::{ExternEffect, has_external_side_effect},
    },
    rule::{DetectFix, Rule},
    violation::Detection,
};

enum ErrorSource {
    External(Span),
    Builtin(Span, String),
}

fn find_error_prone_command(expr: &Expression, context: &LintContext) -> Option<ErrorSource> {
    expr.find_map(context.working_set, &|inner_expr| match &inner_expr.expr {
        Expr::ExternalCall(head, args) => {
            let cmd_name = context.expr_text(head);
            if has_external_side_effect(
                cmd_name,
                ExternEffect::CommonEffect(CommonEffect::FailsInNormalCircumstances),
                context,
                args,
            ) {
                return FindMapResult::Found(ErrorSource::External(head.span));
            }
            FindMapResult::Continue
        }
        Expr::Call(call) => {
            let cmd_name = call.get_call_name(context);
            if can_error(&cmd_name, context, call) {
                log::trace!("Found error-prone builtin: {cmd_name}");
                return FindMapResult::Found(ErrorSource::Builtin(call.head, cmd_name));
            }
            FindMapResult::Continue
        }
        _ => FindMapResult::Continue,
    })
}

struct TryInsteadOfDo;

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

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

    fn short_description(&self) -> &'static str {
        "Use 'try' blocks instead of 'do' blocks for error-prone operations"
    }

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

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

    fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
        let violations = context.detect(|expr, ctx| {
            let Expr::Call(call) = &expr.expr else {
                return vec![];
            };

            if call.get_call_name(ctx) != "do" {
                return vec![];
            }

            let Some(block_arg) = call.get_positional_arg(0) else {
                return vec![];
            };

            let Some(error_source) = find_error_prone_command(block_arg, ctx) else {
                return vec![];
            };

            let do_span = Span::new(expr.span.start, expr.span.start + 2);
            let (error_span, error_label) = match &error_source {
                ErrorSource::External(span) => (*span, "external command can fail".to_string()),
                ErrorSource::Builtin(span, name) => (*span, format!("`{name}` can error")),
            };

            vec![
                Detection::from_global_span(
                    "Use 'try' instead of 'do' for error-prone operations",
                    do_span,
                )
                .with_primary_label("do keyword")
                .with_extra_label(error_label, error_span),
            ]
        });
        Self::no_fix(violations)
    }
}

pub static RULE: &dyn Rule = &TryInsteadOfDo;

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