Skip to content

WIP: New lint: needless_path_new #14895

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

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bf8e204
cargo dev new_lint
ada4a Apr 22, 2025
5047489
add test
ada4a Apr 22, 2025
999cd13
add some docs
ada4a Apr 22, 2025
fcce0c8
try to impl LateLintPass for NeedlessPathNew
ada4a Apr 30, 2025
9ca18c9
initialize `check` with the output of `#[author]`
ada4a May 24, 2025
04b418c
borrow some stuff from mut_reference
ada4a May 25, 2025
b4f57b5
implements_asref_path
ada4a May 25, 2025
a1fb385
fix some stuff using cargo check
ada4a May 25, 2025
a4fec0e
match on `Path::new(x)` call
ada4a May 25, 2025
2df64de
is_path_new
ada4a May 25, 2025
72ccaae
instantiate `Path` as `GenericArg` to `AsRef`
ada4a May 25, 2025
15ab960
check that the parameter wants an `impl AsRef<Path>`
ada4a May 25, 2025
8c72115
add more tests
ada4a May 25, 2025
99bcb6d
give a suggestion
ada4a May 25, 2025
aa87d44
update lint message
ada4a May 25, 2025
6324a5d
update help message
ada4a May 25, 2025
cd6ff62
more verbose (and better?) lint message
ada4a May 25, 2025
bd2625e
"instantiate" to avoid `skip_binder`
ada4a May 25, 2025
c40aaf9
create `{asref_def_id,path_ty}` once, and reuse in the closure
ada4a May 25, 2025
ef2bf79
update lint declaration
ada4a May 25, 2025
0441c54
rewrite motivation for the `args.len() == 1` check
ada4a May 25, 2025
4dd365f
extract `cx.tcx` into a variable
ada4a May 26, 2025
580fa39
refactor: don't create the intermediate `TyKind::Adt`
ada4a May 26, 2025
cee5d5b
remove the `args.len()` check after all
ada4a May 26, 2025
1e0a3f2
test: method calls
ada4a Jun 3, 2025
5b20b1f
test: two params with the same generic
ada4a Jun 3, 2025
a5f908e
misc: pull `if` into match arm
ada4a Jun 3, 2025
f630401
test: function stored in a variable
ada4a Jun 3, 2025
d38e30d
rm unused params
ada4a Jun 3, 2025
c91bf12
failing test: `ExprKind::Call` which is not a function call
ada4a Jun 4, 2025
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 @@ -6097,6 +6097,7 @@ Released 2018-09-13
[`needless_parens_on_range_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_parens_on_range_literals
[`needless_pass_by_ref_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_ref_mut
[`needless_pass_by_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
[`needless_path_new`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_path_new
[`needless_pub_self`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_pub_self
[`needless_question_mark`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_question_mark
[`needless_range_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_range_loop
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 @@ -550,6 +550,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
crate::needless_pass_by_ref_mut::NEEDLESS_PASS_BY_REF_MUT_INFO,
crate::needless_pass_by_value::NEEDLESS_PASS_BY_VALUE_INFO,
crate::needless_path_new::NEEDLESS_PATH_NEW_INFO,
crate::needless_question_mark::NEEDLESS_QUESTION_MARK_INFO,
crate::needless_update::NEEDLESS_UPDATE_INFO,
crate::neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD_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 @@ -270,6 +270,7 @@ mod needless_maybe_sized;
mod needless_parens_on_range_literals;
mod needless_pass_by_ref_mut;
mod needless_pass_by_value;
mod needless_path_new;
mod needless_question_mark;
mod needless_update;
mod neg_cmp_op_on_partial_ord;
Expand Down Expand Up @@ -951,5 +952,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(|_| Box::new(cloned_ref_to_slice_refs::ClonedRefToSliceRefs::new(conf)));
store.register_late_pass(|_| Box::new(infallible_try_from::InfallibleTryFrom));
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
store.register_late_pass(|_| Box::new(needless_path_new::NeedlessPathNew));
// add lints here, do not remove this comment, it's used in `new_lint`
}
108 changes: 108 additions & 0 deletions clippy_lints/src/needless_path_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::path_res;
use clippy_utils::source::snippet;
use clippy_utils::ty::implements_trait;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, List, Ty};
use rustc_session::declare_lint_pass;
use rustc_span::sym;
use std::iter;

declare_clippy_lint! {
/// ### What it does
/// Detects expressions being enclosed in `Path::new` when passed to a function that accepts
/// `impl AsRef<Path>`, when the enclosed expression could be used.
///
/// ### Why is this bad?
/// It is unnecessarily verbose
///
/// ### Example
/// ```no_run
/// # use std::{fs, path::Path};
/// fs::write(Path::new("foo.txt"), "foo");
/// ```
/// Use instead:
/// ```no_run
/// # use std::{fs, path::Path};
/// fs::write("foo.txt", "foo");
/// ```
#[clippy::version = "1.89.0"]
pub NEEDLESS_PATH_NEW,
nursery,
"an argument passed to a function that accepts `impl AsRef<Path>` \
being enclosed in `Path::new` when the argument implements the trait"
}

declare_lint_pass!(NeedlessPathNew => [NEEDLESS_PATH_NEW]);

impl<'tcx> LateLintPass<'tcx> for NeedlessPathNew {
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) {
match e.kind {
ExprKind::Call(fn_expr, args) => {
check_arguments(cx, &mut args.iter(), cx.typeck_results().expr_ty(fn_expr));
},
ExprKind::MethodCall(_, receiver, arguments, _)
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(e.hir_id) =>
{
let args = cx.typeck_results().node_args(e.hir_id);
let method_type = cx.tcx.type_of(def_id).instantiate(cx.tcx, args);
check_arguments(cx, &mut iter::once(receiver).chain(arguments.iter()), method_type);
},
_ => (),
}
}
}

fn check_arguments<'tcx>(
cx: &LateContext<'tcx>,
arguments: &mut dyn Iterator<Item = &'tcx Expr<'tcx>>,
Copy link
Contributor Author

@ada4a ada4a May 26, 2025

Choose a reason for hiding this comment

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

I have this function signature from mut_reference -- should I maybe replace the dyn with impl here?

type_definition: Ty<'tcx>,
) {
let tcx = cx.tcx;
// whether `func` is `Path::new`
let is_path_new = |func: &Expr<'_>| {
if let ExprKind::Path(ref qpath) = func.kind
&& let QPath::TypeRelative(ty, path) = qpath
&& let Some(did) = path_res(cx, *ty).opt_def_id()
&& tcx.is_diagnostic_item(sym::Path, did)
&& path.ident.name == sym::new
{
true
} else {
false
}
};

let Some(path_def_id) = tcx.get_diagnostic_item(sym::Path) else {
return;
};
let path_ty = Ty::new_adt(tcx, tcx.adt_def(path_def_id), List::empty());
let Some(asref_def_id) = tcx.get_diagnostic_item(sym::AsRef) else {
return;
};

let implements_asref_path = |arg| implements_trait(cx, arg, asref_def_id, &[path_ty.into()]);

if let ty::FnDef(..) | ty::FnPtr(..) = type_definition.kind() {
let parameters = type_definition.fn_sig(tcx).skip_binder().inputs();
Copy link
Contributor Author

@ada4a ada4a May 29, 2025

Choose a reason for hiding this comment

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

I thought about this whole thing a bit more.

As I said towards the end of this comment, what we want to check is whether a particular parameter of a function is impl AsRef<Path>, i.e. find a p such that fn foo(p: impl AsRef<Path>).

But the latter is just syntax sugar for fn foo<P: AsRef<Path>>(p: P), so we are in fact looking for a parameter whose type is defined in a generic type parameter of the function.

Therefore I think we shouldn't actually calling skip_binder here. Without that, I get the following chain:

  1. The type of type_definition.fn_sig(tcx) is PolyFnSig, which is an alias to Binder<'tcx, FnSig<'tcx>>
  2. that one has a Binder::inputs method, which returns a Binder<'tcx, &'tcx [Ty<'tcx>]>
  3. I can kind of iterate over that using Binder::iter, which finally gives me impl Iterator<Item=Binder<'tcx, Ty<'tcx>>>
  4. and now I'm stuck. What I think I have achieved at this point is turn a function like fn foo<P, T>(p: P, t: T, n: u32) into something like [ p<P,T>: P, t<P, T>: T, n<P, T>: u32 ] (syntax very much pseudocode-ish). So from there I need to first "shed" the unnecessary generics, and then?... No idea to be honest

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, this slipped through, indeed Zulip would be a better choice for those kind of discussion. Have you read the documentation on ParamEnv? You could get the caller bounds that the caller has to adhere to for the parameter index you're interested in, and check whether the type of the argument would meet those bounds (so that for example a bound of impl AsRef<Path> + Clone would not receive an object which implements AsRef<Path> but is not Cloneable).

Also, you will have to check that the type of the parameter you're interested in does not appear in another parameter or a clause applying to another parameter type, as for example

fn foo<P: AsRef<Path>>(x: P, y: P) {}

must use the same type for both parameters, and you better let this untouched.

Does that help?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, the whole Analysis section from the dev-guide seems to be exactly what I need! Thank you very much for this and all the other pointers, I'll read them and see what I come up with –and ask on Zulip if I don't understand something

for (argument, parameter) in iter::zip(arguments, parameters) {
if let ExprKind::Call(func, args) = argument.kind
&& is_path_new(func)
&& implements_asref_path(cx.typeck_results().expr_ty(&args[0]))
&& implements_asref_path(*parameter)
{
span_lint_and_sugg(
cx,
NEEDLESS_PATH_NEW,
argument.span,
"the expression enclosed in `Path::new` implements `AsRef<Path>`",
"remove the enclosing `Path::new`",
format!("{}", snippet(cx, args[0].span, "..")),
Applicability::MachineApplicable,
);
}
}
}
}
54 changes: 54 additions & 0 deletions tests/ui/needless_path_new.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#![warn(clippy::needless_path_new)]

use std::fs;
use std::path::Path;

fn takes_path(_: &Path) {}

fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}

fn takes_two_impl_paths_with_the_same_generic<P: AsRef<Path>>(_: P, _: P) {}

struct Foo;

impl Foo {
fn takes_path(_: &Path) {}
fn takes_self_and_path(&self, _: &Path) {}
fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}
fn takes_self_and_path_and_impl_path(&self, _: &Path, _: impl AsRef<Path>) {}
}

fn main() {
let f = Foo;

fs::write("foo.txt", "foo"); //~ needless_path_new

fs::copy(
"foo", //~ needless_path_new
"bar", //~ needless_path_new
);

Foo::takes_path(Path::new("foo"));

f.takes_self_and_path_and_impl_path(
Path::new("foo"),
"bar", //~ needless_path_new
);

// we can and should change both at the same time
takes_two_impl_paths_with_the_same_generic(
"foo", //~ needless_path_new
"bar", //~ needless_path_new
);

// no warning
takes_path(Path::new("foo"));

// the paramater that _could_ be passed directly, was
// the parameter that could't, wasn't
takes_path_and_impl_path(Path::new("foo"), "bar");

// same but as a method
Foo::takes_path_and_impl_path(Path::new("foo"), "bar");
f.takes_self_and_path_and_impl_path(Path::new("foo"), "bar");
}
65 changes: 65 additions & 0 deletions tests/ui/needless_path_new.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#![warn(clippy::needless_path_new)]

use std::fs;
use std::path::Path;

fn takes_path(_: &Path) {}

fn takes_impl_path(_: impl AsRef<Path>) {}

fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}

fn takes_two_impl_paths_with_the_same_generic<P: AsRef<Path>>(_: P, _: P) {}

struct Foo;

impl Foo {
fn takes_path(_: &Path) {}
fn takes_self_and_path(&self, _: &Path) {}
fn takes_path_and_impl_path(_: &Path, _: impl AsRef<Path>) {}
fn takes_self_and_path_and_impl_path(&self, _: &Path, _: impl AsRef<Path>) {}
}

fn main() {
let f = Foo;

fs::write(Path::new("foo.txt"), "foo"); //~ needless_path_new

fs::copy(
Path::new("foo"), //~ needless_path_new
Path::new("bar"), //~ needless_path_new
);

Foo::takes_path(Path::new("foo"));

f.takes_self_and_path_and_impl_path(
Path::new("foo"),
Path::new("bar"), //~ needless_path_new
);

// we can and should change both at the same time
takes_two_impl_paths_with_the_same_generic(
Path::new("foo"), //~ needless_path_new
Path::new("bar"), //~ needless_path_new
);

let a = takes_impl_path;

a(Path::new("foo.txt")); //~ needless_path_new

// no warning
takes_path(Path::new("foo"));

// the paramater that _could_ be passed directly, was
// the parameter that could't, wasn't
takes_path_and_impl_path(Path::new("foo"), "bar");

// same but as a method
Foo::takes_path_and_impl_path(Path::new("foo"), "bar");
f.takes_self_and_path_and_impl_path(Path::new("foo"), "bar");

fn foo() -> Option<&'static Path> {
// Some(...) is `ExprKind::Call`, but we don't consider it
Some(Path::new("foo.txt"))
}
}
41 changes: 41 additions & 0 deletions tests/ui/needless_path_new.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:24:15
|
LL | fs::write(Path::new("foo.txt"), "foo");
| ^^^^^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo.txt"`
|
= note: `-D clippy::needless-path-new` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::needless_path_new)]`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:27:9
|
LL | Path::new("foo"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:28:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:35:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:40:9
|
LL | Path::new("foo"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"foo"`

error: the expression enclosed in `Path::new` implements `AsRef<Path>`
--> tests/ui/needless_path_new.rs:41:9
|
LL | Path::new("bar"),
| ^^^^^^^^^^^^^^^^ help: remove the enclosing `Path::new`: `"bar"`

error: aborting due to 6 previous errors

Loading