-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add new lint paths_from_format
#8833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4596d61
6c2f56f
5a6ca6b
47e4a45
7980350
1a35d49
29384d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,132 @@ | ||||||||
use clippy_utils::diagnostics::span_lint_and_sugg; | ||||||||
use clippy_utils::macros::{root_macro_call, FormatArgsExpn}; | ||||||||
use clippy_utils::sugg::Sugg; | ||||||||
use clippy_utils::ty::is_type_diagnostic_item; | ||||||||
use rustc_errors::Applicability; | ||||||||
use rustc_hir::{Expr, ExprKind}; | ||||||||
use rustc_lint::{LateContext, LateLintPass}; | ||||||||
use rustc_session::{declare_lint_pass, declare_tool_lint}; | ||||||||
use rustc_span::sym; | ||||||||
use std::fmt::Write as _; | ||||||||
use std::path::Path; | ||||||||
|
||||||||
declare_clippy_lint! { | ||||||||
/// ### What it does | ||||||||
/// Checks for `PathBuf::from(format!(..))` calls. | ||||||||
/// | ||||||||
/// ### Why is this bad? | ||||||||
/// It is not OS-agnostic, and can be harder to read. | ||||||||
/// | ||||||||
/// ### Known Problems | ||||||||
/// `.join()` introduces additional allocations that are not present when `PathBuf::push` is | ||||||||
/// used instead. | ||||||||
Comment on lines
+21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at this, I think it would be better to suggest Though, in that case we should have the applicability be less than machine applicable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, to clarify, you mean you want to do some or all of the following?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, if my comment wasn't clear. I mean a combination of the first two. So suggest Does this make sense, or should I try to describe it better? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm looking at the docs for
Functionally equivalent to
Will you be ok with this? Given it should still be machine applicable but I don't think it will have the same problems as before. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You have to be careful when using the array as all the values have to be the same type. |
||||||||
/// | ||||||||
/// ### Example | ||||||||
/// ```rust | ||||||||
/// use std::path::PathBuf; | ||||||||
/// let base_path = "/base"; | ||||||||
/// PathBuf::from(format!("{}/foo/bar", base_path)); | ||||||||
/// ``` | ||||||||
/// Use instead: | ||||||||
/// ```rust | ||||||||
/// use std::path::Path; | ||||||||
/// let base_path = "/base"; | ||||||||
/// Path::new(&base_path).join("foo").join("bar"); | ||||||||
/// ``` | ||||||||
#[clippy::version = "1.62.0"] | ||||||||
pub PATHS_FROM_FORMAT, | ||||||||
pedantic, | ||||||||
"builds a `PathBuf` from a format macro" | ||||||||
} | ||||||||
|
||||||||
declare_lint_pass!(PathsFromFormat => [PATHS_FROM_FORMAT]); | ||||||||
|
||||||||
impl<'tcx> LateLintPass<'tcx> for PathsFromFormat { | ||||||||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { | ||||||||
if_chain! { | ||||||||
if let ExprKind::Call(_, [arg, ..]) = expr.kind; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now, this lint only checks for function calls, where the output is a The logic should also contain a check if the code originated from a macro. I believe the check should be as simple as |
||||||||
if let ty = cx.typeck_results().expr_ty(expr); | ||||||||
if is_type_diagnostic_item(cx, ty, sym::PathBuf); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we maybe also check for calls to |
||||||||
if let Some(macro_call) = root_macro_call(arg.span); | ||||||||
if cx.tcx.item_name(macro_call.def_id) == sym::format; | ||||||||
if let Some(format_args) = FormatArgsExpn::find_nested(cx, arg, macro_call.expn); | ||||||||
then { | ||||||||
let format_string_parts = format_args.format_string.parts; | ||||||||
let format_value_args = format_args.args; | ||||||||
let string_parts: Vec<&str> = format_string_parts | ||||||||
.iter() | ||||||||
.map(rustc_span::Symbol::as_str) | ||||||||
.collect(); | ||||||||
let mut applicability = Applicability::MachineApplicable; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the applicability is explicitly set with the lint emission, you can remove this value and all usages of it. For |
||||||||
let real_vars: Vec<Sugg<'_>> = format_value_args | ||||||||
.iter() | ||||||||
.map(|x| Sugg::hir_with_applicability(cx, x.param.value, "..", &mut applicability)) | ||||||||
.collect(); | ||||||||
let mut paths_zip = string_parts.iter().take(real_vars.len()).zip(real_vars.clone()); | ||||||||
let mut sugg = String::new(); | ||||||||
if let Some((part, arg)) = paths_zip.next() { | ||||||||
if is_valid_use_case(string_parts.first().unwrap_or(&""), string_parts.get(1).unwrap_or(&"")) { | ||||||||
return; | ||||||||
} | ||||||||
if part.is_empty() { | ||||||||
sugg = format!("Path::new(&{arg})"); | ||||||||
} | ||||||||
else { | ||||||||
Comment on lines
+73
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Formatting nit, as well as the other else cases in the
Suggested change
|
||||||||
push_comps(&mut sugg, part); | ||||||||
let _ = write!(sugg, ".join(&{arg})"); | ||||||||
} | ||||||||
} | ||||||||
for n in 1..real_vars.len() { | ||||||||
if let Some((part, arg)) = paths_zip.next() { | ||||||||
if is_valid_use_case(string_parts.get(n).unwrap_or(&""), string_parts.get(n+1).unwrap_or(&"")) { | ||||||||
return; | ||||||||
} | ||||||||
else if n < real_vars.len() { | ||||||||
push_comps(&mut sugg, part); | ||||||||
let _ = write!(sugg, ".join(&{arg})"); | ||||||||
} | ||||||||
else { | ||||||||
sugg = format!("{sugg}.join(&{arg})"); | ||||||||
Comment on lines
+86
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason, why you use two different methods to append text to an existing string? |
||||||||
} | ||||||||
} | ||||||||
} | ||||||||
if real_vars.len() < string_parts.len() { | ||||||||
push_comps(&mut sugg, string_parts[real_vars.len()]); | ||||||||
} | ||||||||
span_lint_and_sugg( | ||||||||
cx, | ||||||||
PATHS_FROM_FORMAT, | ||||||||
expr.span, | ||||||||
"`format!(..)` used to form `PathBuf`", | ||||||||
"consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability", | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove the second part, as some users might not consider this more readable. The OS-agnostic part is also the main point, IMO. |
||||||||
sugg, | ||||||||
Applicability::MaybeIncorrect, | ||||||||
); | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
fn push_comps(string: &mut String, path: &str) { | ||||||||
let mut path = path.to_string(); | ||||||||
if !string.is_empty() { | ||||||||
path = path.trim_start_matches(|c| c == '\\' || c == '/').to_string(); | ||||||||
} | ||||||||
for n in Path::new(&path).components() { | ||||||||
let mut x = n.as_os_str().to_string_lossy().to_string(); | ||||||||
if string.is_empty() { | ||||||||
let _ = write!(string, "Path::new(\"{x}\")"); | ||||||||
} else { | ||||||||
x = x.trim_end_matches(|c| c == '/' || c == '\\').to_string(); | ||||||||
let _ = write!(string, ".join(\"{x}\")"); | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
// In essence, this checks if the format!() used is made for concatenation of the filenames / folder | ||||||||
// names itself. This returns true when it is something like | ||||||||
// `PathBuf::from(format!("/x/folder{}/textfile.txt", folder_number))` | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on the comment in the related issue, I would actually think that such a paht can still be problematic. I'd suggest either, checking if the path starts with |
||||||||
fn is_valid_use_case(string: &str, string2: &str) -> bool { | ||||||||
!(string.is_empty() || string.ends_with('/') || string.ends_with('\\')) | ||||||||
|| !(string2.is_empty() || string2.starts_with('/') || string2.starts_with('\\')) | ||||||||
} | ||||||||
merelymyself marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
#![warn(clippy::paths_from_format)] | ||
|
||
use std::path::PathBuf; | ||
|
||
fn main() { | ||
let mut base_path1 = ""; | ||
let mut base_path2 = ""; | ||
PathBuf::from(format!("{base_path1}/foo/bar")); | ||
PathBuf::from(format!("/foo/bar/{base_path1}")); | ||
PathBuf::from(format!("/foo/{base_path1}/bar")); | ||
PathBuf::from(format!("foo/{base_path1}/bar")); | ||
PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr")); | ||
PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr/{base_path2}")); | ||
PathBuf::from(format!("{base_path2}/foo/{base_path1}/bar")); | ||
PathBuf::from(format!("foo/{base_path1}a/bar")); | ||
PathBuf::from(format!("foo/a{base_path1}/bar")); | ||
PathBuf::from(format!(r"C:\{base_path2}\foo\{base_path1}\bar")); | ||
PathBuf::from(format!("C:\\{base_path2}\\foo\\{base_path1}\\bar")); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:8:5 | ||
| | ||
LL | PathBuf::from(format!("{base_path1}/foo/bar")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
= note: `-D clippy::paths-from-format` implied by `-D warnings` | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new(&base_path1).join("foo").join("bar"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:9:5 | ||
| | ||
LL | PathBuf::from(format!("/foo/bar/{base_path1}")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("/").join("foo").join("bar").join(&base_path1); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:10:5 | ||
| | ||
LL | PathBuf::from(format!("/foo/{base_path1}/bar")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("/").join("foo").join(&base_path1).join("bar"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:11:5 | ||
| | ||
LL | PathBuf::from(format!("foo/{base_path1}/bar")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("foo").join(&base_path1).join("bar"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:12:5 | ||
| | ||
LL | PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("foo").join("foooo").join(&base_path1).join("bar").join("barrr"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:13:5 | ||
| | ||
LL | PathBuf::from(format!("foo/foooo/{base_path1}/bar/barrr/{base_path2}")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("foo").join("foooo").join(&base_path1).join("bar").join("barrr").join(&base_path2); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:14:5 | ||
| | ||
LL | PathBuf::from(format!("{base_path2}/foo/{base_path1}/bar")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new(&base_path2).join("foo").join(&base_path1).join("bar"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:17:5 | ||
| | ||
LL | PathBuf::from(format!(r"C:/{base_path2}/foo/{base_path1}/bar")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("C:/").join(&base_path2).join("foo").join(&base_path1).join("bar"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: `format!(..)` used to form `PathBuf` | ||
--> $DIR/paths_from_format.rs:18:5 | ||
| | ||
LL | PathBuf::from(format!("C:/{base_path2}/foo/{base_path1}/bar")); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | ||
help: consider using `Path::new()` and `.join()` to make it OS-agnostic and improve code readability | ||
| | ||
LL | Path::new("C:/").join(&base_path2).join("foo").join(&base_path1).join("bar"); | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
error: aborting due to 9 previous errors | ||
|
Uh oh!
There was an error while loading. Please reload this page.