nu-lint 1.3.0

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

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

struct CommaFixData {
    comma_span: Span,
}

fn find_comma_outside_comment(between_text: &str) -> Option<usize> {
    let mut offset = 0;
    for line in between_text.split('\n') {
        let comment_start = line.find('#').unwrap_or(line.len());
        if let Some(comma_pos) = line[..comment_start].find(',') {
            return Some(offset + comma_pos);
        }
        offset += line.len() + 1;
    }
    None
}

fn check_list_commas(
    context: &LintContext,
    span: Span,
    items: &[ListItem],
) -> Vec<(Detection, CommaFixData)> {
    let mut violations = Vec::new();
    if items.len() < 2 {
        return violations;
    }
    let list_text = context.span_text(span);
    if !list_text.trim_start().starts_with('[') {
        return violations;
    }
    for i in 0..items.len() - 1 {
        let current_expr = match &items[i] {
            ListItem::Item(expr) | ListItem::Spread(_, expr) => expr,
        };
        let next_expr = match &items[i + 1] {
            ListItem::Item(expr) | ListItem::Spread(_, expr) => expr,
        };
        if current_expr.span.end >= next_expr.span.start {
            continue;
        }
        let between_span = Span::new(current_expr.span.end, next_expr.span.start);
        let between_text = context.span_text(between_span);
        if let Some(comma_pos) = find_comma_outside_comment(between_text) {
            let comma_span = Span::new(
                between_span.start + comma_pos,
                between_span.start + comma_pos + 1,
            );
            violations.push((
                Detection::from_global_span("Omit commas between list items", comma_span),
                CommaFixData { comma_span },
            ));
        }
    }
    violations
}

struct OmitListCommas;

impl DetectFix for OmitListCommas {
    type FixInput<'a> = CommaFixData;

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

    fn short_description(&self) -> &'static str {
        "Omit commas between list items."
    }

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

    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| match &expr.expr {
            Expr::List(items) => check_list_commas(ctx, expr.span, items),
            _ => vec![],
        })
    }

    fn fix(&self, _context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
        Some(Fix {
            explanation: "Remove comma".into(),
            replacements: vec![Replacement::new(fix_data.comma_span, String::new())],
        })
    }
}

pub static RULE: &dyn Rule = &OmitListCommas;
#[cfg(test)]
mod detect_bad;
#[cfg(test)]
mod generated_fix;
#[cfg(test)]
mod ignore_good;