nu-lint 1.3.0

Linter for Nu shell scripts that helpfully suggests improvements
Documentation
use std::collections::BTreeSet;

use nu_protocol::Span;

use crate::{
    LintLevel,
    context::LintContext,
    rule::{DetectFix, Rule},
    violation::{Detection, Fix, Replacement},
};

struct FixData {
    span: Span,
}

struct NothingOutsideFunctionSignature;

impl DetectFix for NothingOutsideFunctionSignature {
    type FixInput<'a> = FixData;

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

    fn short_description(&self) -> &'static str {
        "`nothing` type used outside signature"
    }

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

    fn long_description(&self) -> Option<&'static str> {
        Some(
            r#"'nothing' is a type annotation keyword for function signatures. It indicates that a function returns no value.

To represent the absence of a value in a function body, use 'null' instead."#,
        )
    }

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

    fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
        let signature_spans: Vec<Span> = context
            .custom_commands()
            .into_iter()
            .map(|cmd| cmd.signature_span)
            .collect();

        let results = context.detect_with_fix_data(|expr, context| {
            let expr_text = context.expr_text(expr).trim();

            if expr_text != "nothing" {
                return vec![];
            }

            let is_in_signature = signature_spans
                .iter()
                .any(|sig_span| sig_span.contains_span(expr.span));

            if is_in_signature {
                return vec![];
            }

            let detection = Detection::from_global_span(
                "The keyword 'nothing' should only be used in function signatures, not in \
                 expressions or function bodies",
                expr.span,
            )
            .with_primary_label("'nothing' used outside function signature");

            vec![(detection, FixData { span: expr.span })]
        });

        let mut seen = BTreeSet::new();
        results
            .into_iter()
            .filter(|(_, fix_data)| seen.insert((fix_data.span.start, fix_data.span.end)))
            .collect()
    }

    fn fix(&self, _context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
        Some(Fix {
            explanation: "Replace 'nothing' with 'null'".into(),
            replacements: vec![Replacement::new(fix_data.span, "null".to_string())],
        })
    }
}

pub static RULE: &dyn Rule = &NothingOutsideFunctionSignature;

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