use std::ops::ControlFlow;
use nu_protocol::{
Span,
ast::{Expr, Expression},
};
use crate::{
LintLevel,
ast::string::{StringFormat, bare_word_needs_quotes},
context::LintContext,
rule::{DetectFix, Rule},
violation::{Detection, Fix, Replacement},
};
struct FixData {
quoted_span: Span,
unquoted_content: String,
}
fn is_in_command_position(expr: &Expression, parent: Option<&Expression>) -> bool {
parent.is_none_or(|parent| match &parent.expr {
Expr::ExternalCall(head, _args) => head.span == expr.span,
Expr::Block(_) | Expr::Closure(_) | Expr::Subexpression(_) | Expr::MatchBlock(_) => true,
Expr::BinaryOp(lhs, _, _) => lhs.span == expr.span,
_ => false,
})
}
struct UnnecessaryStringQuotes;
impl DetectFix for UnnecessaryStringQuotes {
type FixInput<'a> = FixData;
fn id(&self) -> &'static str {
"string_may_be_bare"
}
fn short_description(&self) -> &'static str {
"Quoted string can be bare word"
}
fn long_description(&self) -> Option<&'static str> {
Some(
r#"If you use a bare word plainly on the command line (that
is, not inside a data structure or used as a command parameter) or inside
round brackets ( ), it will be interpreted as an external command."#,
)
}
fn source_link(&self) -> Option<&'static str> {
Some("https://www.nushell.sh/book/working_with_strings.html#bare-word-strings")
}
fn level(&self) -> LintLevel {
LintLevel::Hint
}
fn detect<'a>(&self, context: &'a LintContext) -> Vec<(Detection, Self::FixInput<'a>)> {
let mut results = Vec::new();
context.traverse_with_parent(|expr, parent| {
let Expr::String(_) = &expr.expr else {
return ControlFlow::Continue(());
};
if is_in_command_position(expr, parent) {
log::trace!(
"Skipping {} - in command position",
context.span_text(expr.span)
);
return ControlFlow::Continue(());
}
let Some(string_format) = StringFormat::from_expression(expr, context) else {
return ControlFlow::Continue(());
};
let (unquoted_content, quote_type) = match &string_format {
StringFormat::Double(s) => (s, "double"),
StringFormat::Single(s) => (s, "single"),
StringFormat::BareWord(_)
| StringFormat::InterpolationDouble(_)
| StringFormat::InterpolationSingle(_)
| StringFormat::Backtick(_)
| StringFormat::Raw(_) => return ControlFlow::Continue(()),
};
if bare_word_needs_quotes(unquoted_content) {
log::trace!("String '{unquoted_content}' needs quotes");
return ControlFlow::Continue(());
}
log::trace!("String '{unquoted_content}' can be a bare word");
let violation = Detection::from_global_span(
format!("Unnecessary {quote_type} quotes around '{unquoted_content}'"),
expr.span,
)
.with_primary_label("can be a bare word");
results.push((
violation,
FixData {
quoted_span: expr.span,
unquoted_content: unquoted_content.clone(),
},
));
ControlFlow::Continue(())
});
results
}
fn fix(&self, _context: &LintContext, fix_data: &Self::FixInput<'_>) -> Option<Fix> {
Some(Fix {
explanation: format!("Remove quotes from '{}'", fix_data.unquoted_content).into(),
replacements: vec![Replacement::new(
fix_data.quoted_span,
fix_data.unquoted_content.clone(),
)],
})
}
}
pub static RULE: &dyn Rule = &UnnecessaryStringQuotes;
#[cfg(test)]
mod detect_bad;
#[cfg(test)]
mod generated_fix;
#[cfg(test)]
mod ignore_good;