diff --git a/CHANGELOG.md b/CHANGELOG.md index cc966972939a..88037208cadb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6170,6 +6170,7 @@ Released 2018-09-13 [`used_underscore_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#used_underscore_items [`useless_asref`]: https://rust-lang.github.io/rust-clippy/master/index.html#useless_asref [`useless_attribute`]: https://rust-lang.github.io/rust-clippy/master/index.html#useless_attribute +[`useless_concat`]: https://rust-lang.github.io/rust-clippy/master/index.html#useless_concat [`useless_conversion`]: https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion [`useless_format`]: https://rust-lang.github.io/rust-clippy/master/index.html#useless_format [`useless_let_if_seq`]: https://rust-lang.github.io/rust-clippy/master/index.html#useless_let_if_seq diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 304622afe530..00fffd7585b1 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -770,6 +770,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::unwrap_in_result::UNWRAP_IN_RESULT_INFO, crate::upper_case_acronyms::UPPER_CASE_ACRONYMS_INFO, crate::use_self::USE_SELF_INFO, + crate::useless_concat::USELESS_CONCAT_INFO, crate::useless_conversion::USELESS_CONVERSION_INFO, crate::vec::USELESS_VEC_INFO, crate::vec_init_then_push::VEC_INIT_THEN_PUSH_INFO, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 017e6e2a1423..04acee26661d 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -388,6 +388,7 @@ mod unwrap; mod unwrap_in_result; mod upper_case_acronyms; mod use_self; +mod useless_concat; mod useless_conversion; mod vec; mod vec_init_then_push; @@ -967,5 +968,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp)); store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound)); store.register_late_pass(move |_| Box::new(arbitrary_source_item_ordering::ArbitrarySourceItemOrdering::new(conf))); + store.register_late_pass(|_| Box::new(useless_concat::UselessConcat)); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/clippy_lints/src/useless_concat.rs b/clippy_lints/src/useless_concat.rs new file mode 100644 index 000000000000..ebff1dd17a6d --- /dev/null +++ b/clippy_lints/src/useless_concat.rs @@ -0,0 +1,101 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::macro_backtrace; +use clippy_utils::match_def_path; +use clippy_utils::source::snippet_opt; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind}; +use rustc_lexer::{Cursor, TokenKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::declare_lint_pass; + +declare_clippy_lint! { + /// ### What it does + /// Checks that the `concat!` macro has at least two arguments. + /// + /// ### Why is this bad? + /// If there are less than 2 arguments, then calling the macro is doing nothing. + /// + /// ### Example + /// ```no_run + /// let x = concat!("a"); + /// ``` + /// Use instead: + /// ```no_run + /// let x = "a"; + /// ``` + #[clippy::version = "1.85.0"] + pub USELESS_CONCAT, + suspicious, + "checks that the `concat` macro has at least two arguments" +} + +declare_lint_pass!(UselessConcat => [USELESS_CONCAT]); + +impl LateLintPass<'_> for UselessConcat { + fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { + // Check that the expression is generated by a macro. + if expr.span.from_expansion() + // Check that it's a string literal. + && let ExprKind::Lit(lit) = expr.kind + && let LitKind::Str(_, _) = lit.node + // Get the direct parent of the expression. + && let Some(macro_call) = macro_backtrace(expr.span).next() + // Check if the `concat` macro from the `core` library. + && match_def_path(cx, macro_call.def_id, &["core", "macros", "builtin", "concat"]) + // We get the original code to parse it. + && let Some(original_code) = snippet_opt(cx, macro_call.span) + // This check allows us to ensure that the code snippet: + // 1. Doesn't come from proc-macro expansion. + // 2. Doesn't come from foreign macro expansion. + // + // It works as follows: if the snippet we get doesn't contain `concat!(`, then it + // means it's not code written in the current crate so we shouldn't lint. + && let mut parts = original_code.split('!') + && parts.next().is_some_and(|p| p.trim() == "concat") + && parts.next().is_some_and(|p| p.trim().starts_with('(')) + { + let mut literal = None; + let mut current_pos = 0; + let mut cursor = Cursor::new(&original_code); + let mut nb_commas = 0; + let mut nb_idents = 0; + loop { + let token = cursor.advance_token(); + match token.kind { + TokenKind::Eof => break, + TokenKind::Literal { .. } => { + if literal.is_some() { + return; + } + literal = Some(&original_code[current_pos..current_pos + token.len as usize]); + }, + TokenKind::Ident => nb_idents += 1, + TokenKind::Comma => { + nb_commas += 1; + if nb_commas > 1 { + return; + } + }, + // We're inside a macro definition and we are manipulating something we likely + // shouldn't so aborting. + TokenKind::Dollar => return, + _ => {}, + } + current_pos += token.len as usize; + } + // There should always be the ident of the `concat` macro. + if nb_idents == 1 { + span_lint_and_sugg( + cx, + USELESS_CONCAT, + macro_call.span, + "unneeded use of `concat!` macro", + "replace with", + literal.unwrap_or("\"\"").into(), + Applicability::MachineApplicable, + ); + } + } + } +} diff --git a/tests/ui/useless_concat.fixed b/tests/ui/useless_concat.fixed new file mode 100644 index 000000000000..0ead2d362f68 --- /dev/null +++ b/tests/ui/useless_concat.fixed @@ -0,0 +1,19 @@ +#![warn(clippy::useless_concat)] +#![allow(clippy::print_literal)] + +macro_rules! my_concat { + ($fmt:literal $(, $e:expr)*) => { + println!(concat!("ERROR: ", $fmt), $($e,)*); + } +} + +fn main() { + let x = ""; //~ useless_concat + let x = "a"; //~ useless_concat + println!("b: {}", "a"); //~ useless_concat + // Should not lint. + let x = concat!("a", "b"); + let local_i32 = 1; + my_concat!("{}", local_i32); + let x = concat!(file!(), "#L", line!()); +} diff --git a/tests/ui/useless_concat.rs b/tests/ui/useless_concat.rs new file mode 100644 index 000000000000..87182f96c952 --- /dev/null +++ b/tests/ui/useless_concat.rs @@ -0,0 +1,19 @@ +#![warn(clippy::useless_concat)] +#![allow(clippy::print_literal)] + +macro_rules! my_concat { + ($fmt:literal $(, $e:expr)*) => { + println!(concat!("ERROR: ", $fmt), $($e,)*); + } +} + +fn main() { + let x = concat!(); //~ useless_concat + let x = concat!("a"); //~ useless_concat + println!("b: {}", concat!("a")); //~ useless_concat + // Should not lint. + let x = concat!("a", "b"); + let local_i32 = 1; + my_concat!("{}", local_i32); + let x = concat!(file!(), "#L", line!()); +} diff --git a/tests/ui/useless_concat.stderr b/tests/ui/useless_concat.stderr new file mode 100644 index 000000000000..aa97827c6393 --- /dev/null +++ b/tests/ui/useless_concat.stderr @@ -0,0 +1,23 @@ +error: unneeded use of `concat!` macro + --> tests/ui/useless_concat.rs:11:13 + | +LL | let x = concat!(); + | ^^^^^^^^^ help: replace with: `""` + | + = note: `-D clippy::useless-concat` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::useless_concat)]` + +error: unneeded use of `concat!` macro + --> tests/ui/useless_concat.rs:12:13 + | +LL | let x = concat!("a"); + | ^^^^^^^^^^^^ help: replace with: `"a"` + +error: unneeded use of `concat!` macro + --> tests/ui/useless_concat.rs:13:23 + | +LL | println!("b: {}", concat!("a")); + | ^^^^^^^^^^^^ help: replace with: `"a"` + +error: aborting due to 3 previous errors +