nu-lint 1.3.0

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

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

fn check_positional_param(
    param: &PositionalArg,
    param_type: &str,
    context: &LintContext,
) -> Option<Detection> {
    let var_id = param.var_id?;
    let var = context.working_set.get_variable(var_id);
    let param_span = var.declaration_span;

    if param_span.has_inline_doc_comment(context) {
        return None;
    }

    Some(
        Detection::from_global_span(
            format!(
                "{} parameter '{}' in main function is missing documentation comment",
                param_type, param.name
            ),
            param_span,
        )
        .with_primary_label(format!(
            "undocumented {} parameter",
            param_type.to_lowercase()
        )),
    )
}

fn check_main_function(call: &Call, context: &LintContext) -> Vec<Detection> {
    let Some(def) = call.custom_command_def(context) else {
        return vec![];
    };

    if !def.is_main() {
        return vec![];
    }

    let block = context.working_set.get_block(def.body);
    let signature = &block.signature;

    let mut violations = Vec::new();

    for param in &signature.required_positional {
        if let Some(detection) = check_positional_param(param, "Positional", context) {
            violations.push(detection);
        }
    }

    for param in &signature.optional_positional {
        if let Some(detection) = check_positional_param(param, "Optional positional", context) {
            violations.push(detection);
        }
    }

    if let Some(rest_param) = &signature.rest_positional
        && let Some(detection) = check_positional_param(rest_param, "Rest positional", context)
    {
        violations.push(detection);
    }

    violations
}

struct MainPositionalArgsDocs;

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

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

    fn short_description(&self) -> &'static str {
        "Missing docs on main positional parameter"
    }

    fn long_description(&self) -> Option<&'static str> {
        Some("Add a documentation comment after the parameter: <param> # Description of <param>")
    }

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

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

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

pub static RULE: &dyn Rule = &MainPositionalArgsDocs;

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