From 4e8e2fc516cf1e17715ef2c422e22c8b8ac33809 Mon Sep 17 00:00:00 2001 From: Ginza Hatemi Date: Thu, 15 May 2025 05:53:22 -0600 Subject: [PATCH 1/6] Add `format_concat_args` lint to detect `format!()` calls replaceable with `concat!()` --- examples/supplementary/Cargo.lock | 144 ++++++++++++++---- .../format_concat_args/.cargo/config.toml | 6 + .../format_concat_args/.gitignore | 1 + .../format_concat_args/Cargo.toml | 20 +++ .../format_concat_args/README.md | 47 ++++++ .../format_concat_args/rust-toolchain | 3 + .../format_concat_args/src/lib.rs | 72 +++++++++ .../format_concat_args/test_lint.rs | 10 ++ .../format_concat_args/ui/main.rs | 62 ++++++++ .../format_concat_args/ui/main.stderr | 0 10 files changed, 339 insertions(+), 26 deletions(-) create mode 100644 examples/supplementary/format_concat_args/.cargo/config.toml create mode 100644 examples/supplementary/format_concat_args/.gitignore create mode 100644 examples/supplementary/format_concat_args/Cargo.toml create mode 100644 examples/supplementary/format_concat_args/README.md create mode 100644 examples/supplementary/format_concat_args/rust-toolchain create mode 100644 examples/supplementary/format_concat_args/src/lib.rs create mode 100644 examples/supplementary/format_concat_args/test_lint.rs create mode 100644 examples/supplementary/format_concat_args/ui/main.rs create mode 100644 examples/supplementary/format_concat_args/ui/main.stderr diff --git a/examples/supplementary/Cargo.lock b/examples/supplementary/Cargo.lock index 521678109..df387c7c1 100644 --- a/examples/supplementary/Cargo.lock +++ b/examples/supplementary/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.18" @@ -206,8 +215,8 @@ name = "commented_out_code" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "regex", "syn", ] @@ -349,7 +358,26 @@ dependencies = [ "anstyle", "anyhow", "cargo_metadata 0.19.2", - "dylint_internal", + "dylint_internal 4.1.0", + "log", + "once_cell", + "semver", + "serde", + "serde_json", + "tempfile", + "walkdir", +] + +[[package]] +name = "dylint" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288a5390c058d39da1ca1d952b712798904a1a48b10ebd0fa53c86ed90783965" +dependencies = [ + "ansi_term", + "anyhow", + "cargo_metadata 0.19.2", + "dylint_internal 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "log", "once_cell", "semver", @@ -379,12 +407,34 @@ dependencies = [ "toml", ] +[[package]] +name = "dylint_internal" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258d36f53de789719a4c97ecde1df80c16abbcd029c5d25f19c86164caa54cff" +dependencies = [ + "ansi_term", + "anyhow", + "bitflags", + "cargo_metadata 0.19.2", + "git2", + "home", + "if_chain", + "log", + "regex", + "rust-embed", + "rustversion", + "serde", + "thiserror 2.0.12", + "toml", +] + [[package]] name = "dylint_linting" version = "4.1.0" dependencies = [ "cargo_metadata 0.19.2", - "dylint_internal", + "dylint_internal 4.1.0", "paste", "rustversion", "serde", @@ -392,15 +442,48 @@ dependencies = [ "toml", ] +[[package]] +name = "dylint_linting" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3293a3baa87904faeea12506ec152b56655e40fc49709d905a9be6d277b56c8e" +dependencies = [ + "cargo_metadata 0.19.2", + "dylint_internal 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "paste", + "rustversion", + "serde", + "thiserror 2.0.12", + "toml", +] + +[[package]] +name = "dylint_testing" +version = "4.1.0" +dependencies = [ + "anyhow", + "cargo_metadata 0.19.2", + "compiletest_rs", + "dylint 4.1.0", + "dylint_internal 4.1.0", + "env_logger", + "once_cell", + "regex", + "serde_json", + "tempfile", +] + [[package]] name = "dylint_testing" version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c6119c632fc3bdb0d38c2ad41b6d417e5dfc535339850237d461a8725e1651" dependencies = [ "anyhow", "cargo_metadata 0.19.2", "compiletest_rs", - "dylint", - "dylint_internal", + "dylint 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dylint_internal 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger", "once_cell", "regex", @@ -460,8 +543,8 @@ dependencies = [ "cargo-util", "cargo_metadata 0.18.1", "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "once_cell", "pulldown-cmark", ] @@ -493,6 +576,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "format_concat_args" +version = "0.1.0" +dependencies = [ + "clippy_utils", + "dylint_linting 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dylint_testing 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -756,8 +848,8 @@ name = "inconsistent_struct_pattern" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", ] [[package]] @@ -905,9 +997,9 @@ name = "local_ref_cell" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_internal", - "dylint_linting", - "dylint_testing", + "dylint_internal 4.1.0", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", ] [[package]] @@ -937,8 +1029,8 @@ version = "4.1.0" dependencies = [ "cargo_metadata 0.18.1", "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "regex", ] @@ -1083,8 +1175,8 @@ name = "redundant_reference" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "serde", ] @@ -1298,7 +1390,7 @@ name = "supplementary" version = "4.1.0" dependencies = [ "commented_out_code", - "dylint_linting", + "dylint_linting 4.1.0", "escaping_doc_link", "inconsistent_struct_pattern", "local_ref_cell", @@ -1507,8 +1599,8 @@ name = "unnamed_constant" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "serde", ] @@ -1517,9 +1609,9 @@ name = "unnecessary_borrow_mut" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_internal", - "dylint_linting", - "dylint_testing", + "dylint_internal 4.1.0", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", ] [[package]] @@ -1527,9 +1619,9 @@ name = "unnecessary_conversion_for_trait" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_internal", - "dylint_linting", - "dylint_testing", + "dylint_internal 4.1.0", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "tempfile", ] diff --git a/examples/supplementary/format_concat_args/.cargo/config.toml b/examples/supplementary/format_concat_args/.cargo/config.toml new file mode 100644 index 000000000..226eca535 --- /dev/null +++ b/examples/supplementary/format_concat_args/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] + +# For Rust versions 1.74.0 and onward, the following alternative can be used +# (see https://github.com/rust-lang/cargo/pull/12535): +# linker = "dylint-link" diff --git a/examples/supplementary/format_concat_args/.gitignore b/examples/supplementary/format_concat_args/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/examples/supplementary/format_concat_args/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/supplementary/format_concat_args/Cargo.toml b/examples/supplementary/format_concat_args/Cargo.toml new file mode 100644 index 000000000..0708f2a41 --- /dev/null +++ b/examples/supplementary/format_concat_args/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "format_concat_args" +version = "0.1.0" +authors = ["Your Name "] +description = "A Dylint lint to suggest using `concat!(...)` instead of `format!(...)` for all-constant Display arguments." +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "238edf273d195c8e472851ebd60571f77f978ac8" } +dylint_linting = "4.0.1" + +[dev-dependencies] +dylint_testing = "4.0.1" + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/examples/supplementary/format_concat_args/README.md b/examples/supplementary/format_concat_args/README.md new file mode 100644 index 000000000..8b081b9d1 --- /dev/null +++ b/examples/supplementary/format_concat_args/README.md @@ -0,0 +1,47 @@ +# format_concat_args + +A Dylint lint to suggest using `concat!(...)` instead of `format!(...)` when all format arguments are constant. + +This lint identifies instances where `format!` calls could be replaced with more efficient `concat!` calls when all arguments are constant and use the `Display` trait. + +## Examples + +```rust +// Will trigger the lint: +let s = format!("Hello {}", "world"); // Could be: concat!("Hello ", "world") + +// Will not trigger the lint: +let name = "Bob"; +let s = format!("Hello {}", name); // Not constant + +// Will not trigger the lint: +let s = format!("Value: {:?}", 42); // Not using Display trait +``` + +## Building and Testing + +To build and test this lint: + +1. Make sure you have Dylint installed: + ```sh + cargo install dylint-link + ``` + +2. Build the lint: + ```sh + cargo build + ``` + +3. To run the lint against your own code: + ```sh + DYLINT_LIBRARY_PATH=/path/to/target/debug cargo dylint format_concat_args + ``` + +## Limitations + +- The current implementation provides a simple detection of `format!` calls but doesn't implement the full type checking and argument analysis. +- Works only with `std::format!` macros, not with other formatting macros like `println!` or `write!`. + +## References + +- [GitHub Issue #1601](https://github.com/trailofbits/dylint/issues/1601) diff --git a/examples/supplementary/format_concat_args/rust-toolchain b/examples/supplementary/format_concat_args/rust-toolchain new file mode 100644 index 000000000..f898a7a72 --- /dev/null +++ b/examples/supplementary/format_concat_args/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2025-02-20" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/examples/supplementary/format_concat_args/src/lib.rs b/examples/supplementary/format_concat_args/src/lib.rs new file mode 100644 index 000000000..bd05ebc2c --- /dev/null +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -0,0 +1,72 @@ +#![feature(rustc_private)] + +extern crate rustc_ast; +extern crate rustc_errors; +extern crate rustc_hir; +extern crate rustc_lint; +extern crate rustc_middle; +extern crate rustc_session; +extern crate rustc_span; + +#[cfg(not(feature = "rlib"))] +dylint_linting::dylint_library!(); + +use clippy_utils::{ + diagnostics::span_lint_and_sugg, + is_expn_of, +}; +use rustc_hir::Expr; +use rustc_lint::{Lint, LateContext, LateLintPass, LintContext}; +use rustc_session::declare_lint_pass; +use rustc_span::sym; + +// Declare the lint directly +pub static FORMAT_CONCAT_ARGS: &Lint = &Lint { + name: "format_concat_args", + default_level: rustc_lint::Level::Allow, + desc: "Checks for `format!(...)` invocations where `concat!(...)` could be used instead.", + edition_lint_opts: None, + report_in_external_macro: true, + future_incompatible: None, + is_externally_loaded: false, + eval_always: false, + feature_gate: None, + crate_level_only: false, +}; + +// Declare the lint pass +declare_lint_pass!(FormatConcatArgs => [FORMAT_CONCAT_ARGS]); + +impl<'tcx> LateLintPass<'tcx> for FormatConcatArgs { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // Check if the expression is a `format!` macro invocation + if is_expn_of(expr.span, &sym::format_args.as_str()).is_some() { + let expn_data = expr.span.ctxt().outer_expn_data(); + + // Ensure this is from `std::format!` specifically + if expn_data.macro_def_id != cx.tcx.get_diagnostic_item(sym::format_macro) { + return; // Not a std::format! macro call + } + + span_lint_and_sugg( + cx, + FORMAT_CONCAT_ARGS, + expr.span, + "this `format!(...)` invocation might be replaceable with `concat!(...)`", + "consider using concat! if all arguments are constant", + "concat!(...)".to_string(), + rustc_errors::Applicability::HasPlaceholders, + ); + } + } +} + +#[cfg(not(feature = "rlib"))] +#[allow(unused_extern_crates)] +extern crate rustc_driver; + +#[cfg(not(feature = "rlib"))] +#[allow(clippy::float_arithmetic, clippy::option_option, clippy::unreachable)] +fn main() { + dylint_linting::test(env!("CARGO_PKG_NAME"), &[]); +} \ No newline at end of file diff --git a/examples/supplementary/format_concat_args/test_lint.rs b/examples/supplementary/format_concat_args/test_lint.rs new file mode 100644 index 000000000..cc56b5797 --- /dev/null +++ b/examples/supplementary/format_concat_args/test_lint.rs @@ -0,0 +1,10 @@ +// for testing, +fn main() { + // Should trigger the lint + let _s1 = format!("simple string"); + let _s2 = format!("hello {}", "world"); + + // Should not trigger (not format!) + let _s3 = "simple string".to_string(); + println!("hello {}", "world"); +} \ No newline at end of file diff --git a/examples/supplementary/format_concat_args/ui/main.rs b/examples/supplementary/format_concat_args/ui/main.rs new file mode 100644 index 000000000..afc4deefd --- /dev/null +++ b/examples/supplementary/format_concat_args/ui/main.rs @@ -0,0 +1,62 @@ +const MY_CONST_STR: &str = "const_str"; +const MY_CONST_INT: i32 = 123; + +fn main() { + // Cases that SHOULD trigger the lint + format!("simple string without placeholders"); + format!("hello {}", "world"); + format!("number is {}, string is {}", 123, "test"); + format!("number is {}, string is {}", MY_CONST_INT, MY_CONST_STR); + format!("{}", "only a string literal"); + format!("{}{}{}", "a", "b", "c"); + format!("hello {}", MY_CONST_STR); + format!("{}/file.txt", env!("CARGO_MANIFEST_DIR")); + format!("literal {} literal {} literal", "arg1", "arg2"); + + // Cases that SHOULD NOT trigger the lint + let s_var: String = "string_var".to_string(); + format!("hello {}", s_var); + + #[derive(Debug)] + struct Foo; + format!("debug {:?}", Foo); + format!("debug {:?}", MY_CONST_STR); + format!("hex: {:x}", MY_CONST_INT); + + format!("hello {name}", name = "world_named"); + format!("hello {name}", name = MY_CONST_STR); + + println!("this is not format!"); + + let dynamic = std::env::var("PATH").unwrap_or_default(); + format!("Path is {}", dynamic); + + // Edge case: format! with only a literal, no placeholders, no arguments beyond the format string. + let _ = format!("just a literal"); + + // Empty format string + format!(""); // Suggests concat!("") + + // Format string with escape sequences + format!("hello\n{}", "world_newline"); + format!("path: C:\\folder\\{}", "file.txt"); + format!("quotes \"here\" and {}", "there_quotes"); + format!("{{hello}} {}", "braces"); + + // Format string with only placeholders + format!("{}{}", "first", "second"); +} + +// Test with a function call that is const +const fn get_const_string() -> &'static str { + "from_const_fn" +} + +fn test_const_fn_arg() { + format!("Const fn says: {}", get_const_string()); +} + +// Test with different literal types +fn test_literal_types() { + format!("Int: {}, Float: {}, Bool: {}", 10, 3.14f32, true); +} \ No newline at end of file diff --git a/examples/supplementary/format_concat_args/ui/main.stderr b/examples/supplementary/format_concat_args/ui/main.stderr new file mode 100644 index 000000000..e69de29bb From ab1185ceaac4a1853765f2ce60e84cf9ceacaf29 Mon Sep 17 00:00:00 2001 From: Ginza Hatemi Date: Thu, 15 May 2025 08:31:24 -0600 Subject: [PATCH 2/6] fix `cargo.toml` file --- .../format_concat_args/Cargo.toml | 14 +++++--- .../format_concat_args/README.md | 35 ------------------- .../format_concat_args/src/lib.rs | 5 +-- 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/examples/supplementary/format_concat_args/Cargo.toml b/examples/supplementary/format_concat_args/Cargo.toml index 0708f2a41..24d660fb5 100644 --- a/examples/supplementary/format_concat_args/Cargo.toml +++ b/examples/supplementary/format_concat_args/Cargo.toml @@ -7,14 +7,20 @@ edition = "2021" publish = false [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] -clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "238edf273d195c8e472851ebd60571f77f978ac8" } -dylint_linting = "4.0.1" +clippy_utils = { workspace = true } +dylint_linting = { path = "../../../utils/linting" } [dev-dependencies] -dylint_testing = "4.0.1" +dylint_testing = { path = "../../../utils/testing" } + +[features] +rlib = ["dylint_linting/constituent"] + +[lints] +workspace = true [package.metadata.rust-analyzer] rustc_private = true diff --git a/examples/supplementary/format_concat_args/README.md b/examples/supplementary/format_concat_args/README.md index 8b081b9d1..7a617bbfa 100644 --- a/examples/supplementary/format_concat_args/README.md +++ b/examples/supplementary/format_concat_args/README.md @@ -1,47 +1,12 @@ # format_concat_args -A Dylint lint to suggest using `concat!(...)` instead of `format!(...)` when all format arguments are constant. - -This lint identifies instances where `format!` calls could be replaced with more efficient `concat!` calls when all arguments are constant and use the `Display` trait. ## Examples -```rust -// Will trigger the lint: -let s = format!("Hello {}", "world"); // Could be: concat!("Hello ", "world") - -// Will not trigger the lint: -let name = "Bob"; -let s = format!("Hello {}", name); // Not constant - -// Will not trigger the lint: -let s = format!("Value: {:?}", 42); // Not using Display trait -``` ## Building and Testing -To build and test this lint: - -1. Make sure you have Dylint installed: - ```sh - cargo install dylint-link - ``` - -2. Build the lint: - ```sh - cargo build - ``` - -3. To run the lint against your own code: - ```sh - DYLINT_LIBRARY_PATH=/path/to/target/debug cargo dylint format_concat_args - ``` - ## Limitations -- The current implementation provides a simple detection of `format!` calls but doesn't implement the full type checking and argument analysis. -- Works only with `std::format!` macros, not with other formatting macros like `println!` or `write!`. - ## References -- [GitHub Issue #1601](https://github.com/trailofbits/dylint/issues/1601) diff --git a/examples/supplementary/format_concat_args/src/lib.rs b/examples/supplementary/format_concat_args/src/lib.rs index bd05ebc2c..bb36e6883 100644 --- a/examples/supplementary/format_concat_args/src/lib.rs +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -16,7 +16,7 @@ use clippy_utils::{ is_expn_of, }; use rustc_hir::Expr; -use rustc_lint::{Lint, LateContext, LateLintPass, LintContext}; +use rustc_lint::{Lint, LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::sym; @@ -63,9 +63,6 @@ impl<'tcx> LateLintPass<'tcx> for FormatConcatArgs { #[cfg(not(feature = "rlib"))] #[allow(unused_extern_crates)] -extern crate rustc_driver; - -#[cfg(not(feature = "rlib"))] #[allow(clippy::float_arithmetic, clippy::option_option, clippy::unreachable)] fn main() { dylint_linting::test(env!("CARGO_PKG_NAME"), &[]); From e69fce92e9387a11c96629ad907d915e68332bb0 Mon Sep 17 00:00:00 2001 From: Ginza Hatemi Date: Thu, 15 May 2025 05:53:22 -0600 Subject: [PATCH 3/6] Add `format_concat_args` lint to detect `format!()` calls replaceable with `concat!()` --- examples/supplementary/Cargo.lock | 144 ++++++++++++++---- .../format_concat_args/.cargo/config.toml | 6 + .../format_concat_args/.gitignore | 1 + .../format_concat_args/Cargo.toml | 20 +++ .../format_concat_args/README.md | 47 ++++++ .../format_concat_args/rust-toolchain | 3 + .../format_concat_args/src/lib.rs | 72 +++++++++ .../format_concat_args/test_lint.rs | 10 ++ .../format_concat_args/ui/main.rs | 62 ++++++++ .../format_concat_args/ui/main.stderr | 0 10 files changed, 339 insertions(+), 26 deletions(-) create mode 100644 examples/supplementary/format_concat_args/.cargo/config.toml create mode 100644 examples/supplementary/format_concat_args/.gitignore create mode 100644 examples/supplementary/format_concat_args/Cargo.toml create mode 100644 examples/supplementary/format_concat_args/README.md create mode 100644 examples/supplementary/format_concat_args/rust-toolchain create mode 100644 examples/supplementary/format_concat_args/src/lib.rs create mode 100644 examples/supplementary/format_concat_args/test_lint.rs create mode 100644 examples/supplementary/format_concat_args/ui/main.rs create mode 100644 examples/supplementary/format_concat_args/ui/main.stderr diff --git a/examples/supplementary/Cargo.lock b/examples/supplementary/Cargo.lock index 51195e001..a8733a0a9 100644 --- a/examples/supplementary/Cargo.lock +++ b/examples/supplementary/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.18" @@ -206,8 +215,8 @@ name = "commented_out_code" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "regex", "syn", ] @@ -349,7 +358,26 @@ dependencies = [ "anstyle", "anyhow", "cargo_metadata 0.19.2", - "dylint_internal", + "dylint_internal 4.1.0", + "log", + "once_cell", + "semver", + "serde", + "serde_json", + "tempfile", + "walkdir", +] + +[[package]] +name = "dylint" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288a5390c058d39da1ca1d952b712798904a1a48b10ebd0fa53c86ed90783965" +dependencies = [ + "ansi_term", + "anyhow", + "cargo_metadata 0.19.2", + "dylint_internal 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "log", "once_cell", "semver", @@ -379,12 +407,34 @@ dependencies = [ "toml", ] +[[package]] +name = "dylint_internal" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258d36f53de789719a4c97ecde1df80c16abbcd029c5d25f19c86164caa54cff" +dependencies = [ + "ansi_term", + "anyhow", + "bitflags", + "cargo_metadata 0.19.2", + "git2", + "home", + "if_chain", + "log", + "regex", + "rust-embed", + "rustversion", + "serde", + "thiserror 2.0.12", + "toml", +] + [[package]] name = "dylint_linting" version = "4.1.0" dependencies = [ "cargo_metadata 0.19.2", - "dylint_internal", + "dylint_internal 4.1.0", "paste", "rustversion", "serde", @@ -392,15 +442,48 @@ dependencies = [ "toml", ] +[[package]] +name = "dylint_linting" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3293a3baa87904faeea12506ec152b56655e40fc49709d905a9be6d277b56c8e" +dependencies = [ + "cargo_metadata 0.19.2", + "dylint_internal 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "paste", + "rustversion", + "serde", + "thiserror 2.0.12", + "toml", +] + +[[package]] +name = "dylint_testing" +version = "4.1.0" +dependencies = [ + "anyhow", + "cargo_metadata 0.19.2", + "compiletest_rs", + "dylint 4.1.0", + "dylint_internal 4.1.0", + "env_logger", + "once_cell", + "regex", + "serde_json", + "tempfile", +] + [[package]] name = "dylint_testing" version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c6119c632fc3bdb0d38c2ad41b6d417e5dfc535339850237d461a8725e1651" dependencies = [ "anyhow", "cargo_metadata 0.19.2", "compiletest_rs", - "dylint", - "dylint_internal", + "dylint 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dylint_internal 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger", "once_cell", "regex", @@ -460,8 +543,8 @@ dependencies = [ "cargo-util", "cargo_metadata 0.18.1", "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "once_cell", "pulldown-cmark", ] @@ -493,6 +576,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "format_concat_args" +version = "0.1.0" +dependencies = [ + "clippy_utils", + "dylint_linting 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dylint_testing 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -756,8 +848,8 @@ name = "inconsistent_struct_pattern" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", ] [[package]] @@ -905,9 +997,9 @@ name = "local_ref_cell" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_internal", - "dylint_linting", - "dylint_testing", + "dylint_internal 4.1.0", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", ] [[package]] @@ -937,8 +1029,8 @@ version = "4.1.0" dependencies = [ "cargo_metadata 0.18.1", "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "regex", ] @@ -1083,8 +1175,8 @@ name = "redundant_reference" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "serde", ] @@ -1298,7 +1390,7 @@ name = "supplementary" version = "4.1.0" dependencies = [ "commented_out_code", - "dylint_linting", + "dylint_linting 4.1.0", "escaping_doc_link", "inconsistent_struct_pattern", "local_ref_cell", @@ -1507,8 +1599,8 @@ name = "unnamed_constant" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_linting", - "dylint_testing", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "serde", ] @@ -1517,9 +1609,9 @@ name = "unnecessary_borrow_mut" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_internal", - "dylint_linting", - "dylint_testing", + "dylint_internal 4.1.0", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", ] [[package]] @@ -1527,9 +1619,9 @@ name = "unnecessary_conversion_for_trait" version = "4.1.0" dependencies = [ "clippy_utils", - "dylint_internal", - "dylint_linting", - "dylint_testing", + "dylint_internal 4.1.0", + "dylint_linting 4.1.0", + "dylint_testing 4.1.0", "tempfile", ] diff --git a/examples/supplementary/format_concat_args/.cargo/config.toml b/examples/supplementary/format_concat_args/.cargo/config.toml new file mode 100644 index 000000000..226eca535 --- /dev/null +++ b/examples/supplementary/format_concat_args/.cargo/config.toml @@ -0,0 +1,6 @@ +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] + +# For Rust versions 1.74.0 and onward, the following alternative can be used +# (see https://github.com/rust-lang/cargo/pull/12535): +# linker = "dylint-link" diff --git a/examples/supplementary/format_concat_args/.gitignore b/examples/supplementary/format_concat_args/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/examples/supplementary/format_concat_args/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/supplementary/format_concat_args/Cargo.toml b/examples/supplementary/format_concat_args/Cargo.toml new file mode 100644 index 000000000..0708f2a41 --- /dev/null +++ b/examples/supplementary/format_concat_args/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "format_concat_args" +version = "0.1.0" +authors = ["Your Name "] +description = "A Dylint lint to suggest using `concat!(...)` instead of `format!(...)` for all-constant Display arguments." +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "238edf273d195c8e472851ebd60571f77f978ac8" } +dylint_linting = "4.0.1" + +[dev-dependencies] +dylint_testing = "4.0.1" + +[package.metadata.rust-analyzer] +rustc_private = true diff --git a/examples/supplementary/format_concat_args/README.md b/examples/supplementary/format_concat_args/README.md new file mode 100644 index 000000000..8b081b9d1 --- /dev/null +++ b/examples/supplementary/format_concat_args/README.md @@ -0,0 +1,47 @@ +# format_concat_args + +A Dylint lint to suggest using `concat!(...)` instead of `format!(...)` when all format arguments are constant. + +This lint identifies instances where `format!` calls could be replaced with more efficient `concat!` calls when all arguments are constant and use the `Display` trait. + +## Examples + +```rust +// Will trigger the lint: +let s = format!("Hello {}", "world"); // Could be: concat!("Hello ", "world") + +// Will not trigger the lint: +let name = "Bob"; +let s = format!("Hello {}", name); // Not constant + +// Will not trigger the lint: +let s = format!("Value: {:?}", 42); // Not using Display trait +``` + +## Building and Testing + +To build and test this lint: + +1. Make sure you have Dylint installed: + ```sh + cargo install dylint-link + ``` + +2. Build the lint: + ```sh + cargo build + ``` + +3. To run the lint against your own code: + ```sh + DYLINT_LIBRARY_PATH=/path/to/target/debug cargo dylint format_concat_args + ``` + +## Limitations + +- The current implementation provides a simple detection of `format!` calls but doesn't implement the full type checking and argument analysis. +- Works only with `std::format!` macros, not with other formatting macros like `println!` or `write!`. + +## References + +- [GitHub Issue #1601](https://github.com/trailofbits/dylint/issues/1601) diff --git a/examples/supplementary/format_concat_args/rust-toolchain b/examples/supplementary/format_concat_args/rust-toolchain new file mode 100644 index 000000000..f898a7a72 --- /dev/null +++ b/examples/supplementary/format_concat_args/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2025-02-20" +components = ["llvm-tools-preview", "rustc-dev"] diff --git a/examples/supplementary/format_concat_args/src/lib.rs b/examples/supplementary/format_concat_args/src/lib.rs new file mode 100644 index 000000000..bd05ebc2c --- /dev/null +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -0,0 +1,72 @@ +#![feature(rustc_private)] + +extern crate rustc_ast; +extern crate rustc_errors; +extern crate rustc_hir; +extern crate rustc_lint; +extern crate rustc_middle; +extern crate rustc_session; +extern crate rustc_span; + +#[cfg(not(feature = "rlib"))] +dylint_linting::dylint_library!(); + +use clippy_utils::{ + diagnostics::span_lint_and_sugg, + is_expn_of, +}; +use rustc_hir::Expr; +use rustc_lint::{Lint, LateContext, LateLintPass, LintContext}; +use rustc_session::declare_lint_pass; +use rustc_span::sym; + +// Declare the lint directly +pub static FORMAT_CONCAT_ARGS: &Lint = &Lint { + name: "format_concat_args", + default_level: rustc_lint::Level::Allow, + desc: "Checks for `format!(...)` invocations where `concat!(...)` could be used instead.", + edition_lint_opts: None, + report_in_external_macro: true, + future_incompatible: None, + is_externally_loaded: false, + eval_always: false, + feature_gate: None, + crate_level_only: false, +}; + +// Declare the lint pass +declare_lint_pass!(FormatConcatArgs => [FORMAT_CONCAT_ARGS]); + +impl<'tcx> LateLintPass<'tcx> for FormatConcatArgs { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + // Check if the expression is a `format!` macro invocation + if is_expn_of(expr.span, &sym::format_args.as_str()).is_some() { + let expn_data = expr.span.ctxt().outer_expn_data(); + + // Ensure this is from `std::format!` specifically + if expn_data.macro_def_id != cx.tcx.get_diagnostic_item(sym::format_macro) { + return; // Not a std::format! macro call + } + + span_lint_and_sugg( + cx, + FORMAT_CONCAT_ARGS, + expr.span, + "this `format!(...)` invocation might be replaceable with `concat!(...)`", + "consider using concat! if all arguments are constant", + "concat!(...)".to_string(), + rustc_errors::Applicability::HasPlaceholders, + ); + } + } +} + +#[cfg(not(feature = "rlib"))] +#[allow(unused_extern_crates)] +extern crate rustc_driver; + +#[cfg(not(feature = "rlib"))] +#[allow(clippy::float_arithmetic, clippy::option_option, clippy::unreachable)] +fn main() { + dylint_linting::test(env!("CARGO_PKG_NAME"), &[]); +} \ No newline at end of file diff --git a/examples/supplementary/format_concat_args/test_lint.rs b/examples/supplementary/format_concat_args/test_lint.rs new file mode 100644 index 000000000..cc56b5797 --- /dev/null +++ b/examples/supplementary/format_concat_args/test_lint.rs @@ -0,0 +1,10 @@ +// for testing, +fn main() { + // Should trigger the lint + let _s1 = format!("simple string"); + let _s2 = format!("hello {}", "world"); + + // Should not trigger (not format!) + let _s3 = "simple string".to_string(); + println!("hello {}", "world"); +} \ No newline at end of file diff --git a/examples/supplementary/format_concat_args/ui/main.rs b/examples/supplementary/format_concat_args/ui/main.rs new file mode 100644 index 000000000..afc4deefd --- /dev/null +++ b/examples/supplementary/format_concat_args/ui/main.rs @@ -0,0 +1,62 @@ +const MY_CONST_STR: &str = "const_str"; +const MY_CONST_INT: i32 = 123; + +fn main() { + // Cases that SHOULD trigger the lint + format!("simple string without placeholders"); + format!("hello {}", "world"); + format!("number is {}, string is {}", 123, "test"); + format!("number is {}, string is {}", MY_CONST_INT, MY_CONST_STR); + format!("{}", "only a string literal"); + format!("{}{}{}", "a", "b", "c"); + format!("hello {}", MY_CONST_STR); + format!("{}/file.txt", env!("CARGO_MANIFEST_DIR")); + format!("literal {} literal {} literal", "arg1", "arg2"); + + // Cases that SHOULD NOT trigger the lint + let s_var: String = "string_var".to_string(); + format!("hello {}", s_var); + + #[derive(Debug)] + struct Foo; + format!("debug {:?}", Foo); + format!("debug {:?}", MY_CONST_STR); + format!("hex: {:x}", MY_CONST_INT); + + format!("hello {name}", name = "world_named"); + format!("hello {name}", name = MY_CONST_STR); + + println!("this is not format!"); + + let dynamic = std::env::var("PATH").unwrap_or_default(); + format!("Path is {}", dynamic); + + // Edge case: format! with only a literal, no placeholders, no arguments beyond the format string. + let _ = format!("just a literal"); + + // Empty format string + format!(""); // Suggests concat!("") + + // Format string with escape sequences + format!("hello\n{}", "world_newline"); + format!("path: C:\\folder\\{}", "file.txt"); + format!("quotes \"here\" and {}", "there_quotes"); + format!("{{hello}} {}", "braces"); + + // Format string with only placeholders + format!("{}{}", "first", "second"); +} + +// Test with a function call that is const +const fn get_const_string() -> &'static str { + "from_const_fn" +} + +fn test_const_fn_arg() { + format!("Const fn says: {}", get_const_string()); +} + +// Test with different literal types +fn test_literal_types() { + format!("Int: {}, Float: {}, Bool: {}", 10, 3.14f32, true); +} \ No newline at end of file diff --git a/examples/supplementary/format_concat_args/ui/main.stderr b/examples/supplementary/format_concat_args/ui/main.stderr new file mode 100644 index 000000000..e69de29bb From e3d7e3195340728f09cf046663ade5be61ec3a95 Mon Sep 17 00:00:00 2001 From: Ginza Hatemi Date: Thu, 15 May 2025 08:31:24 -0600 Subject: [PATCH 4/6] fix `cargo.toml` file --- .../format_concat_args/Cargo.toml | 14 +++++--- .../format_concat_args/README.md | 35 ------------------- .../format_concat_args/src/lib.rs | 5 +-- 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/examples/supplementary/format_concat_args/Cargo.toml b/examples/supplementary/format_concat_args/Cargo.toml index 0708f2a41..24d660fb5 100644 --- a/examples/supplementary/format_concat_args/Cargo.toml +++ b/examples/supplementary/format_concat_args/Cargo.toml @@ -7,14 +7,20 @@ edition = "2021" publish = false [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] -clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "238edf273d195c8e472851ebd60571f77f978ac8" } -dylint_linting = "4.0.1" +clippy_utils = { workspace = true } +dylint_linting = { path = "../../../utils/linting" } [dev-dependencies] -dylint_testing = "4.0.1" +dylint_testing = { path = "../../../utils/testing" } + +[features] +rlib = ["dylint_linting/constituent"] + +[lints] +workspace = true [package.metadata.rust-analyzer] rustc_private = true diff --git a/examples/supplementary/format_concat_args/README.md b/examples/supplementary/format_concat_args/README.md index 8b081b9d1..7a617bbfa 100644 --- a/examples/supplementary/format_concat_args/README.md +++ b/examples/supplementary/format_concat_args/README.md @@ -1,47 +1,12 @@ # format_concat_args -A Dylint lint to suggest using `concat!(...)` instead of `format!(...)` when all format arguments are constant. - -This lint identifies instances where `format!` calls could be replaced with more efficient `concat!` calls when all arguments are constant and use the `Display` trait. ## Examples -```rust -// Will trigger the lint: -let s = format!("Hello {}", "world"); // Could be: concat!("Hello ", "world") - -// Will not trigger the lint: -let name = "Bob"; -let s = format!("Hello {}", name); // Not constant - -// Will not trigger the lint: -let s = format!("Value: {:?}", 42); // Not using Display trait -``` ## Building and Testing -To build and test this lint: - -1. Make sure you have Dylint installed: - ```sh - cargo install dylint-link - ``` - -2. Build the lint: - ```sh - cargo build - ``` - -3. To run the lint against your own code: - ```sh - DYLINT_LIBRARY_PATH=/path/to/target/debug cargo dylint format_concat_args - ``` - ## Limitations -- The current implementation provides a simple detection of `format!` calls but doesn't implement the full type checking and argument analysis. -- Works only with `std::format!` macros, not with other formatting macros like `println!` or `write!`. - ## References -- [GitHub Issue #1601](https://github.com/trailofbits/dylint/issues/1601) diff --git a/examples/supplementary/format_concat_args/src/lib.rs b/examples/supplementary/format_concat_args/src/lib.rs index bd05ebc2c..bb36e6883 100644 --- a/examples/supplementary/format_concat_args/src/lib.rs +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -16,7 +16,7 @@ use clippy_utils::{ is_expn_of, }; use rustc_hir::Expr; -use rustc_lint::{Lint, LateContext, LateLintPass, LintContext}; +use rustc_lint::{Lint, LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::sym; @@ -63,9 +63,6 @@ impl<'tcx> LateLintPass<'tcx> for FormatConcatArgs { #[cfg(not(feature = "rlib"))] #[allow(unused_extern_crates)] -extern crate rustc_driver; - -#[cfg(not(feature = "rlib"))] #[allow(clippy::float_arithmetic, clippy::option_option, clippy::unreachable)] fn main() { dylint_linting::test(env!("CARGO_PKG_NAME"), &[]); From 6c7ab327ef33b89b8daaaf37b0770b7c228e7373 Mon Sep 17 00:00:00 2001 From: Ginza Hatemi Date: Thu, 15 May 2025 09:29:54 -0600 Subject: [PATCH 5/6] fixing lint errors --- examples/supplementary/format_concat_args/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/supplementary/format_concat_args/src/lib.rs b/examples/supplementary/format_concat_args/src/lib.rs index bb36e6883..30f8be720 100644 --- a/examples/supplementary/format_concat_args/src/lib.rs +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -16,14 +16,14 @@ use clippy_utils::{ is_expn_of, }; use rustc_hir::Expr; -use rustc_lint::{Lint, LateContext, LateLintPass}; +use rustc_lint::{Lint, LateContext, LateLintPass, Level}; use rustc_session::declare_lint_pass; use rustc_span::sym; // Declare the lint directly pub static FORMAT_CONCAT_ARGS: &Lint = &Lint { name: "format_concat_args", - default_level: rustc_lint::Level::Allow, + default_level: Level::Allow, desc: "Checks for `format!(...)` invocations where `concat!(...)` could be used instead.", edition_lint_opts: None, report_in_external_macro: true, @@ -40,7 +40,7 @@ declare_lint_pass!(FormatConcatArgs => [FORMAT_CONCAT_ARGS]); impl<'tcx> LateLintPass<'tcx> for FormatConcatArgs { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { // Check if the expression is a `format!` macro invocation - if is_expn_of(expr.span, &sym::format_args.as_str()).is_some() { + if is_expn_of(expr.span, sym::format_args.as_str()).is_some() { let expn_data = expr.span.ctxt().outer_expn_data(); // Ensure this is from `std::format!` specifically From 5aded76b2117b6a69144cd82b545c0966b0560b1 Mon Sep 17 00:00:00 2001 From: Ginza Hatemi Date: Thu, 15 May 2025 10:01:15 -0600 Subject: [PATCH 6/6] fixed some clippy issues --- examples/supplementary/format_concat_args/Cargo.toml | 2 +- examples/supplementary/format_concat_args/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/supplementary/format_concat_args/Cargo.toml b/examples/supplementary/format_concat_args/Cargo.toml index 24d660fb5..e03702f0d 100644 --- a/examples/supplementary/format_concat_args/Cargo.toml +++ b/examples/supplementary/format_concat_args/Cargo.toml @@ -10,7 +10,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -clippy_utils = { workspace = true } +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "7bb54d91be1af212faaa078786c1d2271a67d4f9" } dylint_linting = { path = "../../../utils/linting" } [dev-dependencies] diff --git a/examples/supplementary/format_concat_args/src/lib.rs b/examples/supplementary/format_concat_args/src/lib.rs index bb36e6883..30f8be720 100644 --- a/examples/supplementary/format_concat_args/src/lib.rs +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -16,14 +16,14 @@ use clippy_utils::{ is_expn_of, }; use rustc_hir::Expr; -use rustc_lint::{Lint, LateContext, LateLintPass}; +use rustc_lint::{Lint, LateContext, LateLintPass, Level}; use rustc_session::declare_lint_pass; use rustc_span::sym; // Declare the lint directly pub static FORMAT_CONCAT_ARGS: &Lint = &Lint { name: "format_concat_args", - default_level: rustc_lint::Level::Allow, + default_level: Level::Allow, desc: "Checks for `format!(...)` invocations where `concat!(...)` could be used instead.", edition_lint_opts: None, report_in_external_macro: true, @@ -40,7 +40,7 @@ declare_lint_pass!(FormatConcatArgs => [FORMAT_CONCAT_ARGS]); impl<'tcx> LateLintPass<'tcx> for FormatConcatArgs { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { // Check if the expression is a `format!` macro invocation - if is_expn_of(expr.span, &sym::format_args.as_str()).is_some() { + if is_expn_of(expr.span, sym::format_args.as_str()).is_some() { let expn_data = expr.span.ctxt().outer_expn_data(); // Ensure this is from `std::format!` specifically