Skip to content

Commit b031924

Browse files
committed
add lint paths_from_format
1 parent 89d443a commit b031924

File tree

6 files changed

+249
-1
lines changed

6 files changed

+249
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4461,6 +4461,7 @@ Released 2018-09-13
44614461
[`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
44624462
[`partialeq_to_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_to_none
44634463
[`path_buf_push_overwrite`]: https://rust-lang.github.io/rust-clippy/master/index.html#path_buf_push_overwrite
4464+
[`paths_from_format`]: https://rust-lang.github.io/rust-clippy/master/index.html#paths_from_format
44644465
[`pattern_type_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#pattern_type_mismatch
44654466
[`permissions_set_readonly_false`]: https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false
44664467
[`positional_named_format_parameters`]: https://rust-lang.github.io/rust-clippy/master/index.html#positional_named_format_parameters

clippy_dev/src/update_lints.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ pub fn deprecate(name: &str, reason: Option<&String>) {
397397
let Some(lint) = lints.iter().find(|l| l.name == name_lower) else { eprintln!("error: failed to find lint `{name}`"); return; };
398398

399399
let mod_path = {
400-
let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module));
400+
let mut mod_path = Path::new("clippy_lints").join("src").join(&lint.module);
401401
if mod_path.is_dir() {
402402
mod_path = mod_path.join("mod");
403403
}

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ mod partial_pub_fields;
234234
mod partialeq_ne_impl;
235235
mod partialeq_to_none;
236236
mod pass_by_ref_or_value;
237+
mod paths_from_format;
237238
mod pattern_type_mismatch;
238239
mod permissions_set_readonly_false;
239240
mod precedence;
@@ -908,6 +909,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
908909
store.register_late_pass(|_| Box::new(fn_null_check::FnNullCheck));
909910
store.register_late_pass(|_| Box::new(permissions_set_readonly_false::PermissionsSetReadonlyFalse));
910911
store.register_late_pass(|_| Box::new(size_of_ref::SizeOfRef));
912+
store.register_late_pass(|_| Box::new(paths_from_format::PathsFromFormat));
911913
// add lints here, do not remove this comment, it's used in `new_lint`
912914
}
913915

clippy_lints/src/paths_from_format.rs

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::macros::{root_macro_call, FormatArgsExpn};
3+
use clippy_utils::sugg::Sugg;
4+
use clippy_utils::ty::is_type_diagnostic_item;
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{Expr, ExprKind};
7+
use rustc_lint::{LateContext, LateLintPass};
8+
use rustc_session::{declare_lint_pass, declare_tool_lint};
9+
use rustc_span::sym;
10+
use std::fmt::Write as _;
11+
use std::path::Path;
12+
13+
declare_clippy_lint! {
14+
/// ### What it does
15+
/// Checks for `PathBuf::from(format!(..))` calls.
16+
///
17+
/// ### Why is this bad?
18+
/// It is not OS-agnostic, and can be harder to read.
19+
///
20+
/// ### Known Problems
21+
/// `.join()` introduces additional allocations that are not present when `PathBuf::push` is
22+
/// used instead.
23+
///
24+
/// ### Example
25+
/// ```rust
26+
/// use std::path::PathBuf;
27+
/// let base_path = "/base";
28+
/// PathBuf::from(format!("{}/foo/bar", base_path));
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// use std::path::Path;
33+
/// let base_path = "/base";
34+
/// Path::new(&base_path).join("foo").join("bar");
35+
/// ```
36+
#[clippy::version = "1.62.0"]
37+
pub PATHS_FROM_FORMAT,
38+
pedantic,
39+
"builds a `PathBuf` from a format macro"
40+
}
41+
42+
declare_lint_pass!(PathsFromFormat => [PATHS_FROM_FORMAT]);
43+
44+
impl<'tcx> LateLintPass<'tcx> for PathsFromFormat {
45+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
46+
if_chain! {
47+
if let ExprKind::Call(_, args) = expr.kind;
48+
if let ty = cx.typeck_results().expr_ty(expr);
49+
if is_type_diagnostic_item(cx, ty, sym::PathBuf);
50+
if !args.is_empty();
51+
if let Some(macro_call) = root_macro_call(args[0].span);
52+
if cx.tcx.item_name(macro_call.def_id) == sym::format;
53+
if let Some(format_args) = FormatArgsExpn::find_nested(cx, &args[0], macro_call.expn);
54+
then {
55+
let format_string_parts = format_args.format_string.parts;
56+
let format_value_args = format_args.args;
57+
let string_parts: Vec<&str> = format_string_parts.iter().map(rustc_span::Symbol::as_str).collect();
58+
let mut applicability = Applicability::MachineApplicable;
59+
let real_vars: Vec<Sugg<'_>> = format_value_args.iter().map(|x| Sugg::hir_with_applicability(cx, x.param.value, "..", &mut applicability)).collect();
60+
let mut paths_zip = string_parts.iter().take(real_vars.len()).zip(real_vars.clone());
61+
let mut sugg = String::new();
62+
if let Some((part, arg)) = paths_zip.next() {
63+
if is_valid_use_case(string_parts.first().unwrap_or(&""), string_parts.get(1).unwrap_or(&"")) {
64+
return;
65+
}
66+
if part.is_empty() {
67+
sugg = format!("Path::new(&{arg})");
68+
}
69+
else {
70+
push_comps(&mut sugg, part);
71+
let _ = write!(sugg, ".join(&{arg})");
72+
}
73+
}
74+
for n in 1..real_vars.len() {
75+
if let Some((part, arg)) = paths_zip.next() {
76+
if is_valid_use_case(string_parts.get(n).unwrap_or(&""), string_parts.get(n+1).unwrap_or(&"")) {
77+
return;
78+
}
79+
else if n < real_vars.len() {
80+
push_comps(&mut sugg, part);
81+
let _ = write!(sugg, ".join(&{arg})");
82+
}
83+
else {
84+
sugg = format!("{sugg}.join(&{arg})");
85+
}
86+
}
87+
}
88+
if real_vars.len() < string_parts.len() {
89+
push_comps(&mut sugg, string_parts[real_vars.len()]);
90+
}
91+
span_lint_and_sugg(
92+
cx,
93+
PATHS_FROM_FORMAT,
94+
expr.span,
95+
"`format!(..)` used to form `PathBuf`",
96+
"consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability",
97+
sugg,
98+
Applicability::MaybeIncorrect,
99+
);
100+
}
101+
}
102+
}
103+
}
104+
105+
fn push_comps(string: &mut String, path: &str) {
106+
let mut path = path.to_string();
107+
if !string.is_empty() {
108+
path = path.trim_start_matches(|c| c == '\\' || c == '/').to_string();
109+
}
110+
for n in Path::new(&path).components() {
111+
let mut x = n.as_os_str().to_string_lossy().to_string();
112+
if string.is_empty() {
113+
let _ = write!(string, "Path::new(\"{x}\")");
114+
} else {
115+
x = x.trim_end_matches(|c| c == '/' || c == '\\').to_string();
116+
let _ = write!(string, ".join(\"{x}\")");
117+
}
118+
}
119+
}
120+
121+
fn is_valid_use_case(string: &str, string2: &str) -> bool {
122+
!(string.is_empty() || string.ends_with('/') || string.ends_with('\\'))
123+
|| !(string2.is_empty() || string2.starts_with('/') || string2.starts_with('\\'))
124+
}

tests/ui/paths_from_format.rs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![warn(clippy::paths_from_format)]
2+
3+
use std::path::PathBuf;
4+
5+
fn main() {
6+
let mut base_path1 = "";
7+
let mut base_path2 = "";
8+
PathBuf::from(format!("{base_path1}/foo/bar"));
9+
PathBuf::from(format!("/foo/bar/{base_path1}"));
10+
PathBuf::from(format!("/foo/{base_path1}/bar"));
11+
PathBuf::from(format!("foo/{base_path1}/bar"));
12+
PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr"));
13+
PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr/{base_path2}"));
14+
PathBuf::from(format!("{base_path2}/foo/{base_path1}/bar"));
15+
PathBuf::from(format!("foo/{base_path1}a/bar"));
16+
PathBuf::from(format!("foo/a{base_path1}/bar"));
17+
PathBuf::from(format!(r"C:\{base_path2}\foo\{base_path1}\bar"));
18+
PathBuf::from(format!("C:\\{base_path2}\\foo\\{base_path1}\\bar"));
19+
}

tests/ui/paths_from_format.stderr

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
error: `format!(..)` used to form `PathBuf`
2+
--> $DIR/paths_from_format.rs:8:5
3+
|
4+
LL | PathBuf::from(format!("{base_path1}/foo/bar"));
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::paths-from-format` implied by `-D warnings`
8+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
9+
|
10+
LL | Path::new(&base_path1).join("foo").join("bar");
11+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12+
13+
error: `format!(..)` used to form `PathBuf`
14+
--> $DIR/paths_from_format.rs:9:5
15+
|
16+
LL | PathBuf::from(format!("/foo/bar/{base_path1}"));
17+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18+
|
19+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
20+
|
21+
LL | Path::new("/").join("foo").join("bar").join(&base_path1);
22+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23+
24+
error: `format!(..)` used to form `PathBuf`
25+
--> $DIR/paths_from_format.rs:10:5
26+
|
27+
LL | PathBuf::from(format!("/foo/{base_path1}/bar"));
28+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29+
|
30+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
31+
|
32+
LL | Path::new("/").join("foo").join(&base_path1).join("bar");
33+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
34+
35+
error: `format!(..)` used to form `PathBuf`
36+
--> $DIR/paths_from_format.rs:11:5
37+
|
38+
LL | PathBuf::from(format!("foo/{base_path1}/bar"));
39+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40+
|
41+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
42+
|
43+
LL | Path::new("foo").join(&base_path1).join("bar");
44+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
45+
46+
error: `format!(..)` used to form `PathBuf`
47+
--> $DIR/paths_from_format.rs:12:5
48+
|
49+
LL | PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr"));
50+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51+
|
52+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
53+
|
54+
LL | Path::new("foo").join("foooo").join(&base_path1).join("bar").join("barrr");
55+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
56+
57+
error: `format!(..)` used to form `PathBuf`
58+
--> $DIR/paths_from_format.rs:13:5
59+
|
60+
LL | PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr/{base_path2}"));
61+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62+
|
63+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
64+
|
65+
LL | Path::new("foo").join("foooo").join(&base_path1).join("bar").join("barrr").join(&base_path2);
66+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67+
68+
error: `format!(..)` used to form `PathBuf`
69+
--> $DIR/paths_from_format.rs:14:5
70+
|
71+
LL | PathBuf::from(format!("{base_path2}/foo/{base_path1}/bar"));
72+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
73+
|
74+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
75+
|
76+
LL | Path::new(&base_path2).join("foo").join(&base_path1).join("bar");
77+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78+
79+
error: `format!(..)` used to form `PathBuf`
80+
--> $DIR/paths_from_format.rs:17:5
81+
|
82+
LL | PathBuf::from(format!(r"C:/{base_path2}/foo/{base_path1}/bar"));
83+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84+
|
85+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
86+
|
87+
LL | Path::new("C:/").join(&base_path2).join("foo").join(&base_path1).join("bar");
88+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89+
90+
error: `format!(..)` used to form `PathBuf`
91+
--> $DIR/paths_from_format.rs:18:5
92+
|
93+
LL | PathBuf::from(format!("C:/{base_path2}/foo/{base_path1}/bar"));
94+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
95+
|
96+
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability
97+
|
98+
LL | Path::new("C:/").join(&base_path2).join("foo").join(&base_path1).join("bar");
99+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
100+
101+
error: aborting due to 9 previous errors
102+

0 commit comments

Comments
 (0)