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..e03702f0d --- /dev/null +++ b/examples/supplementary/format_concat_args/Cargo.toml @@ -0,0 +1,26 @@ +[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", "rlib"] + +[dependencies] +clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "7bb54d91be1af212faaa078786c1d2271a67d4f9" } +dylint_linting = { path = "../../../utils/linting" } + +[dev-dependencies] +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 new file mode 100644 index 000000000..7a617bbfa --- /dev/null +++ b/examples/supplementary/format_concat_args/README.md @@ -0,0 +1,12 @@ +# format_concat_args + + +## Examples + + +## Building and Testing + +## Limitations + +## References + 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..30f8be720 --- /dev/null +++ b/examples/supplementary/format_concat_args/src/lib.rs @@ -0,0 +1,69 @@ +#![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, 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: 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)] +#[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