nu-lint 1.3.0

Linter for Nu shell scripts that helpfully suggests improvements
Documentation
use nu_protocol::ast::{Block, Call, Expr, Pipeline, PipelineElement};

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

fn extract_print_message(call: &Call, context: &LintContext) -> Option<String> {
    call.get_first_positional_arg()
        .map(|expr| expr.span_text(context).to_string())
}

fn extract_exit_code(call: &Call) -> Option<i64> {
    call.get_first_positional_arg()
        .and_then(|code_expr| match &code_expr.expr {
            Expr::Int(code) => Some(*code),
            _ => None,
        })
}

struct ErrorToStdout {
    span: nu_protocol::Span,
}

fn check_print_exit_calls(
    print_call: &Call,
    exit_call: &Call,
    context: &LintContext,
) -> Option<ErrorToStdout> {
    (print_call.get_call_name(context) == "print" && !print_call.has_named_flag("stderr"))
        .then(|| exit_call.get_call_name(context) == "exit")
        .filter(|&is_exit| is_exit)
        .and_then(|_| {
            let print_message = extract_print_message(print_call, context)?;
            let exit_code = extract_exit_code(exit_call)?;
            (exit_code != 0).then_some(print_message)
        })
        .map(|_print_message| ErrorToStdout {
            span: print_call.span(),
        })
}

fn check_sequential_print_exit(
    first: &PipelineElement,
    second: &PipelineElement,
    context: &LintContext,
) -> Option<ErrorToStdout> {
    let Expr::Call(print_call) = &first.expr.expr else {
        return None;
    };
    let Expr::Call(exit_call) = &second.expr.expr else {
        return None;
    };
    check_print_exit_calls(print_call, exit_call, context)
}

fn check_same_pipeline_print_exit(
    pipeline: &Pipeline,
    context: &LintContext,
) -> Option<ErrorToStdout> {
    use crate::ast::pipeline::PipelineExt;

    pipeline
        .find_command_pairs(
            context,
            |call, ctx| call.get_call_name(ctx) == "print" && !call.has_named_flag("stderr"),
            |call, ctx| call.get_call_name(ctx) == "exit",
        )
        .into_iter()
        .find_map(|pair| check_print_exit_calls(pair.first, pair.second, context))
}

fn create_violation(pattern: &ErrorToStdout) -> Detection {
    Detection::from_global_span(
        "Error message printed to stdout instead of stderr",
        pattern.span,
    )
    .with_primary_label("prints error to stdout")
}

fn check_sequential_patterns<'a>(
    block: &'a Block,
    context: &'a LintContext,
) -> impl Iterator<Item = ErrorToStdout> + 'a {
    block.pipelines.windows(2).filter_map(move |pipelines| {
        let [first_pipeline, second_pipeline] = pipelines else {
            return None;
        };

        let [first_elem] = &first_pipeline.elements[..] else {
            return None;
        };
        let [second_elem] = &second_pipeline.elements[..] else {
            return None;
        };

        check_sequential_print_exit(first_elem, second_elem, context)
    })
}

fn check_same_pipeline_patterns<'a>(
    block: &'a Block,
    context: &'a LintContext,
) -> impl Iterator<Item = ErrorToStdout> + 'a {
    block
        .pipelines
        .iter()
        .filter_map(move |pipeline| check_same_pipeline_print_exit(pipeline, context))
}

fn check_block_patterns(block: &Block, context: &LintContext) -> Vec<Detection> {
    check_same_pipeline_patterns(block, context)
        .chain(check_sequential_patterns(block, context))
        .map(|pattern| create_violation(&pattern))
        .collect()
}

struct ErrorsToStderr;

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

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

    fn short_description(&self) -> &'static str {
        "Error messages should go to stderr, not stdout"
    }

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

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

    fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
        let main_violations = check_block_patterns(context.ast, context);

        let nested_violations: Vec<_> = context.detect(|expr, ctx| match &expr.expr {
            Expr::Closure(block_id) | Expr::Block(block_id) | Expr::Subexpression(block_id) => {
                let block = ctx.working_set.get_block(*block_id);
                check_block_patterns(block, ctx)
            }
            _ => vec![],
        });

        Self::no_fix(
            main_violations
                .into_iter()
                .chain(nested_violations)
                .collect(),
        )
    }
}

pub static RULE: &dyn Rule = &ErrorsToStderr;

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