use nu_protocol::ast::{
Assignment, Bits, Boolean, Comparison, Expr, Expression, FindMapResult, Math, Operator,
Traverse,
};
use crate::{
LintLevel,
ast::expression::ExpressionExt,
context::LintContext,
rule::{DetectFix, Rule},
violation::Detection,
};
#[derive(Debug, PartialEq)]
enum ProblematicPattern {
StandaloneOperator(String),
ExternalBooleanOperator(String),
LiteralBinaryOp(String),
}
fn is_operator_keyword(s: &str) -> bool {
use Boolean as B;
use Comparison as C;
use Math as M;
[
B::And.as_str(),
B::Or.as_str(),
B::Xor.as_str(),
M::Add.as_str(),
M::Subtract.as_str(),
M::Multiply.as_str(),
M::Divide.as_str(),
M::FloorDivide.as_str(),
M::Modulo.as_str(),
M::Pow.as_str(),
M::Concatenate.as_str(),
C::Equal.as_str(),
C::NotEqual.as_str(),
C::LessThan.as_str(),
C::GreaterThan.as_str(),
C::LessThanOrEqual.as_str(),
C::GreaterThanOrEqual.as_str(),
C::RegexMatch.as_str(),
C::NotRegexMatch.as_str(),
C::In.as_str(),
C::NotIn.as_str(),
C::Has.as_str(),
C::NotHas.as_str(),
C::StartsWith.as_str(),
C::NotStartsWith.as_str(),
C::EndsWith.as_str(),
C::NotEndsWith.as_str(),
Bits::BitOr.as_str(),
Bits::BitXor.as_str(),
Bits::BitAnd.as_str(),
Bits::ShiftLeft.as_str(),
Bits::ShiftRight.as_str(),
Assignment::Assign.as_str(),
Assignment::AddAssign.as_str(),
Assignment::SubtractAssign.as_str(),
Assignment::MultiplyAssign.as_str(),
Assignment::DivideAssign.as_str(),
Assignment::ConcatenateAssign.as_str(),
]
.contains(&s)
}
fn analyze_ast_expression(expr: &Expression, context: &LintContext) -> Option<ProblematicPattern> {
expr.find_map(context.working_set, &|sub_expr| {
match &sub_expr.expr {
Expr::BinaryOp(left, op, right) => handle_binary_op(left, op, right, context),
Expr::Operator(op) => {
FindMapResult::Found(ProblematicPattern::StandaloneOperator(format!("{op}")))
}
Expr::UnaryNot(inner) => {
if inner.as_ref().contains_variables(context) {
FindMapResult::Continue
} else {
FindMapResult::Found(ProblematicPattern::LiteralBinaryOp("not".to_string()))
}
}
Expr::ExternalCall(head, _args) => analyze_external_call(head, context)
.map_or(FindMapResult::Continue, FindMapResult::Found),
_ => FindMapResult::Continue,
}
})
}
fn handle_binary_op(
left: &Expression,
op: &Expression,
right: &Expression,
context: &LintContext,
) -> FindMapResult<ProblematicPattern> {
if let Some(p) = analyze_binary_operation(left, op, right, context) {
return FindMapResult::Found(p);
}
if let Some(left_problem) = analyze_ast_expression(left, context) {
return FindMapResult::Found(left_problem);
}
if let Some(right_problem) = analyze_ast_expression(right, context) {
return FindMapResult::Found(right_problem);
}
FindMapResult::Stop
}
fn analyze_binary_operation(
left: &Expression,
op: &Expression,
right: &Expression,
context: &LintContext,
) -> Option<ProblematicPattern> {
let Expr::Operator(operator) = &op.expr else {
return None;
};
match operator {
Operator::Boolean(Boolean::And | Boolean::Or)
if !left.contains_variables(context) && !right.contains_variables(context) =>
{
Some(ProblematicPattern::LiteralBinaryOp(format!("{operator}")))
}
_ => None,
}
}
fn analyze_external_call(head: &Expression, context: &LintContext) -> Option<ProblematicPattern> {
let (Expr::GlobPattern(pattern, _) | Expr::String(pattern)) = &head.expr else {
return None;
};
if !is_operator_keyword(pattern.as_str()) {
return None;
}
context
.working_set
.find_decl(pattern.as_bytes())
.is_none()
.then(|| ProblematicPattern::ExternalBooleanOperator(pattern.clone()))
}
fn is_valid_interpolation(expr: &Expression, context: &LintContext) -> bool {
match &expr.expr {
Expr::Subexpression(_) => analyze_ast_expression(expr, context).is_none(),
Expr::FullCellPath(cell_path) => match &cell_path.head.expr {
Expr::Subexpression(_) => analyze_ast_expression(&cell_path.head, context).is_none(),
_ => true, },
_ => true,
}
}
fn create_violation(span: nu_protocol::Span, pattern: ProblematicPattern) -> Detection {
let (message, label) = match pattern {
ProblematicPattern::StandaloneOperator(op) => (
format!(
"String interpolation contains standalone operator '{op}' which will cause \
runtime error"
),
format!("standalone '{op}' operator"),
),
ProblematicPattern::ExternalBooleanOperator(op) => (
format!(
"String interpolation attempts to call operator '{op}' as external command, which \
will cause runtime error"
),
format!("'{op}' parsed as external command"),
),
ProblematicPattern::LiteralBinaryOp(op) => (
format!(
"String interpolation contains '{op}' operation on literal values, likely \
intended as text"
),
format!("literal '{op}' operation"),
),
};
Detection::from_global_span(message, span).with_primary_label(label)
}
fn check_string_interpolation(
exprs: &[Expression],
span: nu_protocol::Span,
context: &LintContext,
) -> Option<Detection> {
exprs
.iter()
.filter(|expr| !matches!(expr.expr, Expr::String(_)))
.find_map(|expr| {
if is_valid_interpolation(expr, context) {
None
} else {
analyze_ast_expression(expr, context).map(|pattern| create_violation(span, pattern))
}
})
}
struct EscapeStringInterpolationOperators;
impl DetectFix for EscapeStringInterpolationOperators {
type FixInput<'a> = ();
fn id(&self) -> &'static str {
"unescaped_interpolation"
}
fn short_description(&self) -> &'static str {
"Unescaped braces in string interpolation"
}
fn source_link(&self) -> Option<&'static str> {
Some("https://www.nushell.sh/book/working_with_strings.html#string-interpolation")
}
fn level(&self) -> LintLevel {
LintLevel::Error
}
fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
Self::no_fix(context.detect(|expr, ctx| {
if let Expr::StringInterpolation(exprs) = &expr.expr
&& let Some(violation) = check_string_interpolation(exprs, expr.span, ctx)
{
vec![violation]
} else {
vec![]
}
}))
}
}
pub static RULE: &dyn Rule = &EscapeStringInterpolationOperators;
#[cfg(test)]
mod detect_bad;
#[cfg(test)]
mod ignore_good;