nu-lint 1.3.0

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

use crate::{
    ast::{
        call::CallExt,
        regex::{contains_regex_special_chars, escape_regex},
        string::StringFormat,
    },
    context::LintContext,
};

pub mod lines_each_to_parse;
pub mod lines_instead_of_split;
pub mod simplify_regex_parse;
pub mod split_first_to_parse;
pub mod split_row_get_inline;
pub mod split_row_get_multistatement;
pub mod split_row_space_to_split_words;

pub fn is_split_row_call(call: &Call, context: &LintContext) -> bool {
    call.get_call_name(context) == "split row"
}

pub fn is_split_call(call: &Call, context: &LintContext) -> bool {
    matches!(call.get_call_name(context).as_str(), "split row" | "split")
}

pub fn is_indexed_access_call(call: &Call, context: &LintContext) -> bool {
    matches!(call.get_call_name(context).as_str(), "get" | "skip")
}

pub fn extract_index_from_call(call: &Call, context: &LintContext) -> Option<usize> {
    call.get_first_positional_arg()
        .and_then(|arg| context.expr_text(arg).parse().ok())
}

pub fn extract_delimiter_from_split_call(call: &Call, context: &LintContext) -> Option<String> {
    if !is_split_call(call, context) {
        return None;
    }
    let arg = call.get_first_positional_arg()?;
    match &arg.expr {
        Expr::String(s) | Expr::RawString(s) => Some(s.clone()),
        _ => StringFormat::from_expression(arg, context).map(|fmt| fmt.content().to_string()),
    }
}

pub fn needs_regex_for_delimiter(delimiter: &str) -> bool {
    contains_regex_special_chars(delimiter)
}

pub fn generate_parse_pattern(delimiter: &str, num_fields: usize) -> (String, bool) {
    let needs_regex = needs_regex_for_delimiter(delimiter);

    if needs_regex {
        let escaped = escape_regex(delimiter);
        let pattern = (0..num_fields)
            .map(|i| format!("(?P<field{i}>.*)"))
            .collect::<Vec<_>>()
            .join(&escaped);
        (pattern, true)
    } else {
        let pattern = (0..num_fields)
            .map(|i| format!("{{field{i}}}"))
            .collect::<Vec<_>>()
            .join(delimiter);
        (pattern, false)
    }
}

pub fn generate_parse_replacement(delimiter: &str, indexed_fields: &[usize]) -> String {
    let max_field = indexed_fields.iter().copied().max().unwrap_or(0);
    let num_fields = max_field + 2;
    let (pattern, needs_regex) = generate_parse_pattern(delimiter, num_fields);

    if needs_regex {
        format!("parse --regex '{pattern}'")
    } else {
        format!("parse \"{pattern}\"")
    }
}