Add new literal_string_with_formatting_arg lint

This commit is contained in:
Guillaume Gomez 2024-09-18 00:09:34 +02:00
parent 53994bda92
commit cd7cec9066
4 changed files with 106 additions and 0 deletions

View file

@ -5639,6 +5639,7 @@ Released 2018-09-13
[`lines_filter_map_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok
[`linkedlist`]: https://rust-lang.github.io/rust-clippy/master/index.html#linkedlist
[`lint_groups_priority`]: https://rust-lang.github.io/rust-clippy/master/index.html#lint_groups_priority
[`literal_string_with_formatting_arg`]: https://rust-lang.github.io/rust-clippy/master/index.html#literal_string_with_formatting_arg
[`little_endian_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#little_endian_bytes
[`logic_bug`]: https://rust-lang.github.io/rust-clippy/master/index.html#logic_bug
[`lossy_float_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#lossy_float_literal

View file

@ -276,6 +276,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::literal_representation::MISTYPED_LITERAL_SUFFIXES_INFO,
crate::literal_representation::UNREADABLE_LITERAL_INFO,
crate::literal_representation::UNUSUAL_BYTE_GROUPINGS_INFO,
crate::literal_string_with_formatting_arg::LITERAL_STRING_WITH_FORMATTING_ARG_INFO,
crate::loops::EMPTY_LOOP_INFO,
crate::loops::EXPLICIT_COUNTER_LOOP_INFO,
crate::loops::EXPLICIT_INTO_ITER_LOOP_INFO,

View file

@ -49,6 +49,7 @@ extern crate rustc_lexer;
extern crate rustc_lint;
extern crate rustc_middle;
extern crate rustc_parse;
extern crate rustc_parse_format;
extern crate rustc_resolve;
extern crate rustc_session;
extern crate rustc_span;
@ -196,6 +197,7 @@ mod let_with_type_underscore;
mod lifetimes;
mod lines_filter_map_ok;
mod literal_representation;
mod literal_string_with_formatting_arg;
mod loops;
mod macro_metavars_in_unsafe;
mod macro_use;
@ -959,6 +961,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(move |_| Box::new(manual_div_ceil::ManualDivCeil::new(conf)));
store.register_late_pass(|_| Box::new(manual_is_power_of_two::ManualIsPowerOfTwo));
store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions));
store.register_early_pass(|| Box::new(literal_string_with_formatting_arg::LiteralStringWithFormattingArg));
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));

View file

@ -0,0 +1,101 @@
use rustc_ast::ast::{Expr, ExprKind};
use rustc_ast::token::LitKind;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_parse_format::{ParseMode, Parser, Piece};
use rustc_session::declare_lint_pass;
use rustc_span::BytePos;
use clippy_utils::diagnostics::span_lint;
declare_clippy_lint! {
/// ### What it does
/// Checks if string literals have formatting arguments outside of macros
/// using them (like `format!`).
///
/// ### Why is this bad?
/// It will likely not generate the expected content.
///
/// ### Example
/// ```no_run
/// let x: Option<usize> = None;
/// let y = "hello";
/// x.expect("{y:?}");
/// ```
/// Use instead:
/// ```no_run
/// let x: Option<usize> = None;
/// let y = "hello";
/// x.expect(&format!("{y:?}"));
/// ```
#[clippy::version = "1.83.0"]
pub LITERAL_STRING_WITH_FORMATTING_ARG,
suspicious,
"Checks if string literals have formatting arguments"
}
declare_lint_pass!(LiteralStringWithFormattingArg => [LITERAL_STRING_WITH_FORMATTING_ARG]);
impl EarlyLintPass for LiteralStringWithFormattingArg {
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
if let ExprKind::Lit(lit) = expr.kind {
let add = match lit.kind {
LitKind::Str => 1,
LitKind::StrRaw(nb) => nb as usize + 2,
_ => return,
};
let fmt_str = lit.symbol.as_str();
let lo = expr.span.lo();
let mut current = fmt_str;
let mut diff_len = 0;
let mut parser = Parser::new(current, None, None, false, ParseMode::Format);
let mut spans = Vec::new();
while let Some(piece) = parser.next() {
if let Some(error) = parser.errors.last() {
// We simply ignore the errors and move after them.
if error.span.end >= current.len() {
break;
}
current = &current[error.span.end + 1..];
diff_len = fmt_str.len() - current.len();
parser = Parser::new(current, None, None, false, ParseMode::Format);
} else if let Piece::NextArgument(arg) = piece {
let mut pos = arg.position_span;
pos.start += diff_len;
pos.end += diff_len;
let start = fmt_str[..pos.start].rfind('{').unwrap_or(pos.start);
// If this is a unicode character escape, we don't want to lint.
if start > 1 && fmt_str[..start].ends_with("\\u") {
continue;
}
let mut end = fmt_str[pos.end..].find('}').map_or(pos.end, |found| found + pos.end);
if fmt_str[start..end].contains(':') {
end += 1;
}
spans.push(
expr.span
.with_hi(lo + BytePos((start + add) as _))
.with_lo(lo + BytePos((end + add) as _)),
);
}
}
if spans.len() == 1 {
span_lint(
cx,
LITERAL_STRING_WITH_FORMATTING_ARG,
spans,
"this looks like a formatting argument but it is not part of a formatting macro",
);
} else if spans.len() > 1 {
span_lint(
cx,
LITERAL_STRING_WITH_FORMATTING_ARG,
spans,
"these look like formatting arguments but are not part of a formatting macro",
);
}
}
}
}