use nu_protocol::{
Span,
ast::{Argument, Block, Expr, Pipeline},
};
use crate::{
Fix, LintLevel, Replacement,
ast::{block::BlockExt, call::CallExt, pipeline::PipelineExt},
context::LintContext,
rule::{DetectFix, Rule},
violation::Detection,
};
struct FixData {
span: Span,
parse_pattern: String,
uses_regex: bool,
}
fn extract_parse_from_closure(
closure_block: &Block,
closure_param_id: nu_protocol::VarId,
ctx: &LintContext,
) -> Option<(String, bool)> {
if closure_block.pipelines.len() != 1 {
log::trace!(
"Closure has {} pipelines, expected 1",
closure_block.pipelines.len()
);
return None;
}
let pipeline = &closure_block.pipelines[0];
if pipeline.elements.len() != 2 {
log::trace!(
"Closure pipeline has {} elements, expected 2",
pipeline.elements.len()
);
return None;
}
let first = &pipeline.elements[0];
let var_id = match &first.expr.expr {
Expr::Var(id) => *id,
Expr::FullCellPath(fcp) if fcp.tail.is_empty() => {
if let Expr::Var(id) = &fcp.head.expr {
*id
} else {
return None;
}
}
_ => return None,
};
if var_id != closure_param_id {
return None;
}
let second = &pipeline.elements[1];
let Expr::Call(parse_call) = &second.expr.expr else {
log::trace!("Second element is not a Call");
return None;
};
if parse_call.get_call_name(ctx) != "parse" {
log::trace!("Second element is not parse command");
return None;
}
let pattern_arg = parse_call.get_first_positional_arg()?;
let pattern = match &pattern_arg.expr {
Expr::String(s) | Expr::RawString(s) => s.clone(),
_ => ctx.expr_text(pattern_arg).to_string(),
};
let uses_regex = parse_call
.arguments
.iter()
.any(|arg| matches!(arg, Argument::Named((name, _, _)) if name.item == "regex"));
Some((pattern, uses_regex))
}
fn check_pipeline(pipeline: &Pipeline, context: &LintContext) -> Vec<(Detection, FixData)> {
pipeline
.find_command_pairs(
context,
|call, ctx| call.get_call_name(ctx) == "lines",
|call, ctx| call.get_call_name(ctx) == "each",
)
.into_iter()
.filter_map(|pair| {
let closure_arg = pair.second.arguments.iter().find_map(|arg| {
if let Argument::Positional(expr) = arg
&& let Expr::Closure(block_id) = &expr.expr
{
return Some(*block_id);
}
None
})?;
let closure_block = context.working_set.get_block(closure_arg);
let signature = &closure_block.signature;
let param = signature.required_positional.first()?;
let param_id = param.var_id?;
let (parse_pattern, uses_regex) =
extract_parse_from_closure(closure_block, param_id, context)?;
let violation = Detection::from_global_span(
"Remove redundant 'each' wrapper around 'parse'",
pair.span,
)
.with_primary_label("can be simplified")
.with_extra_label("lines command", pair.first.span())
.with_extra_label(
"each closure just calls parse",
Span::new(pair.second.head.start, pair.second.span().end),
);
Some((
violation,
FixData {
span: pair.span,
parse_pattern,
uses_regex,
},
))
})
.collect()
}
struct LinesEachParseRule;
impl DetectFix for LinesEachParseRule {
type FixInput<'a> = FixData;
fn id(&self) -> &'static str {
"lines_each_to_parse"
}
fn short_description(&self) -> &'static str {
"Remove redundant 'each' wrapper around 'parse'"
}
fn long_description(&self) -> Option<&'static str> {
Some(
"The 'parse' command already operates on each line when used after 'lines'. Wrapping \
it in 'each { |l| $l | parse ... }' is redundant. Use 'lines | parse ...' directly.",
)
}
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> {
let replacement = if fix_data.uses_regex {
format!("lines | parse --regex \"{}\"", fix_data.parse_pattern)
} else {
format!("lines | parse \"{}\"", fix_data.parse_pattern)
};
Some(Fix {
explanation: "simplify".into(),
replacements: vec![Replacement::new(fix_data.span, replacement)],
})
}
}
pub static RULE: &dyn Rule = &LinesEachParseRule;
#[cfg(test)]
mod detect_bad;
#[cfg(test)]
mod generated_fix;
#[cfg(test)]
mod ignore_good;