about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume1.gomez@gmail.com>2024-09-18 00:09:34 +0200
committerGuillaume Gomez <guillaume1.gomez@gmail.com>2024-11-19 15:44:19 +0100
commitcd7cec9066f35e2fd661a69830fd57864b41ea44 (patch)
tree8e971c922cf686f28e585115d285636bc29e6823
parent53994bda923a81d1c12d3e32dab3c208beaaca63 (diff)
downloadrust-cd7cec9066f35e2fd661a69830fd57864b41ea44.tar.gz
rust-cd7cec9066f35e2fd661a69830fd57864b41ea44.zip
Add new `literal_string_with_formatting_arg` lint
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/lib.rs3
-rw-r--r--clippy_lints/src/literal_string_with_formatting_arg.rs101
4 files changed, 106 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd3124ee9a3..3a82bfce82b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index dff60f76b74..a3f89b3f70c 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -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,
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index c9064df25ac..44e36b99b5d 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -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));
diff --git a/clippy_lints/src/literal_string_with_formatting_arg.rs b/clippy_lints/src/literal_string_with_formatting_arg.rs
new file mode 100644
index 00000000000..e72f0de90c6
--- /dev/null
+++ b/clippy_lints/src/literal_string_with_formatting_arg.rs
@@ -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",
+                );
+            }
+        }
+    }
+}