Skip to content

Commit fe92ccb

Browse files
committed
add lint paths_from_format
1 parent 09e6c23 commit fe92ccb

10 files changed

+276
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4133,6 +4133,7 @@ Released 2018-09-13
41334133
[`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
41344134
[`partialeq_to_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_to_none
41354135
[`path_buf_push_overwrite`]: https://rust-lang.github.io/rust-clippy/master/index.html#path_buf_push_overwrite
4136+
[`paths_from_format`]: https://rust-lang.github.io/rust-clippy/master/index.html#paths_from_format
41364137
[`pattern_type_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#pattern_type_mismatch
41374138
[`positional_named_format_parameters`]: https://rust-lang.github.io/rust-clippy/master/index.html#positional_named_format_parameters
41384139
[`possible_missing_comma`]: https://rust-lang.github.io/rust-clippy/master/index.html#possible_missing_comma

clippy_dev/src/update_lints.rs

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

421421
let mod_path = {
422-
let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module));
422+
let mut mod_path = Path::new("clippy_lints").join("src").join(&lint.module);
423423
if mod_path.is_dir() {
424424
mod_path = mod_path.join("mod");
425425
}

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ store.register_lints(&[
479479
partialeq_to_none::PARTIALEQ_TO_NONE,
480480
pass_by_ref_or_value::LARGE_TYPES_PASSED_BY_VALUE,
481481
pass_by_ref_or_value::TRIVIALLY_COPY_PASS_BY_REF,
482+
paths_from_format::PATHS_FROM_FORMAT,
482483
pattern_type_mismatch::PATTERN_TYPE_MISMATCH,
483484
precedence::PRECEDENCE,
484485
ptr::CMP_NULL,

clippy_lints/src/lib.register_pedantic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ store.register_group(true, "clippy::pedantic", Some("clippy_pedantic"), vec![
8282
LintId::of(operators::VERBOSE_BIT_MASK),
8383
LintId::of(pass_by_ref_or_value::LARGE_TYPES_PASSED_BY_VALUE),
8484
LintId::of(pass_by_ref_or_value::TRIVIALLY_COPY_PASS_BY_REF),
85+
LintId::of(paths_from_format::PATHS_FROM_FORMAT),
8586
LintId::of(ranges::RANGE_MINUS_ONE),
8687
LintId::of(ranges::RANGE_PLUS_ONE),
8788
LintId::of(redundant_else::REDUNDANT_ELSE),

clippy_lints/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ mod panic_unimplemented;
327327
mod partialeq_ne_impl;
328328
mod partialeq_to_none;
329329
mod pass_by_ref_or_value;
330+
mod paths_from_format;
330331
mod pattern_type_mismatch;
331332
mod precedence;
332333
mod ptr;
@@ -906,6 +907,9 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
906907
store.register_late_pass(|_| Box::new(bool_to_int_with_if::BoolToIntWithIf));
907908
store.register_late_pass(|_| Box::new(box_default::BoxDefault));
908909
store.register_late_pass(|_| Box::new(implicit_saturating_add::ImplicitSaturatingAdd));
910+
store.register_late_pass(|| Box::new(bool_to_int_with_if::BoolToIntWithIf));
911+
store.register_late_pass(|| Box::new(box_default::BoxDefault));
912+
store.register_late_pass(|| Box::new(paths_from_format::PathsFromFormat));
909913
// add lints here, do not remove this comment, it's used in `new_lint`
910914
}
911915

clippy_lints/src/paths_from_format.rs

Lines changed: 124 additions & 0 deletions
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+
}

src/docs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ docs! {
394394
"partialeq_ne_impl",
395395
"partialeq_to_none",
396396
"path_buf_push_overwrite",
397+
"paths_from_format",
397398
"pattern_type_mismatch",
398399
"possible_missing_comma",
399400
"precedence",

src/docs/paths_from_format.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
### What it does
2+
Checks for `PathBuf::from(format!(..))` calls.
3+
4+
### Why is this bad?
5+
It is not OS-agnostic, and can be harder to read.
6+
7+
### Known Problems
8+
`.join()` introduces additional allocations that are not present when `Pathbuf::push` is
9+
used instead.
10+
11+
### Example
12+
```
13+
use std::path::PathBuf;
14+
let base_path = "/base";
15+
PathBuf::from(format!("{}/foo/bar", base_path));
16+
```
17+
Use instead:
18+
```
19+
use std::path::Path;
20+
let base_path = "/base";
21+
Path::new(base_path).join("foo").join("bar");
22+
```

tests/ui/paths_from_format.rs

Lines changed: 19 additions & 0 deletions
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!("{}/foo/bar", base_path1));
9+
PathBuf::from(format!("/foo/bar/{}", base_path1));
10+
PathBuf::from(format!("/foo/{}/bar", base_path1));
11+
PathBuf::from(format!("foo/{}/bar", base_path1));
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

Lines changed: 102 additions & 0 deletions
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!("{}/foo/bar", base_path1));
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/{}/bar", base_path1));
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/{}/bar", base_path1));
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)