nu-lint 1.3.0

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

use super::{
    extract_delimiter_from_split_call, extract_index_from_call, generate_parse_replacement,
    is_indexed_access_call, is_split_row_call,
};
use crate::{
    Fix, LintLevel, Replacement,
    ast::{block::BlockExt, pipeline::PipelineExt},
    context::LintContext,
    rule::{DetectFix, Rule},
    violation::Detection,
};

pub enum FixData {
    WithDelimiter {
        span: Span,
        delimiter: String,
        index: usize,
    },
    NoFix,
}

fn check_pipeline(pipeline: &Pipeline, context: &LintContext) -> Vec<(Detection, FixData)> {
    pipeline
        .find_command_pairs(context, is_split_row_call, is_indexed_access_call)
        .into_iter()
        .filter_map(|pair| {
            let index = extract_index_from_call(pair.second, context)?;
            let delimiter = extract_delimiter_from_split_call(pair.first, context);

            let violation = Detection::from_global_span(
                "Extract field by name with 'parse' instead of 'split row | get INDEX'",
                pair.span,
            )
            .with_primary_label("index-based access")
            .with_extra_label("splits into list", pair.first.span())
            .with_extra_label("accesses by numeric index", pair.second.span());

            let fix_data = delimiter.map_or(FixData::NoFix, |delim| FixData::WithDelimiter {
                span: pair.span,
                delimiter: delim,
                index,
            });

            Some((violation, fix_data))
        })
        .collect()
}

struct SplitGetRule;

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

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

    fn short_description(&self) -> &'static str {
        "Extract field by name with 'parse' pattern"
    }

    fn long_description(&self) -> Option<&'static str> {
        Some(
            "Chaining 'split row' with indexed 'get' requires counting field positions manually \
             and doesn't show what each field represents. Use 'parse' to create records with \
             named fields that you can access by name instead of index.",
        )
    }

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

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

    fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
        context.ast.detect_in_pipelines(context, check_pipeline)
    }

    fn fix(&self, _context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
        match fix_data {
            FixData::WithDelimiter {
                span,
                delimiter,
                index,
            } => {
                let replacement = generate_parse_replacement(delimiter, &[*index]);
                Some(Fix {
                    explanation: "replace".into(),
                    replacements: vec![Replacement::new(*span, replacement)],
                })
            }
            FixData::NoFix => None,
        }
    }
}

pub static RULE: &dyn Rule = &SplitGetRule;

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