-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new
literal_string_with_formatting_args
lint (#13410)
Fixes #10195. changelog: Added new [`literal_string_with_formatting_args`] `pedantic` lint [#13410](#13410)
- Loading branch information
Showing
20 changed files
with
308 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
clippy_lints/src/literal_string_with_formatting_args.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
use rustc_ast::{LitKind, StrStyle}; | ||
use rustc_hir::{Expr, ExprKind}; | ||
use rustc_lexer::is_ident; | ||
use rustc_lint::{LateContext, LateLintPass}; | ||
use rustc_parse_format::{ParseMode, Parser, Piece}; | ||
use rustc_session::declare_lint_pass; | ||
use rustc_span::{BytePos, Span}; | ||
|
||
use clippy_utils::diagnostics::span_lint; | ||
use clippy_utils::mir::enclosing_mir; | ||
|
||
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_ARGS, | ||
suspicious, | ||
"Checks if string literals have formatting arguments" | ||
} | ||
|
||
declare_lint_pass!(LiteralStringWithFormattingArg => [LITERAL_STRING_WITH_FORMATTING_ARGS]); | ||
|
||
fn emit_lint(cx: &LateContext<'_>, expr: &Expr<'_>, spans: &[(Span, Option<String>)]) { | ||
if !spans.is_empty() | ||
&& let Some(mir) = enclosing_mir(cx.tcx, expr.hir_id) | ||
{ | ||
let spans = spans | ||
.iter() | ||
.filter_map(|(span, name)| { | ||
if let Some(name) = name { | ||
// We need to check that the name is a local. | ||
if !mir | ||
.var_debug_info | ||
.iter() | ||
.any(|local| !local.source_info.span.from_expansion() && local.name.as_str() == name) | ||
{ | ||
return None; | ||
} | ||
} | ||
Some(*span) | ||
}) | ||
.collect::<Vec<_>>(); | ||
match spans.len() { | ||
0 => {}, | ||
1 => { | ||
span_lint( | ||
cx, | ||
LITERAL_STRING_WITH_FORMATTING_ARGS, | ||
spans, | ||
"this looks like a formatting argument but it is not part of a formatting macro", | ||
); | ||
}, | ||
_ => { | ||
span_lint( | ||
cx, | ||
LITERAL_STRING_WITH_FORMATTING_ARGS, | ||
spans, | ||
"these look like formatting arguments but are not part of a formatting macro", | ||
); | ||
}, | ||
} | ||
} | ||
} | ||
|
||
impl LateLintPass<'_> for LiteralStringWithFormattingArg { | ||
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { | ||
if expr.span.from_expansion() { | ||
return; | ||
} | ||
if let ExprKind::Lit(lit) = expr.kind { | ||
let (add, symbol) = match lit.node { | ||
LitKind::Str(symbol, style) => { | ||
let add = match style { | ||
StrStyle::Cooked => 1, | ||
StrStyle::Raw(nb) => nb as usize + 2, | ||
}; | ||
(add, symbol) | ||
}, | ||
_ => return, | ||
}; | ||
let fmt_str = 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 = ¤t[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; | ||
} | ||
|
||
if fmt_str[start + 1..].trim_start().starts_with('}') { | ||
// We ignore `{}`. | ||
continue; | ||
} | ||
|
||
let end = fmt_str[start + 1..] | ||
.find('}') | ||
.map_or(pos.end, |found| start + 1 + found) | ||
+ 1; | ||
let ident_start = start + 1; | ||
let colon_pos = fmt_str[ident_start..end].find(':'); | ||
let ident_end = colon_pos.unwrap_or(end - 1); | ||
let mut name = None; | ||
if ident_start < ident_end | ||
&& let arg = &fmt_str[ident_start..ident_end] | ||
&& !arg.is_empty() | ||
&& is_ident(arg) | ||
{ | ||
name = Some(arg.to_string()); | ||
} else if colon_pos.is_none() { | ||
// Not a `{:?}`. | ||
continue; | ||
} | ||
spans.push(( | ||
expr.span | ||
.with_hi(lo + BytePos((start + add).try_into().unwrap())) | ||
.with_lo(lo + BytePos((end + add).try_into().unwrap())), | ||
name, | ||
)); | ||
} | ||
} | ||
emit_lint(cx, expr, &spans); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.