use nu_protocol::{
Span, VarId,
ast::{Block, Call, Comparison, Expr, Expression, Operator},
};
use crate::{
LintLevel,
ast::{call::CallExt, expression::ExpressionExt},
context::LintContext,
rule::{DetectFix, Rule},
violation::{Detection, Fix, Replacement},
};
struct FixData {
full_span: Span,
variable: Span,
fallback: Span,
}
struct NullComparison {
var_id: VarId,
var_span: Span,
is_equal: bool,
}
fn get_single_block_expr(block: &Block) -> Option<&Expression> {
let [pipeline] = block.pipelines.as_slice() else {
return None;
};
let [element] = pipeline.elements.as_slice() else {
return None;
};
Some(&element.expr)
}
fn extract_null_comparison(expr: &Expression) -> Option<NullComparison> {
let Expr::BinaryOp(left, op, right) = &expr.expr else {
return None;
};
let is_equal = match &op.expr {
Expr::Operator(Operator::Comparison(Comparison::Equal)) => true,
Expr::Operator(Operator::Comparison(Comparison::NotEqual)) => false,
_ => return None,
};
if matches!(&right.expr, Expr::Nothing)
&& let Some(var_id) = left.extract_direct_var()
{
return Some(NullComparison {
var_id,
var_span: left.span,
is_equal,
});
}
if matches!(&left.expr, Expr::Nothing)
&& let Some(var_id) = right.extract_direct_var()
{
return Some(NullComparison {
var_id,
var_span: right.span,
is_equal,
});
}
None
}
fn detect(call: &Call, expr_span: Span, context: &LintContext) -> Option<(Detection, FixData)> {
if call.get_call_name(context) != "if" {
return None;
}
let condition = call.get_first_positional_arg()?;
let cmp = extract_null_comparison(condition)?;
let then_block_id = call.get_positional_arg(1)?.extract_block_id()?;
let then_block = context.working_set.get_block(then_block_id);
let (is_else_if, else_expr) = call.get_else_branch()?;
if is_else_if {
return None;
}
let else_block_id = else_expr.extract_block_id()?;
let else_block = context.working_set.get_block(else_block_id);
let (var_block, default_block) = if cmp.is_equal {
(else_block, then_block)
} else {
(then_block, else_block)
};
if !get_single_block_expr(var_block)?.matches_var(cmp.var_id) {
return None;
}
let default_span = get_single_block_expr(default_block)?.span;
let detection = Detection::from_global_span(
"This if-null pattern can be simplified with the `default` command",
expr_span,
)
.with_primary_label("simplify with `| default`");
Some((
detection,
FixData {
full_span: expr_span,
variable: cmp.var_span,
fallback: default_span,
},
))
}
struct IfNullToDefault;
impl DetectFix for IfNullToDefault {
type FixInput<'a> = FixData;
fn id(&self) -> &'static str {
"if_null_to_default"
}
fn short_description(&self) -> &'static str {
"Simplify if-null pattern to `| default`"
}
fn source_link(&self) -> Option<&'static str> {
Some("https://www.nushell.sh/commands/docs/default.html")
}
fn level(&self) -> LintLevel {
LintLevel::Warning
}
fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
context.detect_with_fix_data(|expr, ctx| {
if let Expr::Call(call) = &expr.expr {
detect(call, expr.span, ctx).into_iter().collect()
} else {
vec![]
}
})
}
fn fix(&self, context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
let var_text = context.span_text(fix_data.variable);
let default_text = context.span_text(fix_data.fallback);
let replacement = format!("{var_text} | default {default_text}");
Some(Fix {
explanation: "simplify".into(),
replacements: vec![Replacement::new(fix_data.full_span, replacement)],
})
}
}
pub static RULE: &dyn Rule = &IfNullToDefault;
#[cfg(test)]
mod detect_bad;
#[cfg(test)]
mod generated_fix;
#[cfg(test)]
mod ignore_good;