extract each lint into its own module
This commit is contained in:
parent
6c9342782e
commit
0fcb000aed
5 changed files with 430 additions and 391 deletions
38
clippy_lints/src/write/empty_string.rs
Normal file
38
clippy_lints/src/write/empty_string.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use clippy_utils::diagnostics::span_lint_and_then;
|
||||
use clippy_utils::macros::MacroCall;
|
||||
use clippy_utils::source::expand_past_previous_comma;
|
||||
use clippy_utils::sym;
|
||||
use rustc_ast::{FormatArgs, FormatArgsPiece};
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_lint::LateContext;
|
||||
|
||||
use super::{PRINTLN_EMPTY_STRING, WRITELN_EMPTY_STRING};
|
||||
|
||||
pub(super) fn check(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
|
||||
if let [FormatArgsPiece::Literal(sym::LF)] = &format_args.template[..] {
|
||||
let mut span = format_args.span;
|
||||
|
||||
let lint = if name == "writeln" {
|
||||
span = expand_past_previous_comma(cx, span);
|
||||
|
||||
WRITELN_EMPTY_STRING
|
||||
} else {
|
||||
PRINTLN_EMPTY_STRING
|
||||
};
|
||||
|
||||
span_lint_and_then(
|
||||
cx,
|
||||
lint,
|
||||
macro_call.span,
|
||||
format!("empty string literal in `{name}!`"),
|
||||
|diag| {
|
||||
diag.span_suggestion(
|
||||
span,
|
||||
"remove the empty string",
|
||||
String::new(),
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
285
clippy_lints/src/write/literal.rs
Normal file
285
clippy_lints/src/write/literal.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
use clippy_utils::diagnostics::span_lint_and_then;
|
||||
use clippy_utils::macros::format_arg_removal_span;
|
||||
use clippy_utils::source::SpanRangeExt;
|
||||
use clippy_utils::sym;
|
||||
use rustc_ast::token::LitKind;
|
||||
use rustc_ast::{
|
||||
FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatCount, FormatOptions,
|
||||
FormatPlaceholder, FormatTrait,
|
||||
};
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_lint::LateContext;
|
||||
use rustc_span::Span;
|
||||
|
||||
use super::{PRINT_LITERAL, WRITE_LITERAL};
|
||||
|
||||
pub(super) fn check(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
|
||||
let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos);
|
||||
|
||||
let lint_name = if name.starts_with("write") {
|
||||
WRITE_LITERAL
|
||||
} else {
|
||||
PRINT_LITERAL
|
||||
};
|
||||
|
||||
let mut counts = vec![0u32; format_args.arguments.all_args().len()];
|
||||
for piece in &format_args.template {
|
||||
if let FormatArgsPiece::Placeholder(placeholder) = piece {
|
||||
counts[arg_index(&placeholder.argument)] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut suggestion: Vec<(Span, String)> = vec![];
|
||||
// holds index of replaced positional arguments; used to decrement the index of the remaining
|
||||
// positional arguments.
|
||||
let mut replaced_position: Vec<usize> = vec![];
|
||||
let mut sug_span: Option<Span> = None;
|
||||
|
||||
for piece in &format_args.template {
|
||||
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
argument,
|
||||
span: Some(placeholder_span),
|
||||
format_trait: FormatTrait::Display,
|
||||
format_options,
|
||||
}) = piece
|
||||
&& *format_options == FormatOptions::default()
|
||||
&& let index = arg_index(argument)
|
||||
&& counts[index] == 1
|
||||
&& let Some(arg) = format_args.arguments.by_index(index)
|
||||
&& let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind
|
||||
&& !arg.expr.span.from_expansion()
|
||||
&& let Some(value_string) = arg.expr.span.get_source_text(cx)
|
||||
{
|
||||
let (replacement, replace_raw) = match lit.kind {
|
||||
LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) {
|
||||
Some(extracted) => extracted,
|
||||
None => return,
|
||||
},
|
||||
LitKind::Char => (
|
||||
match lit.symbol {
|
||||
sym::DOUBLE_QUOTE => "\\\"",
|
||||
sym::BACKSLASH_SINGLE_QUOTE => "'",
|
||||
_ => match value_string.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
|
||||
Some(stripped) => stripped,
|
||||
None => return,
|
||||
},
|
||||
}
|
||||
.to_string(),
|
||||
false,
|
||||
),
|
||||
LitKind::Bool => (lit.symbol.to_string(), false),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let Some(format_string_snippet) = format_args.span.get_source_text(cx) else {
|
||||
continue;
|
||||
};
|
||||
let format_string_is_raw = format_string_snippet.starts_with('r');
|
||||
|
||||
let replacement = match (format_string_is_raw, replace_raw) {
|
||||
(false, false) => Some(replacement),
|
||||
(false, true) => Some(replacement.replace('\\', "\\\\").replace('"', "\\\"")),
|
||||
(true, false) => match conservative_unescape(&replacement) {
|
||||
Ok(unescaped) => Some(unescaped),
|
||||
Err(UnescapeErr::Lint) => None,
|
||||
Err(UnescapeErr::Ignore) => continue,
|
||||
},
|
||||
(true, true) => {
|
||||
if replacement.contains(['#', '"']) {
|
||||
None
|
||||
} else {
|
||||
Some(replacement)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
sug_span = Some(sug_span.unwrap_or(arg.expr.span).to(arg.expr.span));
|
||||
|
||||
if let Some((_, index)) = format_arg_piece_span(piece) {
|
||||
replaced_position.push(index);
|
||||
}
|
||||
|
||||
if let Some(replacement) = replacement
|
||||
// `format!("{}", "a")`, `format!("{named}", named = "b")
|
||||
// ~~~~~ ~~~~~~~~~~~~~
|
||||
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
|
||||
{
|
||||
let replacement = escape_braces(&replacement, !format_string_is_raw && !replace_raw);
|
||||
suggestion.push((*placeholder_span, replacement));
|
||||
suggestion.push((removal_span, String::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement the index of the remaining by the number of replaced positional arguments
|
||||
if !suggestion.is_empty() {
|
||||
for piece in &format_args.template {
|
||||
relocalize_format_args_indexes(piece, &mut suggestion, &replaced_position);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(span) = sug_span {
|
||||
span_lint_and_then(cx, lint_name, span, "literal with an empty format string", |diag| {
|
||||
if !suggestion.is_empty() {
|
||||
diag.multipart_suggestion("try", suggestion, Applicability::MachineApplicable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract Span and its index from the given `piece`
|
||||
fn format_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> {
|
||||
match piece {
|
||||
FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
argument: FormatArgPosition { index: Ok(index), .. },
|
||||
span: Some(span),
|
||||
..
|
||||
}) => Some((*span, *index)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Relocalizes the indexes of positional arguments in the format string
|
||||
fn relocalize_format_args_indexes(
|
||||
piece: &FormatArgsPiece,
|
||||
suggestion: &mut Vec<(Span, String)>,
|
||||
replaced_position: &[usize],
|
||||
) {
|
||||
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
argument:
|
||||
FormatArgPosition {
|
||||
index: Ok(index),
|
||||
// Only consider positional arguments
|
||||
kind: FormatArgPositionKind::Number,
|
||||
span: Some(span),
|
||||
},
|
||||
format_options,
|
||||
..
|
||||
}) = piece
|
||||
{
|
||||
if suggestion.iter().any(|(s, _)| s.overlaps(*span)) {
|
||||
// If the span is already in the suggestion, we don't need to process it again
|
||||
return;
|
||||
}
|
||||
|
||||
// lambda to get the decremented index based on the replaced positions
|
||||
let decremented_index = |index: usize| -> usize {
|
||||
let decrement = replaced_position.iter().filter(|&&i| i < index).count();
|
||||
index - decrement
|
||||
};
|
||||
|
||||
suggestion.push((*span, decremented_index(*index).to_string()));
|
||||
|
||||
// If there are format options, we need to handle them as well
|
||||
if *format_options != FormatOptions::default() {
|
||||
// lambda to process width and precision format counts and add them to the suggestion
|
||||
let mut process_format_count = |count: &Option<FormatCount>, formatter: &dyn Fn(usize) -> String| {
|
||||
if let Some(FormatCount::Argument(FormatArgPosition {
|
||||
index: Ok(format_arg_index),
|
||||
kind: FormatArgPositionKind::Number,
|
||||
span: Some(format_arg_span),
|
||||
})) = count
|
||||
{
|
||||
suggestion.push((*format_arg_span, formatter(decremented_index(*format_arg_index))));
|
||||
}
|
||||
};
|
||||
|
||||
process_format_count(&format_options.width, &|index: usize| format!("{index}$"));
|
||||
process_format_count(&format_options.precision, &|index: usize| format!(".{index}$"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
|
||||
///
|
||||
/// `r#"a"#` -> (`a`, true)
|
||||
///
|
||||
/// `"b"` -> (`b`, false)
|
||||
fn extract_str_literal(literal: &str) -> Option<(String, bool)> {
|
||||
let (literal, raw) = match literal.strip_prefix('r') {
|
||||
Some(stripped) => (stripped.trim_matches('#'), true),
|
||||
None => (literal, false),
|
||||
};
|
||||
|
||||
Some((literal.strip_prefix('"')?.strip_suffix('"')?.to_string(), raw))
|
||||
}
|
||||
|
||||
enum UnescapeErr {
|
||||
/// Should still be linted, can be manually resolved by author, e.g.
|
||||
///
|
||||
/// ```ignore
|
||||
/// print!(r"{}", '"');
|
||||
/// ```
|
||||
Lint,
|
||||
/// Should not be linted, e.g.
|
||||
///
|
||||
/// ```ignore
|
||||
/// print!(r"{}", '\r');
|
||||
/// ```
|
||||
Ignore,
|
||||
}
|
||||
|
||||
/// Unescape a normal string into a raw string
|
||||
fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
|
||||
let mut unescaped = String::with_capacity(literal.len());
|
||||
let mut chars = literal.chars();
|
||||
let mut err = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'#' => err = true,
|
||||
'\\' => match chars.next() {
|
||||
Some('\\') => unescaped.push('\\'),
|
||||
Some('"') => err = true,
|
||||
_ => return Err(UnescapeErr::Ignore),
|
||||
},
|
||||
_ => unescaped.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
|
||||
}
|
||||
|
||||
/// Replaces `{` with `{{` and `}` with `}}`. If `preserve_unicode_escapes` is `true` the braces
|
||||
/// in `\u{xxxx}` are left unmodified
|
||||
#[expect(clippy::match_same_arms)]
|
||||
fn escape_braces(literal: &str, preserve_unicode_escapes: bool) -> String {
|
||||
#[derive(Clone, Copy)]
|
||||
enum State {
|
||||
Normal,
|
||||
Backslash,
|
||||
UnicodeEscape,
|
||||
}
|
||||
|
||||
let mut escaped = String::with_capacity(literal.len());
|
||||
let mut state = State::Normal;
|
||||
|
||||
for ch in literal.chars() {
|
||||
state = match (ch, state) {
|
||||
// Escape braces outside of unicode escapes by doubling them up
|
||||
('{' | '}', State::Normal) => {
|
||||
escaped.push(ch);
|
||||
State::Normal
|
||||
},
|
||||
// If `preserve_unicode_escapes` isn't enabled stay in `State::Normal`, otherwise:
|
||||
//
|
||||
// \u{aaaa} \\ \x01
|
||||
// ^ ^ ^
|
||||
('\\', State::Normal) if preserve_unicode_escapes => State::Backslash,
|
||||
// \u{aaaa}
|
||||
// ^
|
||||
('u', State::Backslash) => State::UnicodeEscape,
|
||||
// \xAA \\
|
||||
// ^ ^
|
||||
(_, State::Backslash) => State::Normal,
|
||||
// \u{aaaa}
|
||||
// ^
|
||||
('}', State::UnicodeEscape) => State::Normal,
|
||||
_ => state,
|
||||
};
|
||||
|
||||
escaped.push(ch);
|
||||
}
|
||||
|
||||
escaped
|
||||
}
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
use clippy_config::Conf;
|
||||
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
|
||||
use clippy_utils::macros::{FormatArgsStorage, MacroCall, format_arg_removal_span, root_macro_call_first_node};
|
||||
use clippy_utils::source::{SpanRangeExt, expand_past_previous_comma};
|
||||
use clippy_utils::diagnostics::span_lint;
|
||||
use clippy_utils::macros::{FormatArgsStorage, root_macro_call_first_node};
|
||||
use clippy_utils::{is_in_test, sym};
|
||||
use rustc_ast::token::LitKind;
|
||||
use rustc_ast::{
|
||||
FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatCount, FormatOptions,
|
||||
FormatPlaceholder, FormatTrait,
|
||||
};
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_hir::{Expr, Impl, Item, ItemKind, OwnerId};
|
||||
use rustc_lint::{LateContext, LateLintPass, LintContext};
|
||||
use rustc_session::impl_lint_pass;
|
||||
use rustc_span::{BytePos, Span};
|
||||
|
||||
mod empty_string;
|
||||
mod literal;
|
||||
mod use_debug;
|
||||
mod with_newline;
|
||||
|
||||
declare_clippy_lint! {
|
||||
/// ### What it does
|
||||
|
|
@ -326,27 +323,18 @@ impl<'tcx> LateLintPass<'tcx> for Write {
|
|||
|
||||
match diag_name {
|
||||
sym::print_macro | sym::eprint_macro | sym::write_macro => {
|
||||
check_newline(cx, format_args, ¯o_call, name);
|
||||
with_newline::check(cx, format_args, ¯o_call, name);
|
||||
},
|
||||
sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
|
||||
check_empty_string(cx, format_args, ¯o_call, name);
|
||||
empty_string::check(cx, format_args, ¯o_call, name);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
check_literal(cx, format_args, name);
|
||||
literal::check(cx, format_args, name);
|
||||
|
||||
if !self.in_debug_impl() {
|
||||
for piece in &format_args.template {
|
||||
if let &FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
span: Some(span),
|
||||
format_trait: FormatTrait::Debug,
|
||||
..
|
||||
}) = piece
|
||||
{
|
||||
span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting");
|
||||
}
|
||||
}
|
||||
use_debug::check(cx, format_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -364,371 +352,3 @@ fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
|
||||
let Some(&FormatArgsPiece::Literal(last)) = format_args.template.last() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let count_vertical_whitespace = || {
|
||||
format_args
|
||||
.template
|
||||
.iter()
|
||||
.filter_map(|piece| match piece {
|
||||
FormatArgsPiece::Literal(literal) => Some(literal),
|
||||
FormatArgsPiece::Placeholder(_) => None,
|
||||
})
|
||||
.flat_map(|literal| literal.as_str().chars())
|
||||
.filter(|ch| matches!(ch, '\r' | '\n'))
|
||||
.count()
|
||||
};
|
||||
|
||||
if last.as_str().ends_with('\n')
|
||||
// ignore format strings with other internal vertical whitespace
|
||||
&& count_vertical_whitespace() == 1
|
||||
{
|
||||
let mut format_string_span = format_args.span;
|
||||
|
||||
let lint = if name == "write" {
|
||||
format_string_span = expand_past_previous_comma(cx, format_string_span);
|
||||
|
||||
WRITE_WITH_NEWLINE
|
||||
} else {
|
||||
PRINT_WITH_NEWLINE
|
||||
};
|
||||
|
||||
span_lint_and_then(
|
||||
cx,
|
||||
lint,
|
||||
macro_call.span,
|
||||
format!("using `{name}!()` with a format string that ends in a single newline"),
|
||||
|diag| {
|
||||
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
|
||||
let Some(format_snippet) = format_string_span.get_source_text(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if format_args.template.len() == 1 && last == sym::LF {
|
||||
// print!("\n"), write!(f, "\n")
|
||||
|
||||
diag.multipart_suggestion(
|
||||
format!("use `{name}ln!` instead"),
|
||||
vec![(name_span, format!("{name}ln")), (format_string_span, String::new())],
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
} else if format_snippet.ends_with("\\n\"") {
|
||||
// print!("...\n"), write!(f, "...\n")
|
||||
|
||||
let hi = format_string_span.hi();
|
||||
let newline_span = format_string_span.with_lo(hi - BytePos(3)).with_hi(hi - BytePos(1));
|
||||
|
||||
diag.multipart_suggestion(
|
||||
format!("use `{name}ln!` instead"),
|
||||
vec![(name_span, format!("{name}ln")), (newline_span, String::new())],
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
|
||||
if let [FormatArgsPiece::Literal(sym::LF)] = &format_args.template[..] {
|
||||
let mut span = format_args.span;
|
||||
|
||||
let lint = if name == "writeln" {
|
||||
span = expand_past_previous_comma(cx, span);
|
||||
|
||||
WRITELN_EMPTY_STRING
|
||||
} else {
|
||||
PRINTLN_EMPTY_STRING
|
||||
};
|
||||
|
||||
span_lint_and_then(
|
||||
cx,
|
||||
lint,
|
||||
macro_call.span,
|
||||
format!("empty string literal in `{name}!`"),
|
||||
|diag| {
|
||||
diag.span_suggestion(
|
||||
span,
|
||||
"remove the empty string",
|
||||
String::new(),
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
|
||||
let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos);
|
||||
|
||||
let lint_name = if name.starts_with("write") {
|
||||
WRITE_LITERAL
|
||||
} else {
|
||||
PRINT_LITERAL
|
||||
};
|
||||
|
||||
let mut counts = vec![0u32; format_args.arguments.all_args().len()];
|
||||
for piece in &format_args.template {
|
||||
if let FormatArgsPiece::Placeholder(placeholder) = piece {
|
||||
counts[arg_index(&placeholder.argument)] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut suggestion: Vec<(Span, String)> = vec![];
|
||||
// holds index of replaced positional arguments; used to decrement the index of the remaining
|
||||
// positional arguments.
|
||||
let mut replaced_position: Vec<usize> = vec![];
|
||||
let mut sug_span: Option<Span> = None;
|
||||
|
||||
for piece in &format_args.template {
|
||||
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
argument,
|
||||
span: Some(placeholder_span),
|
||||
format_trait: FormatTrait::Display,
|
||||
format_options,
|
||||
}) = piece
|
||||
&& *format_options == FormatOptions::default()
|
||||
&& let index = arg_index(argument)
|
||||
&& counts[index] == 1
|
||||
&& let Some(arg) = format_args.arguments.by_index(index)
|
||||
&& let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind
|
||||
&& !arg.expr.span.from_expansion()
|
||||
&& let Some(value_string) = arg.expr.span.get_source_text(cx)
|
||||
{
|
||||
let (replacement, replace_raw) = match lit.kind {
|
||||
LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) {
|
||||
Some(extracted) => extracted,
|
||||
None => return,
|
||||
},
|
||||
LitKind::Char => (
|
||||
match lit.symbol {
|
||||
sym::DOUBLE_QUOTE => "\\\"",
|
||||
sym::BACKSLASH_SINGLE_QUOTE => "'",
|
||||
_ => match value_string.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
|
||||
Some(stripped) => stripped,
|
||||
None => return,
|
||||
},
|
||||
}
|
||||
.to_string(),
|
||||
false,
|
||||
),
|
||||
LitKind::Bool => (lit.symbol.to_string(), false),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let Some(format_string_snippet) = format_args.span.get_source_text(cx) else {
|
||||
continue;
|
||||
};
|
||||
let format_string_is_raw = format_string_snippet.starts_with('r');
|
||||
|
||||
let replacement = match (format_string_is_raw, replace_raw) {
|
||||
(false, false) => Some(replacement),
|
||||
(false, true) => Some(replacement.replace('\\', "\\\\").replace('"', "\\\"")),
|
||||
(true, false) => match conservative_unescape(&replacement) {
|
||||
Ok(unescaped) => Some(unescaped),
|
||||
Err(UnescapeErr::Lint) => None,
|
||||
Err(UnescapeErr::Ignore) => continue,
|
||||
},
|
||||
(true, true) => {
|
||||
if replacement.contains(['#', '"']) {
|
||||
None
|
||||
} else {
|
||||
Some(replacement)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
sug_span = Some(sug_span.unwrap_or(arg.expr.span).to(arg.expr.span));
|
||||
|
||||
if let Some((_, index)) = format_arg_piece_span(piece) {
|
||||
replaced_position.push(index);
|
||||
}
|
||||
|
||||
if let Some(replacement) = replacement
|
||||
// `format!("{}", "a")`, `format!("{named}", named = "b")
|
||||
// ~~~~~ ~~~~~~~~~~~~~
|
||||
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
|
||||
{
|
||||
let replacement = escape_braces(&replacement, !format_string_is_raw && !replace_raw);
|
||||
suggestion.push((*placeholder_span, replacement));
|
||||
suggestion.push((removal_span, String::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement the index of the remaining by the number of replaced positional arguments
|
||||
if !suggestion.is_empty() {
|
||||
for piece in &format_args.template {
|
||||
relocalize_format_args_indexes(piece, &mut suggestion, &replaced_position);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(span) = sug_span {
|
||||
span_lint_and_then(cx, lint_name, span, "literal with an empty format string", |diag| {
|
||||
if !suggestion.is_empty() {
|
||||
diag.multipart_suggestion("try", suggestion, Applicability::MachineApplicable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract Span and its index from the given `piece`
|
||||
fn format_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> {
|
||||
match piece {
|
||||
FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
argument: FormatArgPosition { index: Ok(index), .. },
|
||||
span: Some(span),
|
||||
..
|
||||
}) => Some((*span, *index)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Relocalizes the indexes of positional arguments in the format string
|
||||
fn relocalize_format_args_indexes(
|
||||
piece: &FormatArgsPiece,
|
||||
suggestion: &mut Vec<(Span, String)>,
|
||||
replaced_position: &[usize],
|
||||
) {
|
||||
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
argument:
|
||||
FormatArgPosition {
|
||||
index: Ok(index),
|
||||
// Only consider positional arguments
|
||||
kind: FormatArgPositionKind::Number,
|
||||
span: Some(span),
|
||||
},
|
||||
format_options,
|
||||
..
|
||||
}) = piece
|
||||
{
|
||||
if suggestion.iter().any(|(s, _)| s.overlaps(*span)) {
|
||||
// If the span is already in the suggestion, we don't need to process it again
|
||||
return;
|
||||
}
|
||||
|
||||
// lambda to get the decremented index based on the replaced positions
|
||||
let decremented_index = |index: usize| -> usize {
|
||||
let decrement = replaced_position.iter().filter(|&&i| i < index).count();
|
||||
index - decrement
|
||||
};
|
||||
|
||||
suggestion.push((*span, decremented_index(*index).to_string()));
|
||||
|
||||
// If there are format options, we need to handle them as well
|
||||
if *format_options != FormatOptions::default() {
|
||||
// lambda to process width and precision format counts and add them to the suggestion
|
||||
let mut process_format_count = |count: &Option<FormatCount>, formatter: &dyn Fn(usize) -> String| {
|
||||
if let Some(FormatCount::Argument(FormatArgPosition {
|
||||
index: Ok(format_arg_index),
|
||||
kind: FormatArgPositionKind::Number,
|
||||
span: Some(format_arg_span),
|
||||
})) = count
|
||||
{
|
||||
suggestion.push((*format_arg_span, formatter(decremented_index(*format_arg_index))));
|
||||
}
|
||||
};
|
||||
|
||||
process_format_count(&format_options.width, &|index: usize| format!("{index}$"));
|
||||
process_format_count(&format_options.precision, &|index: usize| format!(".{index}$"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
|
||||
///
|
||||
/// `r#"a"#` -> (`a`, true)
|
||||
///
|
||||
/// `"b"` -> (`b`, false)
|
||||
fn extract_str_literal(literal: &str) -> Option<(String, bool)> {
|
||||
let (literal, raw) = match literal.strip_prefix('r') {
|
||||
Some(stripped) => (stripped.trim_matches('#'), true),
|
||||
None => (literal, false),
|
||||
};
|
||||
|
||||
Some((literal.strip_prefix('"')?.strip_suffix('"')?.to_string(), raw))
|
||||
}
|
||||
|
||||
enum UnescapeErr {
|
||||
/// Should still be linted, can be manually resolved by author, e.g.
|
||||
///
|
||||
/// ```ignore
|
||||
/// print!(r"{}", '"');
|
||||
/// ```
|
||||
Lint,
|
||||
/// Should not be linted, e.g.
|
||||
///
|
||||
/// ```ignore
|
||||
/// print!(r"{}", '\r');
|
||||
/// ```
|
||||
Ignore,
|
||||
}
|
||||
|
||||
/// Unescape a normal string into a raw string
|
||||
fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
|
||||
let mut unescaped = String::with_capacity(literal.len());
|
||||
let mut chars = literal.chars();
|
||||
let mut err = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'#' => err = true,
|
||||
'\\' => match chars.next() {
|
||||
Some('\\') => unescaped.push('\\'),
|
||||
Some('"') => err = true,
|
||||
_ => return Err(UnescapeErr::Ignore),
|
||||
},
|
||||
_ => unescaped.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
|
||||
}
|
||||
|
||||
/// Replaces `{` with `{{` and `}` with `}}`. If `preserve_unicode_escapes` is `true` the braces in
|
||||
/// `\u{xxxx}` are left unmodified
|
||||
#[expect(clippy::match_same_arms)]
|
||||
fn escape_braces(literal: &str, preserve_unicode_escapes: bool) -> String {
|
||||
#[derive(Clone, Copy)]
|
||||
enum State {
|
||||
Normal,
|
||||
Backslash,
|
||||
UnicodeEscape,
|
||||
}
|
||||
|
||||
let mut escaped = String::with_capacity(literal.len());
|
||||
let mut state = State::Normal;
|
||||
|
||||
for ch in literal.chars() {
|
||||
state = match (ch, state) {
|
||||
// Escape braces outside of unicode escapes by doubling them up
|
||||
('{' | '}', State::Normal) => {
|
||||
escaped.push(ch);
|
||||
State::Normal
|
||||
},
|
||||
// If `preserve_unicode_escapes` isn't enabled stay in `State::Normal`, otherwise:
|
||||
//
|
||||
// \u{aaaa} \\ \x01
|
||||
// ^ ^ ^
|
||||
('\\', State::Normal) if preserve_unicode_escapes => State::Backslash,
|
||||
// \u{aaaa}
|
||||
// ^
|
||||
('u', State::Backslash) => State::UnicodeEscape,
|
||||
// \xAA \\
|
||||
// ^ ^
|
||||
(_, State::Backslash) => State::Normal,
|
||||
// \u{aaaa}
|
||||
// ^
|
||||
('}', State::UnicodeEscape) => State::Normal,
|
||||
_ => state,
|
||||
};
|
||||
|
||||
escaped.push(ch);
|
||||
}
|
||||
|
||||
escaped
|
||||
}
|
||||
|
|
|
|||
18
clippy_lints/src/write/use_debug.rs
Normal file
18
clippy_lints/src/write/use_debug.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use clippy_utils::diagnostics::span_lint;
|
||||
use rustc_ast::{FormatArgs, FormatArgsPiece, FormatPlaceholder, FormatTrait};
|
||||
use rustc_lint::LateContext;
|
||||
|
||||
use super::USE_DEBUG;
|
||||
|
||||
pub(super) fn check(cx: &LateContext<'_>, format_args: &FormatArgs) {
|
||||
for piece in &format_args.template {
|
||||
if let &FormatArgsPiece::Placeholder(FormatPlaceholder {
|
||||
span: Some(span),
|
||||
format_trait: FormatTrait::Debug,
|
||||
..
|
||||
}) = piece
|
||||
{
|
||||
span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting");
|
||||
}
|
||||
}
|
||||
}
|
||||
78
clippy_lints/src/write/with_newline.rs
Normal file
78
clippy_lints/src/write/with_newline.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use clippy_utils::diagnostics::span_lint_and_then;
|
||||
use clippy_utils::macros::MacroCall;
|
||||
use clippy_utils::source::{SpanRangeExt, expand_past_previous_comma};
|
||||
use clippy_utils::sym;
|
||||
use rustc_ast::{FormatArgs, FormatArgsPiece};
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_lint::{LateContext, LintContext};
|
||||
use rustc_span::BytePos;
|
||||
|
||||
use super::{PRINT_WITH_NEWLINE, WRITE_WITH_NEWLINE};
|
||||
|
||||
pub(super) fn check(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
|
||||
let Some(&FormatArgsPiece::Literal(last)) = format_args.template.last() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let count_vertical_whitespace = || {
|
||||
format_args
|
||||
.template
|
||||
.iter()
|
||||
.filter_map(|piece| match piece {
|
||||
FormatArgsPiece::Literal(literal) => Some(literal),
|
||||
FormatArgsPiece::Placeholder(_) => None,
|
||||
})
|
||||
.flat_map(|literal| literal.as_str().chars())
|
||||
.filter(|ch| matches!(ch, '\r' | '\n'))
|
||||
.count()
|
||||
};
|
||||
|
||||
if last.as_str().ends_with('\n')
|
||||
// ignore format strings with other internal vertical whitespace
|
||||
&& count_vertical_whitespace() == 1
|
||||
{
|
||||
let mut format_string_span = format_args.span;
|
||||
|
||||
let lint = if name == "write" {
|
||||
format_string_span = expand_past_previous_comma(cx, format_string_span);
|
||||
|
||||
WRITE_WITH_NEWLINE
|
||||
} else {
|
||||
PRINT_WITH_NEWLINE
|
||||
};
|
||||
|
||||
span_lint_and_then(
|
||||
cx,
|
||||
lint,
|
||||
macro_call.span,
|
||||
format!("using `{name}!()` with a format string that ends in a single newline"),
|
||||
|diag| {
|
||||
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
|
||||
let Some(format_snippet) = format_string_span.get_source_text(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if format_args.template.len() == 1 && last == sym::LF {
|
||||
// print!("\n"), write!(f, "\n")
|
||||
|
||||
diag.multipart_suggestion(
|
||||
format!("use `{name}ln!` instead"),
|
||||
vec![(name_span, format!("{name}ln")), (format_string_span, String::new())],
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
} else if format_snippet.ends_with("\\n\"") {
|
||||
// print!("...\n"), write!(f, "...\n")
|
||||
|
||||
let hi = format_string_span.hi();
|
||||
let newline_span = format_string_span.with_lo(hi - BytePos(3)).with_hi(hi - BytePos(1));
|
||||
|
||||
diag.multipart_suggestion(
|
||||
format!("use `{name}ln!` instead"),
|
||||
vec![(name_span, format!("{name}ln")), (newline_span, String::new())],
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue