nu-lint 1.2.0

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

use crate::{
    Fix, LintLevel, Replacement,
    ast::{call::CallExt, declaration::CustomCommandDef},
    context::LintContext,
    rule::{DetectFix, Rule},
    violation::Detection,
};

fn has_shebang(ctx: &LintContext) -> bool {
    let source = unsafe { ctx.source() };
    source.starts_with("#!")
}

fn check_export_main_in_script(
    call: &Call,
    context: &LintContext,
) -> Option<(Detection, CustomCommandDef)> {
    // Only trigger for files with a shebang (scripts, not modules)
    if !has_shebang(context) {
        return None;
    }

    let func_def = call.custom_command_def(context)?;

    if !func_def.is_exported() {
        return None;
    }

    if !func_def.is_main() {
        return None;
    }

    let export_span = func_def.export_span?;

    let violation = Detection::from_global_span(
        format!(
            "Unnecessary 'export' in script entry point '{}'",
            func_def.name
        ),
        export_span,
    )
    .with_primary_label("remove 'export' keyword")
    .with_extra_label("script entry point", func_def.name_span);

    Some((violation, func_def))
}

struct ScriptExportMain;

impl DetectFix for ScriptExportMain {
    type FixInput<'a> = CustomCommandDef;

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

    fn short_description(&self) -> &'static str {
        "In scripts, 'def main' is the entry point and doesn't need 'export'"
    }

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

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

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

    fn fix(&self, _context: &LintContext, func_def: &Self::FixInput<'_>) -> Option<Fix> {
        let export_span = func_def.export_span?;
        Some(Fix {
            explanation: format!("Remove 'export' keyword from '{}'", func_def.name).into(),
            replacements: vec![Replacement::new(export_span, "")],
        })
    }
}

pub static RULE: &dyn Rule = &ScriptExportMain;

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