nu-lint 1.3.0

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

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

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 flag in &signature.named {
        let Some(var_id) = flag.var_id else {
            continue;
        };

        let var = context.working_set.get_variable(var_id);
        let flag_span = var.declaration_span;

        if !flag_span.has_inline_doc_comment(context) {
            let flag_name = flag.short.map_or_else(
                || format!("--{}", flag.long),
                |short| format!("--{} (-{})", flag.long, short),
            );

            violations.push(
                Detection::from_global_span(
                    format!(
                        "Named parameter '{flag_name}' in main function is missing documentation \
                         comment"
                    ),
                    flag_span,
                )
                .with_primary_label("undocumented flag"),
            );
        }
    }

    violations
}

struct MainNamedArgsDocs;

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

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

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

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

    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 = &MainNamedArgsDocs;

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