nu-lint 1.3.0

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

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

struct FixData {
    interpolation_span: nu_protocol::Span,
    variable_text: String,
}

fn is_single_variable_interpolation(exprs: &[Expression], context: &LintContext) -> Option<String> {
    if exprs.len() != 1 {
        return None;
    }

    let expr = &exprs[0];

    let inner_expr = match &expr.expr {
        Expr::FullCellPath(cell_path) => &cell_path.head,
        _ => expr,
    };

    match &inner_expr.expr {
        Expr::Subexpression(block_id) => {
            let block = context.working_set.get_block(*block_id);

            if block.pipelines.len() != 1 || block.pipelines[0].elements.len() != 1 {
                return None;
            }

            let pipeline_expr = &block.pipelines[0].elements[0].expr;

            match &pipeline_expr.expr {
                Expr::Var(_) | Expr::VarDecl(_) | Expr::FullCellPath(_) => {
                    let var_text = context.expr_text(pipeline_expr);
                    Some(var_text.to_string())
                }
                _ => None,
            }
        }
        _ => None,
    }
}

fn check_interpolation(expr: &Expression, context: &LintContext) -> Option<(Detection, FixData)> {
    let Expr::StringInterpolation(exprs) = &expr.expr else {
        return None;
    };

    let variable_text = is_single_variable_interpolation(exprs, context)?;

    let violation = Detection::from_global_span(
        format!("Unnecessary string interpolation around variable '{variable_text}'"),
        expr.span,
    )
    .with_primary_label("unnecessary interpolation");

    let fix_data = FixData {
        interpolation_span: expr.span,
        variable_text,
    };

    Some((violation, fix_data))
}

struct NeverSpaceSplit;

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

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

    fn short_description(&self) -> &'static str {
        "Unnecessary quotes around variable"
    }

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

    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| check_interpolation(expr, ctx).into_iter().collect())
    }

    fn fix(&self, _context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
        Some(Fix {
            explanation: format!(
                "Remove unnecessary interpolation around '{}'",
                fix_data.variable_text
            )
            .into(),
            replacements: vec![Replacement::new(
                fix_data.interpolation_span,
                fix_data.variable_text.clone(),
            )],
        })
    }
}

pub static RULE: &dyn Rule = &NeverSpaceSplit;

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