Skip to content
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

Add new lint paths_from_format #8833

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4656,6 +4656,7 @@ Released 2018-09-13
[`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
[`partialeq_to_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_to_none
[`path_buf_push_overwrite`]: https://rust-lang.github.io/rust-clippy/master/index.html#path_buf_push_overwrite
[`paths_from_format`]: https://rust-lang.github.io/rust-clippy/master/index.html#paths_from_format
[`pattern_type_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#pattern_type_mismatch
[`permissions_set_readonly_false`]: https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false
[`positional_named_format_parameters`]: https://rust-lang.github.io/rust-clippy/master/index.html#positional_named_format_parameters
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.

[There are over 550 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are over 600 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)

Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.
Expand Down
2 changes: 1 addition & 1 deletion book/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
A collection of lints to catch common mistakes and improve your
[Rust](https://github.com/rust-lang/rust) code.

[There are over 550 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are over 600 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)

Lints are divided into categories, each with a default [lint
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how
Expand Down
2 changes: 1 addition & 1 deletion clippy_dev/src/update_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ pub fn deprecate(name: &str, reason: Option<&String>) {
let Some(lint) = lints.iter().find(|l| l.name == name_lower) else { eprintln!("error: failed to find lint `{name}`"); return; };

let mod_path = {
let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module));
xFrednet marked this conversation as resolved.
Show resolved Hide resolved
let mut mod_path = Path::new("clippy_lints").join("src").join(&lint.module);
if mod_path.is_dir() {
mod_path = mod_path.join("mod");
}
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
crate::partialeq_to_none::PARTIALEQ_TO_NONE_INFO,
crate::pass_by_ref_or_value::LARGE_TYPES_PASSED_BY_VALUE_INFO,
crate::pass_by_ref_or_value::TRIVIALLY_COPY_PASS_BY_REF_INFO,
crate::paths_from_format::PATHS_FROM_FORMAT_INFO,
crate::pattern_type_mismatch::PATTERN_TYPE_MISMATCH_INFO,
crate::permissions_set_readonly_false::PERMISSIONS_SET_READONLY_FALSE_INFO,
crate::precedence::PRECEDENCE_INFO,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ mod partial_pub_fields;
mod partialeq_ne_impl;
mod partialeq_to_none;
mod pass_by_ref_or_value;
mod paths_from_format;
mod pattern_type_mismatch;
mod permissions_set_readonly_false;
mod precedence;
Expand Down Expand Up @@ -910,6 +911,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
store.register_late_pass(|_| Box::new(permissions_set_readonly_false::PermissionsSetReadonlyFalse));
store.register_late_pass(|_| Box::new(size_of_ref::SizeOfRef));
store.register_late_pass(|_| Box::new(multiple_unsafe_ops_per_block::MultipleUnsafeOpsPerBlock));
store.register_late_pass(|_| Box::new(paths_from_format::PathsFromFormat));
// add lints here, do not remove this comment, it's used in `new_lint`
}

Expand Down
132 changes: 132 additions & 0 deletions clippy_lints/src/paths_from_format.rs
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this, I think it would be better to suggest PathBuf directly. Even if that might require additional modifications by the user. PathBuf is intended for modifications as this one AFAIK :)

Though, in that case we should have the applicability be less than machine applicable

Copy link
Author

Choose a reason for hiding this comment

The 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?

  1. Replace .join() with .push()
  2. Replace the Path::new with PathBuf::from
  3. Instead of suggesting a replacement, just flag it out to the user

Copy link
Member

Choose a reason for hiding this comment

The 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 PathBuf::from() and then add segments using push(). The thing is, that this probably change the type of the buffer and require further adjustments by the user. Therefore, we should display the suggestion, but not make it machine applicable.

Does this make sense, or should I try to describe it better? :)

Copy link
Author

@merelymyself merelymyself Jan 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at the docs for PathBuf now, and I just realised that there might be a recommended solution for situations like this 😵‍💫.

let var = "path";

pathbuf = ["/", var, "filename.txt"].iter().collect::<PathBuf>();

Functionally equivalent to

let var = "path";

pathbuf = PathBuf::from(format!("/{var}/filename.txt"));

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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The 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 PathBuf and the first argument is a format macro. The lint logic should also check the first value of ExprKind::Call to ensure that it's a path to a function called new or other related function names.

The logic should also contain a check if the code originated from a macro. I believe the check should be as simple as !expr.span.from_expansion()

if let ty = cx.typeck_results().expr_ty(expr);
if is_type_diagnostic_item(cx, ty, sym::PathBuf);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe also check for calls to Path::new?

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;
Copy link
Member

Choose a reason for hiding this comment

The 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 Sugg::hir_with_applicability you can just use Sugg::hir

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting nit, as well as the other else cases in the if_chain!

Suggested change
}
else {
} else {

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
Copy link
Member

Choose a reason for hiding this comment

The 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",
Copy link
Member

Choose a reason for hiding this comment

The 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))`
Copy link
Member

Choose a reason for hiding this comment

The 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 //?/ which makes it a verbatim path under windows or (IMO the better approach) add a config value, to allow this. I have the feeling the lint can be very noisy without, but ignoring these cases can also be wrong.

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
19 changes: 19 additions & 0 deletions tests/ui/paths_from_format.rs
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"));
}
102 changes: 102 additions & 0 deletions tests/ui/paths_from_format.stderr
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